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];
You could try using a CADisplayLink to drive the animation yourself. This is not too hard to set up since you are using a Linear animation curve anyway. Here's a basic implementation that may work for you:
@property (nonatomic, strong) CADisplayLink *displayLink;@property (nonatomic, assign) CFTimeInterval lastTimerTick;@property (nonatomic, assign) CGFloat animationPointsPerSecond;@property (nonatomic, assign) CGPoint finalContentOffset;-(void)beginAnimation { self.lastTimerTick = 0; self.animationPointsPerSecond = 50; self.finalContentOffset = CGPointMake(..., ...); self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkTick:)]; [self.displayLink setFrameInterval:1]; [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];}-(void)endAnimation { [self.displayLink invalidate]; self.displayLink = nil;}-(void)displayLinkTick { if (self.lastTimerTick = 0) { self.lastTimerTick = self.displayLink.timestamp; return; } CFTimeInterval currentTimestamp = self.displayLink.timestamp; CGPoint newContentOffset = self.collectionView.contentOffset; newContentOffset.x += self.animationPointsPerSecond * (currentTimestamp - self.lastTimerTick) self.collectionView.contentOffset = newContentOffset; self.lastTimerTick = currentTimestamp; if (newContentOffset.x >= self.finalContentOffset.x) [self endAnimation];}
I've built upon what's already in these answers and made a generic manual animator, as everything can be distilled down to a percentage float value and a block.
class ManualAnimator { enum AnimationCurve { case linear, parametric, easeInOut, easeIn, easeOut func modify(_ x: CGFloat) -> CGFloat { switch self { case .linear: return x case .parametric: return x.parametric case .easeInOut: return x.quadraticEaseInOut case .easeIn: return x.quadraticEaseIn case .easeOut: return x.quadraticEaseOut } } } private var displayLink: CADisplayLink? private var start = Date() private var total = TimeInterval(0) private var closure: ((CGFloat) -> Void)? private var animationCurve: AnimationCurve = .linear func animate(duration: TimeInterval, curve: AnimationCurve = .linear, _ animations: @escaping (CGFloat) -> Void) { guard duration > 0 else { animations(1.0); return } reset() start = Date() closure = animations total = duration animationCurve = curve let d = CADisplayLink(target: self, selector: #selector(tick)) d.add(to: .current, forMode: .common) displayLink = d } @objc private func tick() { let delta = Date().timeIntervalSince(start) var percentage = animationCurve.modify(CGFloat(delta) / CGFloat(total)) //print("%:", percentage) if percentage < 0.0 { percentage = 0.0 } else if percentage >= 1.0 { percentage = 1.0; reset() } closure?(percentage) } private func reset() { displayLink?.invalidate() displayLink = nil }}extension CGFloat { fileprivate var parametric: CGFloat { guard self > 0.0 else { return 0.0 } guard self < 1.0 else { return 1.0 } return ((self * self) / (2.0 * ((self * self) - self) + 1.0)) } fileprivate var quadraticEaseInOut: CGFloat { guard self > 0.0 else { return 0.0 } guard self < 1.0 else { return 1.0 } if self < 0.5 { return 2 * self * self } return (-2 * self * self) + (4 * self) - 1 } fileprivate var quadraticEaseOut: CGFloat { guard self > 0.0 else { return 0.0 } guard self < 1.0 else { return 1.0 } return -self * (self - 2) } fileprivate var quadraticEaseIn: CGFloat { guard self > 0.0 else { return 0.0 } guard self < 1.0 else { return 1.0 } return self * self }}
Implementation
let initialOffset = collectionView.contentOffset.ylet delta = collectionView.bounds.size.heightlet animator = ManualAnimator()animator.animate(duration: TimeInterval(1.0), curve: .easeInOut) { [weak self] (percentage) in guard let `self` = self else { return } self.collectionView.contentOffset = CGPoint(x: 0.0, y: initialOffset + (delta * percentage)) if percentage == 1.0 { print("Done") }}
It might be worth combining the animate function with an init method.. it's not a huge deal though.