2

I need to rely on a PageView view that has a currentPage value, in such a way that the PageView itself has ownership of the value (therefore, @State) but I need to update app state when this value changes.

With TabView, I simply put @Binding $selected in as an argument and can act upon changes to this value outside of the UI layer using a custom Binding<Int> with my own getter and setter. That is the method I'm trying right now to put together a solution.

But my PageView is based on an array of UIHostingControllers and a UIViewControllerRepresentable to integrate UIPageViewController from UIKit (I know that Swift in 5.3 will offer the SwiftUI version through TabView, but I don't want to wait until "September")

PageViewController.swift

// Source: https://stackoverflow.com/questions/58388071/how-to-implement-pageview-in-swiftui

struct PageViewController: UIViewControllerRepresentable {
    var controllers: [UIViewController]
    @Binding var currentPage: Int

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
                transitionStyle: .scroll,
                navigationOrientation: .horizontal)
        pageViewController.dataSource = context.coordinator
        pageViewController.delegate = context.coordinator

        return pageViewController
    }

    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
        if controllers.count > 0 {
            pageViewController.setViewControllers([controllers[currentPage]], direction: .forward, animated: true)
        }
    }

    class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
        var parent: PageViewController

        init(_ pageViewController: PageViewController) {
            self.parent = pageViewController
        }

        func pageViewController(
                _ pageViewController: UIPageViewController,
                viewControllerBefore viewController: UIViewController) -> UIViewController? {
            guard let index = parent.controllers.firstIndex(of: viewController) else {
                return nil
            }
            if index == 0 {
                return parent.controllers.last
            }
            return parent.controllers[index - 1]
        }

        func pageViewController(
                _ pageViewController: UIPageViewController,
                viewControllerAfter viewController: UIViewController) -> UIViewController? {
            guard let index = parent.controllers.firstIndex(of: viewController) else {
                return nil
            }
            if index + 1 == parent.controllers.count {
                return parent.controllers.first
            }
            return parent.controllers[index + 1]
        }

        func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
            if completed,
               let visibleViewController = pageViewController.viewControllers?.first,
               let index = parent.controllers.firstIndex(of: visibleViewController) {
                    parent.currentPage = index
            }
        }
    }
}

PageView.swift

struct PageView<Page: View>: View {
    var viewControllers: [UIHostingController<Page>]

    var currentPage: Binding<Int>

    init(_ views: [Page], currentPage: Binding<Int>) {
        self.viewControllers = views.map {
            let ui = UIHostingController(rootView: $0)
            ui.view.backgroundColor = UIColor.clear
            return ui
        }

        self.currentPage = currentPage
    }

    var body: some View {
            ZStack(alignment: .bottom) {
                PageViewController(controllers: viewControllers, currentPage: currentPage)
                PageControl(numberOfPages: viewControllers.count, currentPage: currentPage)
            }.frame(height: 300)
    }
}

Above you can see I'm trying passing in a Binding<Int> argument from the parent view, PageViewTest

PageViewTest.swift

struct PageViewTest: View {
    var pagesData = ["ONE", "TWO"]
    var _currentPage: Int = 0
    
    var currentPage: Binding<Int> {
        Binding<Int>(get: {
            self._currentPage
        }, set: {
            // i.e. Update app state
            print($0)
        })
    }

    var body: some View {
        VStack {
            PageView(pagesData.map {
                Text($0)
            }, currentPage: self.currentPage)
        }
    }
}

This set up works as far as calling the setter routine specified in PageViewTest, but for some reason the binding is not reflected in the PageControl (from PageView.swift) that conforms to UIViewRepresentable so I don't feel like this is THE solution.

Am I passing around the bindings incorrectly? The PageView should own the state of currentPage, but I want its ancestor view to be able to act on changes to it.

@ObservableObject won't work because I just want to send a primitive. CurrentValueSubject/Passthrough won't fire (presumably because the PageView is being reinitialized over and over):

Alternative PageView.swift

struct PageView<Page: View>: View {
    var viewControllers: [UIHostingController<Page>]

    var valStr = PassthroughSubject<Int, Never>()
    var store = Set<AnyCancellable>()

    @State var currentPage: Int = 0 {
        didSet {
            valStr.send(currentPage)
        }
    }

    init(_ views: [Page], _ cb: @escaping (Int) -> ()) {
        self.viewControllers = views.map {
            let ui = UIHostingController(rootView: $0)
            ui.view.backgroundColor = UIColor.clear
            return ui
        }

        valStr.sink(receiveValue: { value in
            cb(value)
        }).store(in: &store)
    }

    var body: some View {
            ZStack(alignment: .bottom) {
                PageViewController(controllers: viewControllers, currentPage: $currentPage)
                PageControl(numberOfPages: viewControllers.count, currentPage: $currentPage)
            }.frame(height: 300)
    }
}
elight
  • 562
  • 2
  • 17
  • This topic [SwiftUI scrollViewDidScroll in UIPageViewController not work properly](https://stackoverflow.com/questions/60621677/swiftui-scrollviewdidscroll-in-uipageviewcontroller-not-work-properly) should be helpful – Asperi Jul 08 '20 at 03:39
  • @Asperi thank you for the link. I have spent some time with this and still haven't solved it. I'm zooming out from this for now and will revisit it later. – elight Jul 08 '20 at 16:07

0 Answers0