This is my PageViewController
:
import SwiftUI
import UIKit
struct PageViewController<Page: View>: UIViewControllerRepresentable {
var pages: [Page]
var onLast: () -> Void
@Binding var currentPage: Int
var direction: UIPageViewController.NavigationOrientation
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: direction)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[context.coordinator.controllers[currentPage]], direction: .forward, animated: true)
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
var controllers = [UIViewController]()
init(_ pageViewController: PageViewController) {
parent = pageViewController
controllers = parent.pages.map { UIHostingController(rootView: $0.ignoresSafeArea()) }
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController?
{
guard let index = controllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return controllers.last
}
return controllers[index - 1]
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController) -> UIViewController?
{
guard let index = controllers.firstIndex(of: viewController) else {
return nil
}
if index + 1 == controllers.count {
return controllers.first
}
return controllers[index + 1]
}
func pageViewController(
_ pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = controllers.firstIndex(of: visibleViewController) {
parent.currentPage = index
}
if finished,
let visibleViewController = pageViewController.viewControllers?.first,
let index = controllers.firstIndex(of: visibleViewController) {
parent.currentPage = index
if parent.direction == .vertical && index == controllers.count - 1 {
if !parent.MarketVModel.discover_queue_space_summaries.isEmpty {
for newSpaceItem in parent.MarketVModel.discover_queue_space_summaries {
controllers.append(
UIHostingController(
rootView:
AnyView(
DiscoverSpacePresentational(
space: newSpaceItem
)
)
)
)
let spaceItemIndex: Int? = parent.MarketVModel.discover_queue_space_summaries.firstIndex(where: { $0.id == newSpaceItem.id })
if spaceItemIndex != nil {
parent.MarketVModel.discover_queue_space_summaries.remove(at: spaceItemIndex!)
}
}
}
}
}
}
}
}
struct PageView<Page: View>: View, Equatable {
static func == (lhs: PageView<Page>, rhs: PageView<Page>) -> Bool {
return true
}
var pages: [Page]
var onLast: () -> Void
@Binding var currentPage: Int
var direction: UIPageViewController.NavigationOrientation = .vertical
var body: some View {
PageViewController(pages: pages, onLast: onLast, currentPage: $currentPage, direction: direction)
}
}
Basically, I have a ViewModel that fetches new content and appends it to MarketVModel.discover_queue_space_summaries
, and then when I'm on the last index, I loop through the arrow and append it to controllers
, which works fine, however, whenever I am both 1. Swiping to the next view && 2. The controllers
is being appended to, the PageViewController does a glitchy-double-swipe, where it swipes to the "next" view automatically, but then recalibrates and displays the same view it glitched on.
I'm assuming the error is because the PageViewController is both trying to animate and figure out the correct view to display, and because during this process, the controllers
array is changing.
Is there a way to not affect the swiping/animation etc. of the pages when controllers
is being mutated?
UPDATE:
Upon further inspection, the double-swipe-glitch appears to happen when anything is happening, (being fetched, being processed, etc.) during the animation swipe (with my finger actively swiping). So I wonder if there is a way to fetch or process data, without the Controller being affected?
UPDATE 2:
I've researched quite a bit more, and have since updated my updateUIViewController
to:
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[context.coordinator.controllers[currentPage]],
direction: .forward,
animated: false,
completion: nil
)
}
Changing animated: true
to animated: false
.
After this change, the odd double swipe bug I mentioned above has gone, however, a new error has appeared if I swipe a little too quickly.
This is the error:
2022-07-05 03:54:30.696098+1000 Hero[16914:5514823] *** Assertion failure in -[UIPageViewController _flushViewController:animated:], UIPageViewController.m:2110
2022-07-05 03:54:30.703586+1000 Hero[16914:5514823] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Don't know about flushed view <_TtGC7SwiftUI14_UIHostingViewGVS_15ModifiedContentVS_7AnyViewVS_30_SafeAreaRegionsIgnoringLayout__: 0x10f1cb8c0; frame = (0 0; 390 844); autoresize = W+H; gestureRecognizers = <NSArray: 0x281495800>; layer = <CALayer: 0x281b49ce0>>'
*** First throw call stack:
(0x1807a50fc 0x198ff5d64 0x18206139c 0x1834e2690 0x1834e2748 0x1834ec564 0x1834ec6ec 0x1834eec48 0x1834df3c8 0x1834df5f4 0x1034ebb14 0x1034f0e08 0x1889d5e54 0x1882ecf5c 0x18839bab8 0x188279ba0 0x188255af4 0x188217f30 0x188227b28 0x18824e34c 0x1b27bc910 0x1b27bc318 0x1b27bb160 0x1881c643c 0x188e42128 0x1881b829c 0x1881c3900 0x1881bc940 0x1881b5d4c 0x1881b4e3c 0x1882bfcd8 0x1881b33d0 0x1881b3444 0x1882a1098 0x1aeaf9a20 0x1881b32f0 0x1881b35ac 0x18074d610 0x18071c8f4 0x18071798c 0x18072b468 0x19c2cf38c 0x1830ce5d0 0x182e4cf74 0x1883ea314 0x188319508 0x1882facb8 0x102def4c4 0x102def7a4 0x10caddaa4)
libc++abi: terminating with uncaught exception of type NSException
After reading the answers here:
https://stackoverflow.com/a/48574512/2458437
and
https://stackoverflow.com/a/17330606/2458437
I'm assuming the problem is happening because pageViewController.setViewControllers
is being called whilst a transition is already in progress.
I've tried multiple solutions. The first one was to put pageViewController.setViewControllers(...)
inside a DispatchQueue.main.async { }
However, I found that it would crash on appear some times.
The other solution I've used is to re-set the view controllers again in the completion handler. I feel like I'm misunderstanding the solution, since I've tried porting it from an older version of Swift, which was ported from Obj-C:
pageViewController.setViewControllers(
[context.coordinator.controllers[currentPage]],
direction: .forward,
animated: false,
completion: { (finished) in
if finished {
pageViewController.setViewControllers(
[context.coordinator.controllers[currentPage]],
direction: .forward,
animated: false,
completion: nil
)
}
}
)
The code I'm referencing is from this answer and is as follows:
__weak YourSelfClass *blocksafeSelf = self;
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:^(BOOL finished){
if(finished)
{
dispatch_async(dispatch_get_main_queue(), ^{
[blocksafeSelf.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:NULL];// bug fix for uipageview controller
});
}
}];
I feel like I'm super close to getting a working solution, but thus far I've not been successful. I really hope someone can help me out, I hope I've provided enough information!
Thank you in advance!