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)
}
}