UITableView does not handle contentOffset correctly when scrolling/paging
From your sample project, I can understand is you are trying to implement infinite scroll through the window content concept so that you can always have fixed number of rows (index paths ,lets say 100) so that when window scrolls down/up - table view remove indexPaths from top/bottom accordingly.
And even though you have more data source item you can always have tableView of indexPaths 100
Basically you are dealing with two problem here:
ContentOffset
Dynamic height
Let's assume we have height is fixed (44) and table is not inverted.
To implement Window for infinite scrolling you have to do following:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let bottom: CGFloat = scrollView.contentSize.height - scrollView.frame.size.height
let buffer: CGFloat = 3 * 44
let scrollPosition = scrollView.contentOffset.y
if (scrollPosition > bottom - buffer) {
dataSource.expose(dataSource.exposedRange.shift(by: 25))
self.tableView.contentOffset.y -= self.dataSource.deltaHeightToRemove
}
}
- Decide how much height buffer you need to keep when scroll goes down. This height buffer is the height after which you decides to insert some more item (25) into the datasource.
- At this point you now have to remove items from the top
- As you remove item from top you are basically telling scrollView to reduce it's content-offset by same height.
In this way total content size will be fixed every time
Hope it will help.
EDIT:-
Here is the modified code which actually does infinite scroll at bottom of
table view with dynamic cell height. This doesn't increase the rows count more than 100. but still loads data in sliding window.
link
How to get UITableView last scroll position before show next UIViewController?
By using contentOffset
.
Save it on leaving:
float savedVerticalContentOffset = tableView.contentOffset.y;
and then when you return and refresh just call:
[tableView setContentOffset:CGPointMake(0, savedVerticalContentOffset)];
How to get the scroll position of a UITableView in real time
Small gotcha - in Swift 3 it's
func scrollViewDidScroll(_ scrollView: UIScrollView)
and the below is not going to be triggered :
func scrollViewDidScroll(scrollView: UIScrollView)
Setting content offset of a UITableView not working when it's scrolling
if i got you right the following should work:
func scrollViewWillBeginDecelerating(scrollView: UIScrollView) {
scrollTableView()
}
Observe current contentOffset during UiTableView's delete rows animation
Ok, guy! I have a good piece of news for you =] You can use Layer Presentation Tree to observe UI values each moment.
- Wrap tableView updates into CATransaction
let displayLink = CADisplayLink(target: self, selector: #selector(adjustHeader))
displayLink.add(to: .current, forMode: .common)
CATransaction.begin()
CATransaction.setCompletionBlock(displayLink.invalidate)
// do tableView updates here
CATransaction.commit()
- Use layer presentation tree to calculate current offset. For example:
@objc private func adjustHeader() {
guard
let headerPresentationLayer = headerView.layer.presentation(),
let viewPresentationLayer = view.layer.presentation(),
let tableViewPresentationLayer = tableView.layer.presentation()
else { return }
let origin = tableViewPresentationLayer.convert(headerPresentationLayer.frame.origin, to: viewPresentationLayer)
print("LayerPresentationTree: \(origin)")
offsetDepandableView.frame.origin = origin
}
Remembering state: content offset and table cell reuse
If I'm understanding this correctly, you have a scrollview inside each cell that you want to remember the position of. What you need to do is save the content offset along with the text your UITableViewDataSource
is providing and set it in your cellForRowAtIndexPath
method.
UIScrollView contentOffset corrupted after orientation change
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))"
}
}
Related Topics
How to Center Align the Cells of a Uicollectionview
Storing Images Locally on an iOS Device
Date to Milliseconds and Back to Date in Swift
Swift: Can't Get Nsdate Dateformatter Right
Uiimage Imagenamed Returns Nil
iOS How to Detect When App Was Removed from Process
Missing Private Key in the Distribution Certificate on Keychain
"Do Not Embed", "Embed & Sign", "Embed Without Signing". What Are They. What They Do
Swift - How Creating Custom Viewforheaderinsection, Using a Xib File
How to Store an Image in Core Data
View with Continuous Scroll; Both Horizontal and Vertical
Command /Usr/Bin/Codesign Failed with Exit Code 1
How to Disable Caching in Alamofire