方向更改后 UIScrollView contentOffset 损坏
UIScrollView contentOffset corrupted after orientation change
我有一个 UITableView
打开了分页,所以我可以像翻页一样翻阅每个单元格(这是整个 tableView 的高度)。这太棒了
直到我强制改变方向并返回。
现在它可以在单元格之间翻页。所以两个单元格的一部分都是可见的。它会继续这样翻页,直到我将 tableView 一直滚动到顶部。然后它会重置,您可以继续正确向下滚动。
考虑到这些信息,我意识到我只需要简单地将 contentOffset 设置为之前的值。
如果我手动将内容偏移设置为 3000(假设滚动视图高 1000 点)
应该滚动到索引 3
但是在旋转之后,如果我这样做,它会滚动我喜欢索引 16。但是当我从 scrollViewDidScroll
打印时它仍然说偏移量是 3000
当我向上滚动时,在 scrollView 重新计算并决定它不能为负偏移并向偏移添加另外 1000 点之前,内容偏移几乎变为零。每次我向上滚动时它都会变为零,然后将 1000 重新添加到偏移量。
最终它会自己重复,直到您实际上位于顶部的偏移量 0,然后您可以滚动并且一切正常。
这是一个正在发生的事情的例子:
我创建了一个示例项目来让它工作:
class TestViewController: UIViewController {
var isLandscape = false
var tableView: UITableView!
var buttonRotate: UIButton!
var labelOffset: UILabel!
let numberOfItems = 30
override func viewDidLoad() {
super.viewDidLoad()
tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.dataSource = self
tableView.delegate = self
tableView.isPagingEnabled = true
buttonRotate = UIButton()
buttonRotate.backgroundColor = .lightGray
buttonRotate.addTarget(self, action: #selector(clickedRotate(_:)), for: .touchUpInside)
buttonRotate.translatesAutoresizingMaskIntoConstraints = false
buttonRotate.setTitle("Rotate", for: .normal)
view.addSubview(buttonRotate)
buttonRotate.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
buttonRotate.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
labelOffset = UILabel()
labelOffset.translatesAutoresizingMaskIntoConstraints = false
labelOffset.text = "Offset: \(tableView.contentOffset.y)"
view.addSubview(labelOffset)
labelOffset.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
labelOffset.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
}
@IBAction func clickedRotate(_ sender: Any) {
self.isLandscape = !self.isLandscape
if self.isLandscape {
let value = UIInterfaceOrientation.landscapeRight.rawValue
UIDevice.current.setValue(value, forKey: "orientation")
} else {
let value = UIInterfaceOrientation.portrait.rawValue
UIDevice.current.setValue(value, forKey: "orientation")
}
}
//This fixes it by temporarily bringing the offset to zero, which is the same as scrolling to the top. After this it scrolls back to the correct place. It needs to be separated by 0.1 seconds to work
// override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
// coordinator.animate(alongsideTransition: { context in
// }) { (context) in
// self.printScrollValues()
// self.tableView.contentOffset.y = 0
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// self.resetContentOffset()
// }
// }
//
// }
func resetContentOffset() {
let size = tableView.frame.size
let index = 3
let offset = size.height * CGFloat(index)
print("\n\noffset: \(offset)")
tableView.contentOffset.y = offset
}
}
extension TestViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return tableView.frame.size.height
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return numberOfItems
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
cell.textLabel?.text = indexPath.row.description
cell.textLabel?.font = .systemFont(ofSize: 40)
let index = indexPath.row
if index % 2 == 0 {
cell.backgroundColor = .yellow
} else {
cell.backgroundColor = .blue
}
return cell
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(scrollView.contentOffset.y)
labelOffset.text = "Offset: \(Int(scrollView.contentOffset.y))"
}
}
您需要一些东西...
首先,我们需要给 tableView 一个估计的行高。我们可以在这里这样做:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// only need to call this if tableView size has changed
if tableView.estimatedRowHeight != tableView.frame.size.height {
tableView.estimatedRowHeight = tableView.frame.size.height
}
}
接下来,在旋转之后,我们需要告诉tableView重新计算它的布局。处理“设备旋转”操作的正确位置在这里,因为 tableView 可以 由于其他因素(不是在这个测试控制器中,但一般情况下)改变大小:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: nil, completion: {
_ in
// tell the tableView to recalculate its layout
self.tableView.performBatchUpdates(nil, completion: nil)
self.labelOffset.text = "Offset: \(Int(self.tableView.contentOffset.y)) ContentSize: \(Int(self.tableView.contentSize.height))"
})
}
这是您的完整示例,包含这两个函数和少量修改(删除未使用的代码,更改 labelOffset.text
,等等...请参阅代码中的注释):
class TestRotateViewController: UIViewController {
var isLandscape = false
var tableView: UITableView!
var buttonRotate: UIButton!
var labelOffset: UILabel!
let numberOfItems = 30
override func viewDidLoad() {
super.viewDidLoad()
tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.dataSource = self
tableView.delegate = self
tableView.isPagingEnabled = true
buttonRotate = UIButton()
buttonRotate.backgroundColor = .lightGray
buttonRotate.addTarget(self, action: #selector(clickedRotate(_:)), for: .touchUpInside)
buttonRotate.translatesAutoresizingMaskIntoConstraints = false
buttonRotate.setTitle("Rotate", for: .normal)
view.addSubview(buttonRotate)
buttonRotate.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
buttonRotate.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
labelOffset = UILabel()
labelOffset.translatesAutoresizingMaskIntoConstraints = false
labelOffset.text = "Offset: \(tableView.contentOffset.y)"
view.addSubview(labelOffset)
labelOffset.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
labelOffset.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// only need to call this if tableView size has changed
if tableView.estimatedRowHeight != tableView.frame.size.height {
tableView.estimatedRowHeight = tableView.frame.size.height
}
}
// viewDidAppear implemented ONLY to update the labelOffset text
// this is NOT needed for functionality
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
labelOffset.text = "Offset: \(Int(tableView.contentOffset.y)) ContentSize: \(Int(tableView.contentSize.height))"
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: nil, completion: {
_ in
// tell the tableView to recalculate its layout
self.tableView.performBatchUpdates(nil, completion: nil)
self.labelOffset.text = "Offset: \(Int(self.tableView.contentOffset.y)) ContentSize: \(Int(self.tableView.contentSize.height))"
})
}
@IBAction func clickedRotate(_ sender: Any) {
self.isLandscape = !self.isLandscape
if self.isLandscape {
let value = UIInterfaceOrientation.landscapeRight.rawValue
UIDevice.current.setValue(value, forKey: "orientation")
} else {
let value = UIInterfaceOrientation.portrait.rawValue
UIDevice.current.setValue(value, forKey: "orientation")
}
}
}
extension TestRotateViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return tableView.frame.size.height
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return numberOfItems
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = indexPath.row.description
cell.textLabel?.font = .systemFont(ofSize: 40)
let index = indexPath.row
if index % 2 == 0 {
cell.backgroundColor = .yellow
} else {
// light-blue to make it easier to read the black label text
cell.backgroundColor = UIColor(red: 0.0, green: 0.75, blue: 1.0, alpha: 1.0)
}
return cell
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
labelOffset.text = "Offset: \(Int(scrollView.contentOffset.y)) ContentSize: \(Int(scrollView.contentSize.height))"
}
}
我有一个 UITableView
打开了分页,所以我可以像翻页一样翻阅每个单元格(这是整个 tableView 的高度)。这太棒了
直到我强制改变方向并返回。
现在它可以在单元格之间翻页。所以两个单元格的一部分都是可见的。它会继续这样翻页,直到我将 tableView 一直滚动到顶部。然后它会重置,您可以继续正确向下滚动。
考虑到这些信息,我意识到我只需要简单地将 contentOffset 设置为之前的值。
如果我手动将内容偏移设置为 3000(假设滚动视图高 1000 点)
应该滚动到索引 3
但是在旋转之后,如果我这样做,它会滚动我喜欢索引 16。但是当我从 scrollViewDidScroll
当我向上滚动时,在 scrollView 重新计算并决定它不能为负偏移并向偏移添加另外 1000 点之前,内容偏移几乎变为零。每次我向上滚动时它都会变为零,然后将 1000 重新添加到偏移量。 最终它会自己重复,直到您实际上位于顶部的偏移量 0,然后您可以滚动并且一切正常。
这是一个正在发生的事情的例子:
我创建了一个示例项目来让它工作:
class TestViewController: UIViewController {
var isLandscape = false
var tableView: UITableView!
var buttonRotate: UIButton!
var labelOffset: UILabel!
let numberOfItems = 30
override func viewDidLoad() {
super.viewDidLoad()
tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.dataSource = self
tableView.delegate = self
tableView.isPagingEnabled = true
buttonRotate = UIButton()
buttonRotate.backgroundColor = .lightGray
buttonRotate.addTarget(self, action: #selector(clickedRotate(_:)), for: .touchUpInside)
buttonRotate.translatesAutoresizingMaskIntoConstraints = false
buttonRotate.setTitle("Rotate", for: .normal)
view.addSubview(buttonRotate)
buttonRotate.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
buttonRotate.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
labelOffset = UILabel()
labelOffset.translatesAutoresizingMaskIntoConstraints = false
labelOffset.text = "Offset: \(tableView.contentOffset.y)"
view.addSubview(labelOffset)
labelOffset.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
labelOffset.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
}
@IBAction func clickedRotate(_ sender: Any) {
self.isLandscape = !self.isLandscape
if self.isLandscape {
let value = UIInterfaceOrientation.landscapeRight.rawValue
UIDevice.current.setValue(value, forKey: "orientation")
} else {
let value = UIInterfaceOrientation.portrait.rawValue
UIDevice.current.setValue(value, forKey: "orientation")
}
}
//This fixes it by temporarily bringing the offset to zero, which is the same as scrolling to the top. After this it scrolls back to the correct place. It needs to be separated by 0.1 seconds to work
// override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
// coordinator.animate(alongsideTransition: { context in
// }) { (context) in
// self.printScrollValues()
// self.tableView.contentOffset.y = 0
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// self.resetContentOffset()
// }
// }
//
// }
func resetContentOffset() {
let size = tableView.frame.size
let index = 3
let offset = size.height * CGFloat(index)
print("\n\noffset: \(offset)")
tableView.contentOffset.y = offset
}
}
extension TestViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return tableView.frame.size.height
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return numberOfItems
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
cell.textLabel?.text = indexPath.row.description
cell.textLabel?.font = .systemFont(ofSize: 40)
let index = indexPath.row
if index % 2 == 0 {
cell.backgroundColor = .yellow
} else {
cell.backgroundColor = .blue
}
return cell
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(scrollView.contentOffset.y)
labelOffset.text = "Offset: \(Int(scrollView.contentOffset.y))"
}
}
您需要一些东西...
首先,我们需要给 tableView 一个估计的行高。我们可以在这里这样做:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// only need to call this if tableView size has changed
if tableView.estimatedRowHeight != tableView.frame.size.height {
tableView.estimatedRowHeight = tableView.frame.size.height
}
}
接下来,在旋转之后,我们需要告诉tableView重新计算它的布局。处理“设备旋转”操作的正确位置在这里,因为 tableView 可以 由于其他因素(不是在这个测试控制器中,但一般情况下)改变大小:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: nil, completion: {
_ in
// tell the tableView to recalculate its layout
self.tableView.performBatchUpdates(nil, completion: nil)
self.labelOffset.text = "Offset: \(Int(self.tableView.contentOffset.y)) ContentSize: \(Int(self.tableView.contentSize.height))"
})
}
这是您的完整示例,包含这两个函数和少量修改(删除未使用的代码,更改 labelOffset.text
,等等...请参阅代码中的注释):
class TestRotateViewController: UIViewController {
var isLandscape = false
var tableView: UITableView!
var buttonRotate: UIButton!
var labelOffset: UILabel!
let numberOfItems = 30
override func viewDidLoad() {
super.viewDidLoad()
tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.dataSource = self
tableView.delegate = self
tableView.isPagingEnabled = true
buttonRotate = UIButton()
buttonRotate.backgroundColor = .lightGray
buttonRotate.addTarget(self, action: #selector(clickedRotate(_:)), for: .touchUpInside)
buttonRotate.translatesAutoresizingMaskIntoConstraints = false
buttonRotate.setTitle("Rotate", for: .normal)
view.addSubview(buttonRotate)
buttonRotate.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
buttonRotate.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
labelOffset = UILabel()
labelOffset.translatesAutoresizingMaskIntoConstraints = false
labelOffset.text = "Offset: \(tableView.contentOffset.y)"
view.addSubview(labelOffset)
labelOffset.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
labelOffset.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// only need to call this if tableView size has changed
if tableView.estimatedRowHeight != tableView.frame.size.height {
tableView.estimatedRowHeight = tableView.frame.size.height
}
}
// viewDidAppear implemented ONLY to update the labelOffset text
// this is NOT needed for functionality
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
labelOffset.text = "Offset: \(Int(tableView.contentOffset.y)) ContentSize: \(Int(tableView.contentSize.height))"
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: nil, completion: {
_ in
// tell the tableView to recalculate its layout
self.tableView.performBatchUpdates(nil, completion: nil)
self.labelOffset.text = "Offset: \(Int(self.tableView.contentOffset.y)) ContentSize: \(Int(self.tableView.contentSize.height))"
})
}
@IBAction func clickedRotate(_ sender: Any) {
self.isLandscape = !self.isLandscape
if self.isLandscape {
let value = UIInterfaceOrientation.landscapeRight.rawValue
UIDevice.current.setValue(value, forKey: "orientation")
} else {
let value = UIInterfaceOrientation.portrait.rawValue
UIDevice.current.setValue(value, forKey: "orientation")
}
}
}
extension TestRotateViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return tableView.frame.size.height
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return numberOfItems
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = indexPath.row.description
cell.textLabel?.font = .systemFont(ofSize: 40)
let index = indexPath.row
if index % 2 == 0 {
cell.backgroundColor = .yellow
} else {
// light-blue to make it easier to read the black label text
cell.backgroundColor = UIColor(red: 0.0, green: 0.75, blue: 1.0, alpha: 1.0)
}
return cell
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
labelOffset.text = "Offset: \(Int(scrollView.contentOffset.y)) ContentSize: \(Int(scrollView.contentSize.height))"
}
}