Paging is achieved by using UIPageView from SwiftUI. I am having trouble with the same page being displayed twice in a row when swiping through pages. The condition that causes this phenomenon is the speed of page turning. This problem does not occur when the page is turned slowly or very quickly, but it does occur when the page is turned at an intermediate speed. Here is a video of this phenomenon.
The full code for reproduction is attached below. Your assistance would be greatly appreciated. Thank you in advance.
struct ContentView: View {
@State var currentPage: Int = 2
var body: some View {
VStack {
Text(currentPage.description)
.font(.largeTitle)
PageView(
pages: Array(0...100).map({ num in
Text("Page of \(num)")
.font(.largeTitle)
.padding(100)
.background(Color(.systemOrange))
.cornerRadius(10)
}),
currentPage: $currentPage)
}
}
}
struct PageView<Page: View>: UIViewControllerRepresentable {
var pages: [Page]
@Binding var currentPage: Int
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [UIPageViewController.OptionsKey.interPageSpacing: 30])
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
context.coordinator.controllers = pages.map({
let hostingController = UIHostingController(rootView: $0)
hostingController.view.backgroundColor = .clear
return hostingController
})
let currentViewController = context.coordinator.controllers[currentPage]
pageViewController.setViewControllers([currentViewController], direction: .forward, animated: false)
}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self, pages: pages)
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageView
var controllers: [UIViewController]
init(parent: PageView, pages: [Page]) {
self.parent = parent
self.controllers = pages.map({
let hostingController = UIHostingController(rootView: $0)
hostingController.view.backgroundColor = .clear
return hostingController
})
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let index = controllers.firstIndex(of: viewController) else {
return controllers[parent.currentPage - 1]
}
if index == 0 {
return nil
}
return controllers[index - 1]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let index = controllers.firstIndex(of: viewController) else {
return controllers[parent.currentPage + 1]
}
if index + 1 == controllers.count {
return nil
}
return controllers[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed,
let currentViewController = pageViewController.viewControllers?.first,
let currentIndex = controllers.firstIndex(of: currentViewController)
{
parent.currentPage = currentIndex
}
}
}
}
We also tried to realize paging from SwiftUI such as TabView and ScrollView. However, the screen I wanted to display was very heavy, and the method using UIKit's UIPageView was the only one that had almost no screen nibbling.
In
didFinishAnimating
, you only updatecurrentPage
when you can find the current view controller incontrollers
:Consider what happens when you set
parent.currentPage
-updateUIViewController
will be called, and you give a whole new array of VCs to the coordinator. If at this pointdidFinishAnimating
is called again,currentViewController
still contains the old instances. It will not findcurrentViewController
incontrollers
, and fail to setcurrentPage
.Notice that
setViewControllers
only changespageViewController.viewControllers
after the animation completes.So to fix this, just don't give new
UIHostingController
s every time the view updates:This works if you have a constant set of pages. Otherwise, this means that when
pages
change, you won't be able to update the view controllers.A better solution would be to use a data-oriented approach, like all other SwiftUI views do.
The idea is that
PageView
should take a list of data, instead of a list ofView
s. It would also take a function that converts the data toView
s.The coordinator also works with the data. Using
firstIndex(of:)
on the data array, and only creates VCs when necessary.Here is a rough sketch of how to do this. It works, but it can probably be optimised by using
Identifiable
and aDictionary
instead. The VCs made bymakeVC
could also be reused if the data hasn't changed.Example usage: