NavigationSplitView resetting when app enters background

171 Views Asked by At

I've been trying to build an app using NavigationSplitView and SwiftUI on iOS 16. I noticed that as soon almost as the app enters the background (sometimes I have to wait a few seconds before immediately reopening the app) the contents of the NavigationSplitView are reset. This behavior differs from a NavigationStack using essentially the same path and app structure. I'm wondering if anyone has any idea why this happens?

I did a little investigation and can say I've noticed a few things

If using a List with a selection item specified, the selection item is set to nil when the app enters the background. For NavigationSplitView this typically will reset the downstream Detail or Content view since the value is lost

If using a List without the selection parameter specified this effect is mitigated but the view still is reset and things like scroll position are wiped despite no content changing. Self._printChanges() indicates the view is unchanged despite being reset to its initial state.

Using Apple's own WWDC sample app this effect is reproduced

https://developer.apple.com/documentation/swiftui/bringing_robust_navigation_structure_to_your_swiftui_app

This sample app does have some logic that tries to persist the navigation path data but actually doesn't work anyway, but for testing purposes I commented this out

Inside ContentView.swift

//        .task {
//            if let jsonData = navigationData {
//                navigationModel.jsonData = jsonData
//            }
//            for await _ in navigationModel.objectWillChangeSequence {
//                navigationData = navigationModel.jsonData
//            }
//        }

This app is a good sample because you can toggle between NavigationStack and NavigationSplitView and the behavior is isolated to the NavigationSplitView, the NavigationStack does not immediately reset.

Obviously state preservation is an option but adds quite a bit of overhead and things like scroll position are hard to reproduce exactly, and having to fall back on this when the app loses focus for just a few seconds isn't ideal, I'm wondering if there's a workaround or this is a bug especially since this was not present when using NavigationStack. I did test this on the latest iOS 17 beta with the same effect

Including a video link showing the issue https://youtu.be/cKicWv_58_s

I also tried making a small sample app using NavigationView with a 3 column layout and this works normally, so it seems to be isolated to NavigationSplitView

I also included a small more simple sample than the WWDC sample below that can be used to showcase the issue and shows that NavigationView works fine

import SwiftUI

struct ContentView: View {
    @State var presentingNSV: Bool = false
    @State var presentingNV: Bool = false
    var body: some View {
        HStack {
            Button {
                presentingNSV.toggle()
            } label: {
                Text("NavigationSplitView")
            }
            Button {
                presentingNV.toggle()
            } label: {
                Text("NavigationView")
            }
        }
        .fullScreenCover(isPresented: $presentingNSV) {
            NSV(isPresented: $presentingNSV)
        }
        .fullScreenCover(isPresented: $presentingNV) {
            NV(isPresented: $presentingNV)
        }
    }
}

struct TestList: View {
    var body: some View {
        List(0..<1000) { item in
            Text("Item \(item)")
        }
    }
}

struct NSV: View {
    @Binding var isPresented: Bool
    var body: some View {
        let _ = Self._printChanges()
        NavigationSplitView {
            TestList()
            .toolbar {
                ToolbarItem {
                    Button {
                        isPresented = false
                    } label: {
                        Text("Back")
                    }
                }
            }
        } content: {
            TestList()
        } detail: {
            TestList()
        }
    }
}

struct NV: View {
    @Binding var isPresented: Bool
    var body: some View {
        let _ = Self._printChanges()
        NavigationView {
            TestList()
            .toolbar {
                ToolbarItem {
                    Button {
                        isPresented = false
                    } label: {
                        Text("Back")
                    }
                }
            }
            TestList()
            TestList()
        }
        .navigationViewStyle(.columns)
    }
}

Video using this sample code https://youtu.be/GAiRL8TprrM

0

There are 0 best solutions below