iOS 16: NavigationPath is cleared upon backgrounding of app

On iOS 16 (16.4 specifically) and not iOS 17, I am running into an issue where navigationPath in MyNavigationViewModel is cleared when my app is backgrounded. I seemingly couldn't find much discussion around this happening. I'm hoping there is a work around and this is not intended, as the larger project I am working on depends on the state of navigationPath persisting despite being backgrounded.

Note: Since I am working on a larger project, so I'm trying to mimic it's structure, which is why I am doing things like passing my view model as an environment object, and having a button in the side bar to push the initial view on to the navigation destination.

To reproduce and observe the issue using the sample code below:

  1. Swipe from the left edge of the screen to open the side bar (not sure why applying the .balanced style is not having the sidebar displace the detail view, but that's a separate issue I'm not concerned about...)
  2. Click the button "Present Detail View"
  3. Background the app
  4. Wait some time (you will be able to see the print statement in the didSet print out a count of 0 for navigationPath)
  5. Reopen the app
  6. Observe the Base detail view is displayed, as opposed to the view that was pushed by pressing the side bar button. The navigation path was cleared.

Any advice on how to deal with this issue?


public class MyNavigationViewModel: ObservableObject {
    @Published var navigationPath: [MyType] =  [] {
        didSet {
            print("Count: \(self.navigationPath.count)")
        }
    }
}

public struct MyType: Hashable {
    public var string: String
}

public struct MyDestination: View {
    @EnvironmentObject var viewModel: MyNavigationViewModel
    var text: String
    let onBackPressed: ()->()
    let onNextPressed: ()->()
    
    public var body: some View {
        VStack {
            HStack {
                Button(action: { self.onBackPressed() }, label: {Text("Back")})
                Button(action: { self.onNextPressed() }, label: {Text("Next")})
            }
            Text("String: \(self.text).")
        }
    }
}

@available(iOS 16.0, *)
public struct MyNavigationView: View {
    @EnvironmentObject var viewModel: MyNavigationViewModel
    
    public var body: some View {
        NavigationSplitView(
            sidebar: {
                VStack {
                    Button(
                        action: {
                            self.$viewModel.navigationPath.wrappedValue.append(MyType(string: "Hello"))
                        },
                        label: {
                            Text("Present Detail View")
                        }
                    )
                }
            }, detail: {
                NavigationStack(path: self.$viewModel.navigationPath) {
                    Group {
                        Text("This is the detail base view.")
                    }
                    .navigationDestination(for: MyType.self) { data in
                        MyDestination (
                            text: data.string,
                            onBackPressed: {
                                if(self.$viewModel.navigationPath.wrappedValue.count > 0) {
                                    self.$viewModel.navigationPath.wrappedValue.removeLast()
                                }
                            },
                            onNextPressed: {
                                self.$viewModel.navigationPath.wrappedValue.append(MyType(string: "This is the next thing"))
                            }
                        )
                    }
                }
            }
        )
        .navigationSplitViewStyle(.balanced)
        .environmentObject(self.viewModel)
    }
}

@available(iOS 16.0, *)
struct ContentView: View {
    var body: some View {
        MyNavigationView().environmentObject(MyNavigationViewModel())
    }
}

@available(iOS 16.0, *)
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

This is how iOS works. After your app enters the background, it continues in an "active" state for a short while, then is suspended. After it's suspended, it's eligible for termination, and the next time it's brought to the foreground it's re-launched, causing in-memory values like your navigation path to be re-initialized. The previous value is lost unless you do something to persist it.

To solve this, you need to opt into the state restoration mechanism, to cause important information like the navigation path to be persisted across the termination/re-launch:

You can also learn more about app lifecycles here:

iOS 16: NavigationPath is cleared upon backgrounding of app
 
 
Q