Animating UICollectionView contentOffset does not display non-visible cells Animating UICollectionView contentOffset does not display non-visible cells ios ios

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.