Search code examples
iosswiftswiftuiuipageviewcontrolleruiviewcontrollerrepresentable

SwiftUI send action from a page to the PageViewController


I have set up a PageViewController in SwiftUI, following the known Tutorial Interfacing with UIKit, with UIViewControllerRepresentable etc.

My array of controllers consists of simple SwiftUI views. I pass a simple IntroPage struct to supply the content. The views are nested, as is good SwiftUI practice, thus:

PageView
-   IntroScreen                // part of the pages array
    -   VStack 
        -   Text
        -   Image
        -   ButtonView         // a separate view struct
            - HStack
                - ForEach      // iterating through the buttons in my IntroPage Object
                    - Button
PageControl

Now I want to include some buttons on these views. They should be configurable via my IntroPage struct. One of them advances to the next page in the PageViewController, another tells the PageViewController to add more pages first, another button dismisses the entire PageViewController.

I cannot figure out, how get access to these methods in the PageViewController, where to implement them (the instance view? the coordinator of the PageViewController?) and how to reach the objects I need (e.g. the currentPage Binding variable of the PageViewController).

For example, I have implemented a forward() function in the PageViewController's coordinator:

func forward() {
   if parent.currentPage < parent.controllers.count - 1 {
       parent.currentPage += 1
   }
}

...which works fine, with animations and all, if I add a button right beside the PageView on my final View. But I still cannot call this from the button contained in the child view.

Any ideas?

EDIT: Upon request, here is a situation in the ButtonView.

struct IntroButtonView: View {
    var page: IntroPage
    var body: some View {
        HStack() {
           Button(action:dismiss) {
              Text("LocalizedButtonTitle")
           }
           // ... more buttons, based on certain conditions
        }
    }

    func dismiss() { 
         // how to dismiss the modally presented controller ?
    }

    func next()    { 
         // how to advance to the next page
    }

    func expand()  { 
         // how to add more pages to the pages array
    }
}

Or maybe I am completely wrong and still think in terms of "events" rather than "declaration"...


Solution

  • OK, I figured it out. Not intuitive at first, I have to say. Coming from traditional event based programming, it's quite a different way of thinking

    I used a @State variable in the main instance of the view.

    I used @Binding variables to deal with the state both upstream (ViewControllers, Controls) and downstream (subviews). So, for example, I used a variable to tell the dataSource of the UIPageViewController if or not to return a view controller before/after the current one.

    For the dismissing the modally presented controller I used

    @Environment(\.presentationMode) var presentationMode
    
    ...
    
    func dismiss() {
      self.presentationMode.wrapptedValue.dismiss()
    }
    

    Similarly,

    ...
    @Binding var currentPage: int
    ...
    
    Button(action: next) { Text("Next Page") }
    ...
    
    ...
    func next() {
        currentPage += 1
    }
    

    There were a few caveats in deciding how to nest the views and what variables to pick for the bindings, but it is clear to me now. The biggest problem was ultimately where the "source of truth" should be anchored. It turned out, right in the "middle", i.e. below the controller and above the particular views.

    Hope this is useful for others looking for something similar.