Search code examples
swiftuipageviewcontrolleruiviewanimation

How do I animate looping images in a UIPageViewController?


I'm in the process of building a walkthrough for my app, and I'm running into some strange behavior.

Some context on the setup: In my Walkthrough storyboard, I have a view controller called TutorialViewController that has a welcome message, a 'tutorial' button, and a 'skip' button. The 'tutorial' button triggers a segue to my WalkthroughViewController, which has next/back/close buttons and a UIPageIndicator floating over a UIContainerView. Embedded in that UIContainerView is my WalkthroughPageViewController. Finally, I have a WalkthroughContentViewController with two UIImageViews - contentImageView and contentImageViewMask - that is used as the data source for the WalkthroughPageViewController. Through swiping or pressing the navigation buttons, the next or previous view controller is instantiated (though it's really only reusing my WalkthroughContentViewController).

The goal: I'd ideally like to constantly loop an animation through an array of image strings for each page of the walkthrough. For example, if page 1 has an array of 3 image strings, the contentImageView and contentImageViewMask would alternate animating (through alpha values) from imageArray[0] -> imageArray[1] -> imageArray[2] -> imageArray[0] -> imageArray[1] etc.

Each page has a different number of image strings in its imageArray, and I haven't figured out how to smoothly loop through all of them yet. The code I have so far animates between the first two image strings in the array and comes from this answer, which lives in my WalkthroughContentViewController.swift file:

override func viewDidLoad() {
    super.viewDidLoad()

    contentImageViewMask.alpha = 0
    contentImageView.image = UIImage(named: imageArray[0])

    UIView.animate(withDuration: 1.5, delay: 1.5, options: [UIViewAnimationOptions.autoreverse, UIViewAnimationOptions.repeat], animations: {
        self.contentImageViewMask.image = UIImage(named: self.imageArray[1])
        self.contentImageViewMask.alpha = 1
    }, completion: nil)

}

The above sort of works in that, upon pressing the tutorial button, page 1 is immediately set to imageArray[1] (incorrect behavior), while page 2 smoothly animates between imageArray[0] and imageArray[1] (correct behavior). However, swiping more than one page past page 2 deactivates the animation on page 2. Every once in a while, page 1 will start animating correctly at this point, but I can't seem to figure out a reliable way to reproduce this. Here's a video showing the behavior. I took this video prior to hooking up the UIPageIndicator and button actions, so ignore that, but the weird thing is that the page 2 animation only happens through swiping to the page. If you get to page 2 via the next button, the animation doesn't work.

Two things that might be relevant: 1.) In my WalkthroughPageViewController.swift file, I have implemented the two required methods for UIPageViewControllerDataSource, viewControllerBefore and viewControllerAfter, and 2.) in my WalkthroughPageViewController.swift file, I have the following code to alert the delegate when the swipe animation has ended and the next page is fully displayed:

func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {

    if completed {
        if let contentViewController = pageViewController.viewControllers?.first as? WalkthroughContentViewController {
            currentIndex = contentViewController.index
            walkthroughDelegate?.didUpdatePageIndex(currentIndex: contentViewController.index)
        }
    }

}

Any insight would be greatly appreciated.


Solution

  • For anyone looking to do something similar but with a bit of added transitional flexibility (like having images fade to the next), this answer helped me achieve my animation goal of transitioning between images in a way that wasn't jarring.

    @IBOutlet weak var contentImageView: UIImageView!
    
    var index = 0
    let animationDuration: TimeInterval = 0.5
    let switchingInterval: TimeInterval = 2.0
    
    var imageStringArray: [String] = [""]
    var images = [UIImage]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
    
        for string in imageStringArray {
            images.append(UIImage(named: string)!)
        }
    
        contentImageView.image = images[index]
        animateImageView()
    
    }
    
    func animateImageView() {
    
        CATransaction.begin()
    
        CATransaction.setAnimationDuration(animationDuration)
        CATransaction.setCompletionBlock {
            DispatchQueue.main.asyncAfter(deadline: .now() + self.switchingInterval) {
                self.animateImageView()
            }
        }
    
        let transition = CATransition()
        transition.type = kCATransitionFade
    
        contentImageView.layer.add(transition, forKey: kCATransition)
        contentImageView.image = images[index]
    
        CATransaction.commit()
        index = index < images.count - 1 ? index + 1 : 0
    }
    

    It's maybe worth noting that the original answer on that question said that, "Implementing it as a custom image view would be better."

    If you only need to animate between images and the transition doesn't matter, then animatedImage is probably a smarter solution (as matt said above).

        contentImageView.image = UIImage.animatedImage(with: images, duration: 3)