snarik
snarik

Reputation: 1225

How to create custom updateUIViewController behavior when integrating UIKit and Swift UI?

I have a View controller as describe below.

//
//  PageViewController.swift


import SwiftUI
import UIKit

struct PageViewController: UIViewControllerRepresentable {
    var controllers: [UIViewController]
    @Binding var currentPage: Int
    init(controllers: [UIViewController], currentPage: Binding<Int>){
        self.controllers = controllers
        self._currentPage = currentPage
    }
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

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

    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
            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 nil
            }

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

In my swiftUI file I have something similar to:

struct myCoolView : View { 
    @State var currentPage: Int = 0 
    var body: some View { 
        let subViews = [UIHostingController(rootView: mySubView(currentPage: $currentPage), ...]
        return VStack{ 
            PageViewController(controllers: subViews, currentPage: $currentPage)
        }

In my Subviews I have buttons to go backward and forwards in the stack:

struct mySubView : View {
    @Binding var currentPage: Int

    var body: some View {
        HStack {
            Button(action: {self.currentPage -= 1}){
                Text("Go Back")
            }
            Spacer()
            Button(action: {self.currentPage += 1 }){
                Text("Go Forward")
            }
        }
    }
}

The go forward button behaves correctly, the coordinator calls updateUIViewController which activates the .forward animation style. The issue is the back button ALSO displays the forward animation style when it should be calling the forward.

Ive toyed around with a few ways to solve this but none of them really work. What I really want is to create an extension that explicitly triggers either the go forward or go backward update flows. Any ideas how to accomplish that?

Thanks in advance

Upvotes: 2

Views: 1809

Answers (2)

sebhell
sebhell

Reputation: 36

I had the exact same problem with the exact same example.

I solved it by comparing the index of the visible viewController to the "currentIndex" and adjusting the direction accordingly.

func updateUIViewController(_ pageViewController: UIPageViewController, context: UIViewControllerRepresentableContext<PageViewController>) {
    
    var direction: UIPageViewController.NavigationDirection = .forward

    if let visibleViewController = pageViewController.viewControllers?.first,
        let index = controllers.firstIndex(of: visibleViewController),
        index > currentPage {
        direction = .reverse
    }

    pageViewController.setViewControllers([controllers[currentPage]], direction: direction, animated: true)
}

Upvotes: 2

it seems to me you have one

 @State var currentPage: Int = 0

that you use for a number of UIHostingController/mySubView and also in PageViewController. Try using different (an array of) var for each use.

Upvotes: 0

Related Questions