I am also experiencing this problem and have submitted feedback about it (FB14999139).
My debugging results are very similar to yours: the gesture recognizer calls viewControllerBefore and then immediately calls viewControllerAfter, which effectively tries to navigate backwards with the nil result from the viewControllerAfter method, causing the crash.
The best workaround I have found is to explicitly account for these rapid viewControllerBefore viewControllerAfter calls. I did this by comparing the times that they are called, and if they are called too close together, presenting a special view controller.
Presenting a unique view controller that is outside of the pages that I would like flipped has the benefit, at least in terms of how this sample code works, of being caught by the guard statements in both viewControllerBefore and viewControllerAfter methods, effectively disabling gesture navigation. Gesture navigation is only re-enabled by manually pressing the button that displays a view controller from the pre-defined range. To me, this maintains a spatial location better than flipping to the same last page over and over again.
This is not perfect. The user can trigger it by quickly tapping forward and back, but for now I feel like the unique view controller still handles that gracefully. Better than crashing the app!
Here is the example from my test app.
import UIKit
import SwiftUI
class MyPageViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
let viewControllersList: [UIViewController] = {
let colors: [UIColor] = [.orange, .green, .blue]
let viewControllers: [UIViewController] = colors.map { color in
let vc = UIViewController()
vc.view.backgroundColor = color
return vc
}
return viewControllers
}()
/// Time that viewControllerBefore is called.
var beforeTime: TimeInterval = Date.timeIntervalSinceReferenceDate
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self
delegate = self
// Set initial view controller
if let firstVC = viewControllersList.first {
setViewControllers([firstVC], direction: .forward, animated: true, completion: nil)
}
}
// MARK: - UIPageViewControllerDataSource
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let currentIndex = viewControllersList.firstIndex(of: viewController) else { return nil }
// update the time that this method is called
beforeTime = Date.timeIntervalSinceReferenceDate
let previousIndex = currentIndex - 1
return previousIndex >= 0 ? viewControllersList[previousIndex] : nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let currentIndex = viewControllersList.firstIndex(of: viewController) else { return nil }
// compare the time interval since the before method was called
let currentTime = Date.timeIntervalSinceReferenceDate
let delta = currentTime - beforeTime
// guard against the methods being called too quickly.
// I found 0.01 would still allow the crash to happen so went with something longer.
// Note that if the user manually tries to navigate back and forth, this can be triggered.
guard delta > 0.08 else {
print("rapid calls detected")
// a SwiftUI view with a button that calls the `goHomeAction`
let goHomeVC = UIHostingController(rootView: GoHomeView(goHomeAction: { [weak self] in
guard let self = self else { return }
self.setViewControllers([viewControllersList[0]], direction: .reverse, animated: true)
}))
return goHomeVC }
let nextIndex = currentIndex + 1
return nextIndex < viewControllersList.count ? viewControllersList[nextIndex] : nil
}
}
//MARK: - SwiftUI View
struct GoHomeView: View {
var goHomeAction: () -> Void
var body: some View {
Button("Go Home") {
goHomeAction()
}
}
}
FWIW, I have also submitted a support request ticket. I hope there is a better solution than this. I will update if I learn anything more.