iosswiftuiparchment

Navigation bar disappears after the app comes to the foreground again


I'm using Parchment to add menu items at the top. The hierarchy of the main view is the following:

NavigationView -> TabView --> Parchment PagingView ---> NavigationLink(ChildView)

All works well going to the child view and then back again repeatedly. The issue happens when I go to ChildView, then go to the background/Home Screen then re-open. If I click back and then go to the child again the back button and the whole navigation bar disappears.

enter image description here

Here's code to replicate:

import SwiftUI
import Parchment

@main
struct ParchmentBugApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView {
            TabView {
                PagingView(items: [
                    PagingIndexItem(index: 0, title: "View 0"),
                ]) { item in
                    VStack {
                        NavigationLink(destination: ChildView()) {
                            Text("Go to child view")
                        }
                    }
                    .navigationBarHidden(true)
                }
            }
        }
    }
}

struct ChildView: View {
    var body: some View {
        VStack {
            Text("Child View")
        }
        .navigationBarHidden(false)
        .navigationBarTitle("Child View")
    }
}

To replicate:

  1. Launch and go to the child view
  2. Click the home button to send the app to the background
  3. Open the app again
  4. Click on back
  5. Navigate to the child view. The nav bar/back button are not there anymore.

What I noticed:

  1. Removing the TabView makes the problem go away.
  2. Removing PagingView also makes the problem go.

I tried to use a custom PagingController and played with various settings without success. Here's the custom PagingView if someone would like to tinker with the settings as well:

struct CustomPagingView<Item: PagingItem, Page: View>: View {
    private let items: [Item]
    private let options: PagingOptions
    private let content: (Item) -> Page
    
    /// Initialize a new `PageView`.
    ///
    /// - Parameters:
    ///   - options: The configuration parameters we want to customize.
    ///   - items: The array of `PagingItem`s to display in the menu.
    ///   - content: A callback that returns the `View` for each item.
    public init(options: PagingOptions = PagingOptions(),
                items: [Item],
                content: @escaping (Item) -> Page) {
        self.options = options
        self.items = items
        self.content = content
    }
    
    public var body: some View {
        PagingController(items: items, options: options,
                         content: content)
    }
    
    struct PagingController: UIViewControllerRepresentable {
        let items: [Item]
        let options: PagingOptions
        let content: (Item) -> Page
        
        func makeCoordinator() -> Coordinator {
            Coordinator(self)
        }
        
        func makeUIViewController(context: UIViewControllerRepresentableContext<PagingController>) -> PagingViewController {
            let pagingViewController = PagingViewController(options: options)
            return pagingViewController
        }
        
        func updateUIViewController(_ pagingViewController: PagingViewController, context: UIViewControllerRepresentableContext<PagingController>) {
            context.coordinator.parent = self
            if pagingViewController.dataSource == nil {
                pagingViewController.dataSource = context.coordinator
            } else {
                pagingViewController.reloadData()
            }
        }
    }
    
    class Coordinator: PagingViewControllerDataSource {
        var parent: PagingController
        
        init(_ pagingController: PagingController) {
            self.parent = pagingController
        }
        
        func numberOfViewControllers(in pagingViewController: PagingViewController) -> Int {
            return parent.items.count
        }
        
        func pagingViewController(_: PagingViewController, viewControllerAt index: Int) -> UIViewController {
            let view = parent.content(parent.items[index])
            return UIHostingController(rootView: view)
        }
        
        func pagingViewController(_: PagingViewController, pagingItemAt index: Int) -> PagingItem {
            return parent.items[index]
        }
    }
}

Tested on iOS Simulator 14.4 & 14.5, and device 14.5 beta 2.

Any tips or ideas are very much appreciated.


Solution

  • Okay I found the issue while debugging something else that was related to Parchment as well.

    The issue is updateUIViewController() gets called each time the encompassing SwiftUI state changes (and when coming back to the foreground), and the PageController wrapper provided by the library will call reloadData() since the data source data has already been set. So to resolve this just remove/comment out the reloadData() call since the PageController will be re-built if the relevant state changes. The same issue was the cause for the bug I was debugging.

        func updateUIViewController(_ pagingViewController: PagingViewController, context: UIViewControllerRepresentableContext<PagingController>) {
            context.coordinator.parent = self
            if pagingViewController.dataSource == nil {
                pagingViewController.dataSource = context.coordinator
            }
            //else {
            //    pagingViewController.reloadData()
            //}
        }