'UIPageViewController swipe ignores SwiftUI's navigationBarHidden(true)

I'm creating a SwiftUI app that needs a slider with hundreds of pages. Since there's no first-party solution that fits my needs, I've adapted UIPageViewController. Main use-case: the slider should allow tapping on its content which will (un)hide the navigation bar. The hiding works fine alone, but when I swipe while the navigation bar is hidden, it reappears.

How can I keep the navigation bar hidden based on navigationBarHidden(hidden)?

Step by step:

  1. Tap to hide (the navigation bar is now hidden)
  2. Swipe (still hidden)
  3. Swipe (it appears)

Demo video

In most cases, it takes two swipes to unhide the bar, but sometimes it unhides after only one swipe.

Here's the SwiftUI part of the app:

struct ApplicationView: View {

    var body: some View {
        NavigationView {
            ContentView()
                .navigationBarTitle(Text("Test"), displayMode: .inline)
        }
    }

}

struct ContentView: View {

    @State private var hidden = false
    @State private var currentPage = 0

    var body: some View {
        PagerView(0..<100, currentPage: $currentPage) {
            Text("\($0)")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .onTapGesture { hidden.toggle() }
        }
        .navigationBarHidden(hidden)
        .ignoresSafeArea()
        .background(Color.red.ignoresSafeArea())
    }

}

and the adapted UIPageViewController:

struct PagerView<Data: RandomAccessCollection, Page: View>: UIViewControllerRepresentable {

    private let data: Data
    @Binding var currentPage: Data.Index
    private let interPageSpacing: CGFloat
    private let content: (Data.Element) -> Page

    init(
        _ data: Data,
        currentPage: Binding<Data.Index>,
        interPageSpacing: CGFloat = 0,
        @ViewBuilder content: @escaping (Data.Element) -> Page
    ) {
        self.data = data
        self._currentPage = currentPage
        self.interPageSpacing = interPageSpacing
        self.content = content
    }

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

    func updateUIViewController(_ uiViewController: UIPageViewController, context: Context) {
        let direction: UIPageViewController.NavigationDirection

        if let previousViewController = uiViewController.viewControllers?.first as? PageViewController<Page> {
            guard previousViewController.index != currentPage else { return }
            direction = previousViewController.index < currentPage ? .forward : .reverse
        } else {
            direction = .forward
        }

        let page = context.coordinator.page(for: currentPage)
        uiViewController.setViewControllers([page], direction: direction, animated: true)
    }

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

    class Coordinator: NSObject, UIPageViewControllerDelegate, UIPageViewControllerDataSource {

        private var parent: PagerView

        init(_ parent: PagerView) {
            self.parent = parent
        }

        func page(for index: Data.Index) -> PageViewController<Page> {
            return PageViewController(rootView: parent.content(parent.data[index]), index: index)
        }

        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerBefore viewController: UIViewController
        ) -> UIViewController? {
            guard parent.currentPage > parent.data.startIndex else { return nil }
            return page(for: parent.data.index(parent.currentPage, offsetBy: -1))
        }

        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerAfter viewController: UIViewController
        ) -> UIViewController? {
            guard parent.currentPage < parent.data.index(parent.data.endIndex, offsetBy: -1) else { return nil }
            return page(for: parent.data.index(parent.currentPage, offsetBy: 1))
        }

        func pageViewController(
            _ pageViewController: UIPageViewController,
            didFinishAnimating finished: Bool,
            previousViewControllers: [UIViewController],
            transitionCompleted completed: Bool
        ) {
            if completed, let viewController = pageViewController.viewControllers?.first as? PageViewController<Page> {
                parent.currentPage = viewController.index
            }
        }

    }

    class PageViewController<Content: View>: UIHostingController<Content> {

        var index: Data.Index

        init(rootView: Content, index: Data.Index) {
            self.index = index
            super.init(rootView: rootView)
        }

        @MainActor @objc required dynamic init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }

        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = .clear
        }

    }

}


Solution 1:[1]

i was a quick solution,but i'm not very sure :)

update solution

  //your code 
  var body: some View {
        PagerView(0..<100, currentPage: $currentPage) {
            Text("\($0)")
                .frame(maxWidth: .infinity, maxHeight: .infinity)

                // page contain evey text in UIHostingController      
                .navigationBarHidden(true)
                .onTapGesture { hidden.toggle() }
        }
        .navigationBarHidden(hidden)
        .ignoresSafeArea()
        .background(Color.red.ignoresSafeArea())
    }

be like: a -> b

a:

NavigationView{
    let b = controller
    b.navigationBarHidden(true)
    link:b
}

b:
i use a navigationView{} to contain my UI code in b

//wrong!! can not a <- b
NavigationView {
    ZStack{
       custom navigationBar
       pageController
       ...
    }

}
//can a <- b
ZStack{
       custom navigationBar
       //just page
       NavigationView {
          pageController
       }
      
       ...
}

then,it solve a -> b temporary just a trick,i haven't spend time digining

another solution, it works for me?2/16/2022?

        let a = AnyView(TestRedView()).navigationBarHidden(true)
        let b = AnyView(TestBlueView()).navigationBarHidden(true)
        
        return PaginationView(pages: [a, b])
            .navigationBarTitle(Text("Test"), displayMode: .inline)
            .navigationBarHidden(true)
            .edgesIgnoringSafeArea(.all)

Solution 2:[2]

I see that you're embedding a UIHostingController. I've found that having that as a child view will cause the navigation bar to reappear, because under some circumstances it sets hidden to false (not sure when).

Try the approach from this answer, removing access to UINavigationController from your UIHostingController:

class PageViewController<Content: View>: UIHostingController<Content> {

    public override var navigationController: UINavigationController? {
        nil
    }

    ...
}

(This is really something Apple should fix. At the moment UIHostingController doesn't really play nicely when embedded within a SwiftUI view hierarchy.)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 robinst