Reputation: 32243
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:
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
Upvotes: 4
Views: 1828
Reputation: 724
For us, it was due to a device rotation causing a layout pass at the same time as an animation to a new view controller. We could not figure out how to stop the layout pass in this situation.
This worked for us:
let pageController = UIPageViewController()
func updatePageController() {
pageController.setViewControllers(newViewControllers, direction: .forward, animated: true, completion: {[weak self] _ in
// There is a bug with UIPageViewController where a layout pass during an animation
// to a new view controller can cause the UIPageViewController to display the old and new view controller
// In our case, we can compare the view controller `children` count against the the `viewControllers` count
// In other cases, we may need to examine `children` more closely against `viewControllers` to see if there discrepancies.
// Since this is on the animation callback we need to dispatch to the main thread to work around another bug: https://stackoverflow.com/a/24749239/2191796
DispatchQueue.main.async { [weak self] in
guard let self = self else {return}
if self.pageController.children.count != self.pageController.viewControllers?.count {
self.pageController.setViewControllers(self.pageController.viewControllers, direction: .forward, animated: false, completion: nil)
}
}
})
}
Upvotes: 0
Reputation: 1473
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
}
}
}
Upvotes: 3