I’m trying to implement a book like interface in my swift app where the spine of the book is at the centre and the user is able to view two pages. For this, I’m using the UIPageViewController class and creating an array of ViewControllers to be displayed on the screen.
I’m able to initialise the first two pages properly. However when I flip the page to view page 3 and 4, it takes me directly to the last page. I’ve used a ViewContorllerRespresentable to embed this into my Swift UI to see it on the screen.
Here is the code I’m using.
import SwiftUI
import UIKit
import Combine
class CustomPageViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let index = viewControllersList.firstIndex(of: viewController), index >= 2 else {
return nil
}
print("currrent index is", index)
let previousIndex = index - 2
print("MOVING BEHIND")
print(previousIndex+1)
print(previousIndex)
setViewControllers([viewControllersList[previousIndex], viewControllersList[previousIndex + 1]], direction: .reverse, animated: true){finished in
if finished{
print("completed move behind operation")
}
}
return nil // The system manages the transition; no need to return a single view controller.
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let index = viewControllersList.firstIndex(of: viewController), index < viewControllersList.count - 2 else {
return nil
}
print("currrent index is", index)
let nextIndex = index + 2
print("MOVING AHEAD")
print(nextIndex-1)
print(nextIndex)
setViewControllers([viewControllersList[nextIndex-1], viewControllersList[nextIndex]], direction: .forward, animated: true){finished in
if finished{
print("completed move ahead operation")
}
}
return nil // The system manages the transition; no need to return a single view controller.
}
lazy var viewControllersList: [UIViewController] = []
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
self.delegate = self
let firstViewController = UIViewController()
firstViewController.view.backgroundColor = .red
let secondViewController = UIViewController()
secondViewController.view.backgroundColor = .green
let thirdViewController = UIViewController()
thirdViewController.view.backgroundColor = .blue
let fourthViewController = UIViewController()
thirdViewController.view.backgroundColor = .yellow
let fifthViewController = UIViewController()
thirdViewController.view.backgroundColor = .black
let sixthViewController = UIViewController()
thirdViewController.view.backgroundColor = .magenta
viewControllersList = [firstViewController, secondViewController, thirdViewController, fourthViewController, fifthViewController, sixthViewController]
setViewControllers([viewControllersList[0], viewControllersList[1]], direction: .forward, animated: true){finished in
if finished{
print("set initial VC")
}
}
}
func pageViewController(_ pageViewController: UIPageViewController, spineLocationFor interfaceOrientation: UIInterfaceOrientation) -> UIPageViewController.SpineLocation {
if interfaceOrientation.isLandscape{
// self.isDoubleSided = true
print("INSIDE THIS BLOCK OF CODE")
if let currentViewController = self.viewControllers?.first, let currentIndex = viewControllersList.firstIndex(of: currentViewController) {
let nextIndex = currentIndex + 1
let nextViewController = (nextIndex < viewControllersList.count) ? viewControllersList[nextIndex] : nil
if let nextVC = nextViewController {
setViewControllers([currentViewController, nextVC], direction: .forward, animated: true, completion: nil)
} else {
setViewControllers([currentViewController], direction: .forward, animated: true, completion: nil)
}
}
return .mid
}
else{
return .min
}
}
}
// 2. PageViewControllerRepresentable
struct MyViewControllerRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> CustomPageViewController {
let pageViewController = CustomPageViewController(transitionStyle: .pageCurl, navigationOrientation: .horizontal)
return pageViewController
}
func updateUIViewController(_ uiViewController: CustomPageViewController, context: Context) {
// Update the UIPageViewController if needed
}
}
struct TestView : View {
var body: some View{
VStack{
MyViewControllerRepresentable()
}
}
}
I feel that I’m really close to making it happen but I’m missing a small detail. Appreciate your help on this
2
Answers
For the function setViewControllers([viewControllersList[index]], …), only use 1 viewcontroller in the array. That could help already.
For the datasource of the pageController, i would do something like this (vcs is the array of viewcontrollers):
There are several issues here…
First:
When returning
.mid
for spine location,UIPageViewController
automatically sets itself to.isDoubleSided = true
. However, when returning.min
it does not automatically set it to false — which makes sense, because it doesn’t know what you want.So, we need to explicitly set it back to
false
to avoid the two-pages-at-a-time problem.Second:
If we are in two-page mode (spine = .mid), and we have an odd number of pages, we’ll crash when navigating to the last page — because we have only one controller to return.
To avoid that, we can add some code that says:
Third:
If we’re in single-page mode… we’re at an odd-numbered page… we switch to two-page mode (rotate the device)…
If we set the first controller (the left-hand side) to that odd-numbered page, we can never get back to the first page/controller.
e.g.:
If we try to swipe/drag to the previous page, we would need to display:
which, of course, we can’t.
To avoid that, any time we switch from single-page to two-page…
The final issue:
There is a quirk (I’d call it a bug, but what do I know) when switching from
.mid
to.min
. The right-hand controller/view gets its.alpha
set to0.0
. So, when we try to navigate to it, we don’t see it.To fix that, we can set
.alpha = 1.0
on the return view controller every time in bothviewControllerBefore
andviewControllerAfter
.So… here is a complete example to try out:
Worth noting: these issues are not related to using
UIPageViewController
as aUIViewControllerRepresentable
in SwiftUI … the same issues show up when using it as a standard controller in a Swift / UIKit implementation.