'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:
- Tap to hide (the navigation bar is now hidden)
- Swipe (still hidden)
- Swipe (it appears)
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 |