The effect I'm trying to achieve: link to video (note that in this video the issue is present).
Basically, a simultaneous page curl and translation on the x axis (since the video was registered in landscape, the translation appears on the y axis).
The page curl animation is performed under the hood whenever setViewControllers
method of UIPageViewController
is called, while the translation is achieved through UIView.animate
by updating constraints.
Until iOS 15, this worked perfectly fine, however since iOS 16 the animation is laggy and does not work as intended, see below.
The issue is clearly related to the x translation occurring while the curl animation is being performed, as without it everything works fine. Probably this is not the right way to perform multiple animations I assume, however since the page curl happens under the hood I'm not really sure on how to handle it.
My question is: how can I achieve a page curl and x translation animations simultaneously? And on a side note, does anyone knows why the strange behaviour I encountered happens only on newer versions of iOS?
EDIT: to further clarify what I mean, this is a slow-mo video of curl+x translation where the issue is visible. The part of the vc highlighted with red lines should not be transparent, but red. This is a bug.
This is the curl animation with no x translation, in which there is no transparency issue / no bugs.
Sample project which reproduces the issue - note, it is intended to work only in landscape:
import UIKit
final class ViewController: UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
private var pageController: UIPageViewController?
private var pageControllerViewLeadingAnchor: NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
// init
pageController = UIPageViewController(transitionStyle: .pageCurl, navigationOrientation: .horizontal, options: nil)
pageController?.dataSource = self
pageController?.delegate = self
// adding it
addChild(pageController!)
view.addSubview(pageController!.view)
// constraints
pageController?.view.translatesAutoresizingMaskIntoConstraints = false
pageController?.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
pageController?.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
pageControllerViewLeadingAnchor = pageController?.view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: -view.bounds.width/4)
pageControllerViewLeadingAnchor?.isActive = true
pageController?.view.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
DispatchQueue.main.asyncAfter(deadline: .now()+0.5) {
// x translation animation
UIView.animate(withDuration: 0.3, animations: {
self.pageControllerViewLeadingAnchor?.constant = 0
self.view.layoutIfNeeded()
})
// pageController curl animation
let vc = UIViewController()
let vc2 = UIViewController()
vc.view.backgroundColor = .red
vc2.view.backgroundColor = .blue
self.pageController?.setViewControllers([vc, vc2], direction: .forward, animated: true, completion: nil)
}
}
func pageViewController(_ pageViewController: UIPageViewController, spineLocationFor orientation: UIInterfaceOrientation) -> UIPageViewController.SpineLocation {
// setting spine to mid and adding vcs
let vc = UIViewController()
let vc2 = UIViewController()
vc2.view.backgroundColor = .green
pageController?.setViewControllers([vc, vc2], direction: .forward, animated: true, completion: nil)
pageController?.isDoubleSided = true
return .mid
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
return UIViewController()
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
return UIViewController()
}
}
You may want to try embedding the page controller in a "container" UIView
... then animate the container into place.
Give this a try:
final class ViewController: UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
private var pageController: UIPageViewController?
private var containerViewLeadingAnchor: NSLayoutConstraint?
private let pageControllerContainerView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
// init
pageController = UIPageViewController(transitionStyle: .pageCurl, navigationOrientation: .horizontal, options: nil)
pageController?.dataSource = self
pageController?.delegate = self
// adding it
addChild(pageController!)
// add the container view
pageControllerContainerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pageControllerContainerView)
// add the page controller view as a subview of theh container view
pageControllerContainerView.addSubview(pageController!.view)
// constrain page controller view to all 4 sides of the container view
pageController?.view.translatesAutoresizingMaskIntoConstraints = false
pageController?.view.topAnchor.constraint(equalTo: pageControllerContainerView.topAnchor).isActive = true
pageController?.view.bottomAnchor.constraint(equalTo: pageControllerContainerView.bottomAnchor).isActive = true
pageController?.view.leadingAnchor.constraint(equalTo: pageControllerContainerView.leadingAnchor).isActive = true
pageController?.view.trailingAnchor.constraint(equalTo: pageControllerContainerView.trailingAnchor).isActive = true
guard let g = view else { return }
NSLayoutConstraint.activate([
// constrain container view Top / Bottom / Width
pageControllerContainerView.topAnchor.constraint(equalTo: g.topAnchor),
pageControllerContainerView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
pageControllerContainerView.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 1.0),
])
// container view leading constant will be set in viewDidLayoutSubviews()
containerViewLeadingAnchor = pageControllerContainerView.leadingAnchor.constraint(equalTo: g.leadingAnchor)
containerViewLeadingAnchor?.isActive = true
pageControllerContainerView.clipsToBounds = true
}
// track the container width
var cvw: CGFloat = -1
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// only execute if the container view frame width has changed
// (such as on launch)
if cvw != pageControllerContainerView.frame.width {
cvw = pageControllerContainerView.frame.width
containerViewLeadingAnchor?.constant = -cvw * 0.25
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
DispatchQueue.main.asyncAfter(deadline: .now()+1.5) {
// pageController curl animation
let vc = UIViewController()
let vc2 = UIViewController()
vc.view.backgroundColor = .red
vc2.view.backgroundColor = .blue
self.pageController?.setViewControllers([vc, vc2], direction: .forward, animated: true, completion: nil)
// x translation animation
UIView.animate(withDuration: 0.3, animations: {
self.containerViewLeadingAnchor?.constant = 0
self.view.layoutIfNeeded()
})
}
}
func pageViewController(_ pageViewController: UIPageViewController, spineLocationFor orientation: UIInterfaceOrientation) -> UIPageViewController.SpineLocation {
// setting spine to mid and adding vcs
let vc = UIViewController()
let vc2 = UIViewController()
vc2.view.backgroundColor = .green
pageController?.setViewControllers([vc, vc2], direction: .forward, animated: true, completion: nil)
pageController?.isDoubleSided = true
return .mid
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
return UIViewController()
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
return UIViewController()
}
}
Edit
Adding pageControllerContainerView.clipsToBounds = true
appears to resolve the issue.