iosswiftuipageviewcontroller

UIPageViewController transition bugged if animation starts in the meanwhile


I have a strange behaviour in my app using a UIPageViewController. The layout of my app is a PageViewController (camera roll like) with a ads banner on bottom.

The banner's container starts as hidden and, when the ad gets loaded, i set the isHidden=false with an animation.

My problem is that when the banner gets into the screen it breaks the UIPageViewController transition if in progress as shown in this video:

enter image description here

I made a new project that reproduces the error very easy with a few lines, you can checkout it in GITHUB: You just need to spam the "Next" button until the banner gets loaded. It also can be reproduced by swipping the PageViewController but is harder to reproduce.

The full example code is:

class TestViewController: UIViewController,UIPageViewControllerDelegate,UIPageViewControllerDataSource {
    @IBOutlet weak var constraintAdviewHeight: NSLayoutConstraint!
    weak var pageViewController : UIPageViewController?

    @IBOutlet weak var containerAdView: UIView!
    @IBOutlet weak var adView: UIView!
    @IBOutlet weak var containerPager: UIView!

    var currentIndex = 0;
    var clickEnabled = true

    override func viewDidLoad() {
        super.viewDidLoad()
        let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
        pageViewController = pageVC
        pageVC.delegate = self
        pageVC.dataSource = self
        addChildViewController(pageVC)
        pageVC.didMove(toParentViewController: self)
        containerPager.addSubview(pageVC.view)
        pageVC.view.translatesAutoresizingMaskIntoConstraints = true
        pageVC.view.frame = containerPager.bounds
        pushViewControllerForCurrentIndex()
    }
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        self.containerAdView.isHidden = true

        DispatchQueue.main.asyncAfter(deadline: .now()+4) {
            self.simulateBannerLoad()
        }

    }

    @IBAction func buttonClicked(_ sender: Any) {
        guard clickEnabled else {return}
        currentIndex -= 1;
        pushViewControllerForCurrentIndex()

    }

    @IBAction func button2Clicked(_ sender: Any) {
        guard clickEnabled else {return}
        currentIndex += 1;
        pushViewControllerForCurrentIndex()
    }


    private func simulateBannerLoad(){
        constraintAdviewHeight.constant = 50
        pageViewController?.view.setNeedsLayout()
        UIView.animate(withDuration: 0.3,
                       delay: 0, options: .allowUserInteraction, animations: {
                        self.containerAdView.isHidden = false
                        self.view.layoutIfNeeded()
                        self.pageViewController?.view.layoutIfNeeded()
        })

    }

    //MARK: data source

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        return getViewControllerForIndex(currentIndex+1)
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        return getViewControllerForIndex(currentIndex-1)
    }

    func getViewControllerForIndex(_ index:Int) -> UIViewController? {
        guard (index>=0) else {return nil}
        let vc :UIViewController = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(withIdentifier: "pageTest")
        vc.view.backgroundColor = (index % 2 == 0) ? .red : .green
        return vc
    }

    func pushViewControllerForCurrentIndex() {
        guard let vc = getViewControllerForIndex(currentIndex) else {return}
        print("settingViewControllers start")
        clickEnabled = false
        pageViewController?.setViewControllers([vc], direction: .forward, animated: true, completion: { finished in
            print("setViewControllers finished")
            self.clickEnabled = true
        })
    }
}

Note: Another unwanted effect is that the last completion block when the bug occurs does not get called, so it leaves the buttons disabled:

    func pushViewControllerForCurrentIndex() {
        guard let vc = getViewControllerForIndex(currentIndex) else {return}
        print("settingViewControllers start")
        clickEnabled = false
        pageViewController?.setViewControllers([vc], direction: .forward, animated: true, completion: { finished in
            print("setViewControllers finished")
            self.clickEnabled = true
        })
    }

Note2: The banner load event is something I can't control manually. Due to the library used for displaying ads its a callback in the main thread that can happen in any moment. In the sample proyect this is simulated with a DispatchQueue.main.asyncAfter: call

How can I fix that? Thanks


Solution

  • What wrong?

    Layout will interrupt animation.

    How to prevent?

    Not change layout when pageViewController animating.

    When the ad is loaded: Confirm whether pageViewController is animating, if so, wait until the animation is completed and then update, or update

    Sample:

        private func simulateBannerLoad(){
            if clickEnabled {
                self.constraintAdviewHeight.constant = 50
            } else {
                needUpdateConstraint = true
            }
        }
        var needUpdateConstraint = false
        var clickEnabled = true {
            didSet {
                if clickEnabled && needUpdateConstraint {
                    self.constraintAdviewHeight.constant = 50
                    needUpdateConstraint = false
                }
            }
        }