I have a UITableView
with paging turned on so I can flip through each cell (which is the height of the entire tableView) like pages. This works amazing
Until I force an orientation change and back.
Now it pages between cells. So parts of both cells are visible. It continues to page like that until I scroll the tableView all the way up to the top. Then it resets, and you can keep scrolling down correctly.
With this information in mind, I realized I just needed to simply set the contentOffset to be the value it was before.
If i manually set the content offset to be like 3000 (lets pretend the scroll view is 1000 points high)
That should scroll to index 3
But after the rotate if I do that, it scrolls me to like index 16. But it still says the offset is 3000 when i print from scrollViewDidScroll
As I scroll up, the content offset goes nearly to zero before the scrollView recalculates and decides that it can’t be negative offset yet and adds another 1000 points to the offset. Each time i scroll up it goes to zero, and then re-adds 1000 to the offset. Eventually it repeats itself until you are actually at offset 0 at the top, and then you can scroll and everything works fine.
Here is an example of what's happening:
I have created a sample project to get this to work:
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))"
}
}
You needed a couple things...
First, we need to give the tableView an estimated row height. We can do this here:
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
}
}
Next, after the rotation, we need to tell the tableView to recalculate its layout. The proper place to handle "device rotation" actions is here, as the tableView can change size due to other factors (not in this test controller, but in general):
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))"
})
}
Here is your complete example, with those two funcs and minor edits (removed unused code, changed the labelOffset.text
, etc... see the comments in the code):
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))"
}
}