UICollectionview scrollToItemAtIndexPath, not loading visible cells until animation complete
The only answer I have found, is to add a few seconds to the animation. That way the cells are loaded when the scroll arrives
xcode CollectionViewController scrollToItemAtIndexPath not working
Whether it's a bug or a feature, UIKit throws this error whenever scrollToItemAtIndexPath:atScrollPosition:Animated
is called before UICollectionView
has laid out its subviews.
As a workaround, move your scrolling invocation to a place in the view controller lifecycle where you're sure it has already computed its layout, like so:
@implementation CollectionViewControllerSubclass
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// scrolling here doesn't work (results in your assertion failure)
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
NSIndexPath *indexPath = // compute some index path
// scrolling here does work
[self.collectionView scrollToItemAtIndexPath:indexPath
atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally
animated:YES];
}
@end
At the very least, the error message should probably be more helpful. I've opened a rdar://13416281; please dupe.
How UICollectionView.scrollToItemAtIndexPath works?
In my opinion, even you call "scrollToItemAtIndexPath" without animation, however, when it is executed, cells between the last visible cell to the last cell will be created and rendered. Moreover, the collection view will layout subviews during that time. So that your "print" call will be executed before. I printed out "cellForItemAtIndexPath" to prove that. One more thing is if you call "print" delay after 0.5s for example, it will print the last cell.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.collectionView.delegate = self
self.collectionView.dataSource = self
self.collectionView.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: "cellIdentifier")
}
@IBOutlet weak var collectionView: UICollectionView!
@IBAction func didButtonPress(sender: AnyObject) {
let section = 0
let lastItemIndex = self.collectionView.numberOfItemsInSection(section) - 1
let indexPath:NSIndexPath = NSIndexPath.init(forItem: lastItemIndex, inSection: section)
self.collectionView.scrollToItemAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: false)
let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(0.5 * Double(NSEC_PER_SEC)))
dispatch_after(delayTime, dispatch_get_main_queue()) {
print(self.collectionView.indexPathsForVisibleItems())
}
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return CGSizeMake(80, 80)
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 60
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cellIdentifier", forIndexPath: indexPath)
cell.backgroundColor = UIColor.redColor()
print(indexPath)
return cell
}
I hope this will help you.
Animating UICollectionView contentOffset does not display non-visible cells
You should simply add [self.view layoutIfNeeded];
inside the animation block, like so:
[UIView animateWithDuration:((self.collectionView.collectionViewLayout.collectionViewContentSize.width - self.collectionView.contentOffset.x) / 75) delay:0 options:(UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionRepeat | UIViewAnimationOptionBeginFromCurrentState) animations:^{
self.collectionView.contentOffset = CGPointMake(self.collectionView.collectionViewLayout.collectionViewContentSize.width, 0);
[self.view layoutIfNeeded];
} completion:nil];
UICollectionView scrollToItemAtIndex after button action executed
since you are not dismissing but popping the viewcontroller something like this should work:
override func viewWillDisappear(animated: Bool) {
if let mainViewController = navigationController?.topViewController as? MainViewController {
mainViewController.collectionView?.scrollToItemAtIndexPath(NSIndexPath(forItem: currentIndex, inSection: 0), atScrollPosition: .CenteredVertically, animated: false)
}
super.viewWillDisappear(animated)
}
Reload data calling all cells which are not visible in view
You can do the following:
tableView.beginUpdates()
tableView.reloadRows(at: tableView.indexPathsForVisibleRows ?? [IndexPath](), with: .none)
tableView.endUpdates()
Animated scroll-to-item in UICollectionView doesn't always work
I've just run in to a similar / the same issue after adding an item to a UICollectionView
.
What's happening
The issue seems to be that immediately following [collectionView reloadData]
or [collectionView insertItemsAtIndexPaths: @[newItemIndexPath]]
, the collection view's content size is not yet updated.
If you then try to scroll the added item visible, it will fail because the content rect doesn't yet include space for the new item.
A fix
There is a simple and fairly robust work around, which is to post the scroll event on to the next iteration of the run loop like this:
const NSUInteger newIndex =
[self collectionView: self.collectionView numberOfItemsInSection: 0] - 1;
NSIndexPath *const newPath =
[NSIndexPath indexPathForItem: newIndex
inSection: 0];
[self.collectionView insertItemsAtIndexPaths: @[newPath]];
UICollectionViewLayoutAttributes *const layoutAttributes =
[self.collectionView layoutAttributesForItemAtIndexPath: newPath];
dispatch_async(dispatch_get_main_queue(), ^{
[self.collectionView scrollRectToVisible: layoutAttributes.frame
animated: YES];
});
Isn't there a nicer fix?
While it works, this "post the scroll call on the next run loop tick" shenanigans feels hacky.
It would be preferable if UICollectionView
could invoke a callback when it finished updating the content rect. UIKit has callbacks of this style for other methods that perform asynchronous updates to its model. For example, a completion block for UIViewController
transitions and UIView
animations.
UICollectionView
does not provide this callback. As far as I know, there is no other simple clean way to find when it completes its update. In the absence of this, the next run loop tick is a viable horse proxy for the callback unicorn we would prefer to use.
Anything else to know?
It's probably useful to know that UITableView
also has this issue. A similar workaround should work there too.
Related Topics
A Way to Inherit from Multiple Classes
How to Compile "Hello World" Program with Swift on Ubuntu 14.04
Why Do We Need to Explicitly Cast the Optional to Any
Scaling Down a Text's Font Size to Fit Its Length to Another Text in Swiftui
Hide Status Bar in Launch Screen
Swift:How to Handle a Lot of Textures in Memory
Avspeechsynthesizer Errors in iOS 10
Protocol Having Generic Function and Associatedtype
How to Save Value in Nsset Core Data (Swift)
How to Add an Admob Gadbannerview to Every View
Compiler Cannot Infer Return Type
Custom Intialiser on Primitive Types for JSONdecoder
How to Get Distinct Results from a Single Field in Core Data (Swift 4)
Swift Memory Management: Storing Func in Var