SwiftUI navigation regression in iOS 15

I am struggling with an unwanted navigation that is triggered by iOS 15 SwiftUI machinery when an unrelated property of my data model changes. I've distilled this down to the following minimal app (I deliberately squeezed everything into a single view for the sake of brevity here.) To view the bug launch the app in iOS15 and navigate to the third view - then every two seconds it will navigate back and forth from that view. Any idea how to work around this bug?

import SwiftUI

class Model: ObservableObject {
    static let singleton = Model()
    private init() {
        Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [self] _ in
            unrelated += 1
        }
    }
    @Published var unrelated: Int = 0
    @Published var text = "Hello World"
    @Published var showChild = false
    @Published var showSubChild = false
}

struct ContentView: View {
    @ObservedObject var model = Model.singleton
    @State var checked = true

    var body: some View {
        NavigationView {
            VStack {
                Text(model.text)
                Toggle("", isOn: $checked)
                if checked {
                    NavigationLink("First View", destination:
                        NavigationLink("Second View", destination:
                            Text("Third View"),
                            isActive: $model.showSubChild
                        ),
                        isActive: $model.showChild
                    )
                } else {
                    Text("something else")
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View { ContentView() }
}

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

I don't [yet] know the answer to your problem but here are a few observations that may be useful.

Whenever a property such as unrelated is mutated it causes the body function to re-evaluate and rebuild. It rebuilds at the second view (despite both booleans being true) then when the change comes in again to rebuilds to third view.

I think a simplification of the problem is this:

class Model: ObservableObject {
  @Published var showChild = true
  @Published var showSubChild = true
}

struct ContentView: View {
  @StateObject var model = Model()
  
  var body: some View {
    NavigationView {
      VStack {
        Text("Hello")
        NavigationLink("First View",
                       destination: NavigationLink("Second View",
                                                   destination:
                                                    Text("Third View"),
                                                   isActive: $model.showSubChild),
                       isActive: $model.showChild
        )
      }
    }
  }
}

Given this model, why doesn't it come up to the third view? (If you run this the second view will appear instead of pushing both subviews onto the navigation stack.)

Recently, the smart duo over at pointfree published Open Sourcing SwiftUI Navigation and produced a detailed series of episodes about deep linking and navigation. It is on my todo list to watch these, but I suspect your problem is addressed in here!

cc @stephencelis @mbrandonw

Terms of Service

Privacy Policy

Cookie Policy