如果同时开始动画,则 UIPageViewController 转换会出现错误
UIPageViewController transition bugged if animation starts in the meanwhile
我在使用 UIPageViewController
的应用程序中有一个奇怪的行为。
我的应用程序的布局是一个 PageViewController(类似相机胶卷),底部有一个广告横幅。
横幅的容器开始时是隐藏的,当广告加载时,我设置了 isHidden=false
动画。
我的问题是,当横幅进入屏幕时,它会中断 UIPageViewController 转换(如果正在进行),如本视频所示:
我做了一个新项目,只需几行就可以很容易地重现错误,你可以在 GITHUB 中检查它:你只需要向 "Next" 按钮发送垃圾邮件,直到横幅被加载。它也可以通过滑动 PageViewController
来复制,但更难复制。
完整的示例代码是:
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
})
}
}
注意: 另一个不需要的效果是当错误发生时最后一个完成块没有被调用,所以它使按钮处于禁用状态:
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: 横幅加载事件是我无法手动控制的。由于用于显示广告的库,它在主线程中随时可能发生的回调。在示例项目中,这是通过 DispatchQueue.main.asyncAfter:
调用
模拟的
我该如何解决?谢谢
怎么了?
布局会中断动画。
如何预防?
pageViewController 动画时不改变布局。
加载广告时:
确认pageViewController是否正在动画,如果是,等动画完成后再更新,或者update
示例:
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
}
}
}
对我们来说,这是由于设备旋转导致布局传递与新视图控制器的动画同时发生。在这种情况下,我们不知道如何停止布局传递。
这对我们有用:
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:
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)
}
}
})
}
我在使用 UIPageViewController
的应用程序中有一个奇怪的行为。
我的应用程序的布局是一个 PageViewController(类似相机胶卷),底部有一个广告横幅。
横幅的容器开始时是隐藏的,当广告加载时,我设置了 isHidden=false
动画。
我的问题是,当横幅进入屏幕时,它会中断 UIPageViewController 转换(如果正在进行),如本视频所示:
我做了一个新项目,只需几行就可以很容易地重现错误,你可以在 GITHUB 中检查它:你只需要向 "Next" 按钮发送垃圾邮件,直到横幅被加载。它也可以通过滑动 PageViewController
来复制,但更难复制。
完整的示例代码是:
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
})
}
}
注意: 另一个不需要的效果是当错误发生时最后一个完成块没有被调用,所以它使按钮处于禁用状态:
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: 横幅加载事件是我无法手动控制的。由于用于显示广告的库,它在主线程中随时可能发生的回调。在示例项目中,这是通过 DispatchQueue.main.asyncAfter:
调用
我该如何解决?谢谢
怎么了?
布局会中断动画。
如何预防?
pageViewController 动画时不改变布局。
加载广告时: 确认pageViewController是否正在动画,如果是,等动画完成后再更新,或者update
示例:
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
}
}
}
对我们来说,这是由于设备旋转导致布局传递与新视图控制器的动画同时发生。在这种情况下,我们不知道如何停止布局传递。
这对我们有用:
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:
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)
}
}
})
}