Skip to content

Instantly share code, notes, and snippets.

@chriseidhof
Created December 2, 2021 15:27
Show Gist options
  • Save chriseidhof/f7a19206d07ab04335f4156d9e99f40e to your computer and use it in GitHub Desktop.
Save chriseidhof/f7a19206d07ab04335f4156d9e99f40e to your computer and use it in GitHub Desktop.
import SwiftUI
struct DetailView: View {
@Binding var showDetail: Bool
var body: some View {
List {
NavigationLink("detail", isActive: $showDetail) {
Text("Detail")
}
}
}
}
struct MyList: View {
@Binding var levels: [Bool]
var body: some View {
List {
NavigationLink("Detail", isActive: $levels[0]) {
DetailView(showDetail: $levels[1])
}
}
.overlay(
Button("Navigate To Detail") {
levels = [true, true]
}
)
}
}
struct ContentView: View {
@State var levels = [false, false]
var body: some View {
NavigationView {
MyList(levels: $levels)
}
.navigationViewStyle(.stack)
.overlay(Text(levels.map { $0 ? "t" : "f"}.joined()), alignment: .bottom)
}
}
@pacu
Copy link

pacu commented Dec 2, 2021

In regards to this question

I'm playing around with SwiftUI navigation again and I can't seem to figure it out. I want to programmatically navigate from a root view to a detail view. SwiftUI navigates correctly but then immediately pops the last view. Why?

I've experienced this on parent views re-rendering due to some change either in their own models or in their parent view (the grand-parent view?).

It could be that your ContentView is rendered again due to some change and then the SwiftUI rendering engine recreates the whole hierarchy.

Without knowing how you bootstrapped your app, this modifier is a probably suspect .overlay(Text(levels.map { $0 ? "t" : "f"}.joined()), alignment: .bottom) for triggering that change that pops your view out.

One workaround that does work for deep programmatic navigation is setting a new id on the NavigationView, but that breaks the animations.

This tells me that the rendering engine is able to tell that there's no change with thanks to this ID and that's why the workaround works.

This sucks and it's cumbersome and does not look like what Apple always showcases at their conferences and sample code. But I got rid of most navigation bugs by detaching navigation state from view states and maintaining that on another layer of the app that is not bound to SwiftUI's View struct rendering lifecycle.

@chriseidhof
Copy link
Author

Thank you for taking the time to write such a nice comment, but I don't think it's correct. Without the overlay the problem persists.

With regard to the id it's actually the other way around: by setting a new ID the rendering engine transitions to a new view hierarchy and shows the correct view without animating.

@eDeniska
Copy link

eDeniska commented Dec 3, 2021

Tried this myself, even with separate properties for different levels.

Could it be related to double navigation animations happening simultaneously? I mean, UIKit typically throws in warnings when you try to do multiple navigations until animation is complete. However, no warnings in console this time.

@bigmountainstudio
Copy link

bigmountainstudio commented Dec 4, 2021

A couple of notes from testing:

While this causes navigation to pop:
DetailView(showDetail: $levels[1])

These options do not:
MyListDetailView(showDetail: .constant(true))
MyListDetailView(showDetail: .constant(levels[1]))

I also tried:

  • Creating and passing an ObservableObject
  • Using @EnvironmentObject to share levels data

Neither worked. 🙁

I submitted a Feedback for this (FB9795803).

@chriseidhof
Copy link
Author

Thanks. Yes, I have tried those things as well. It's easy to "fix" this by disabling various parts, but in the end, I "only" want navigation that's driven by a single @State property.

@dzmitry-antonenka
Copy link

Hello @chriseidhof , I think there's related post on Apple Forum. When parent view state changes, Navigation automatically pops out child views (on iOS14.0 it propagated changes to update child views instead AFAIK). Unfortunately I don't have 14.0SDK to verify for this use-case

@urbanp11
Copy link

I don't think SwiftUI is ready for this kind of navigation yet. Navigation is controlled by stated, but it has only 2 states... may be we need more? Somehow there is a missing state for performing the action, etc. I would like some interface like withAnimation...

performNavigation(withAnimaion: .animateLastOnly) {
   levels = [true, true]
}

Also it would be awesome if it would work for closing too.

I think for now, you can use workaround:

Button("Navigate To Detail") {
    levels = [true, false]

    // slow down due to the opening of the first screen
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        levels = [true, true]
    }
}

Code with changes:

import SwiftUI

struct DetailView: View {
    @Binding var showDetail: Bool

    var body: some View {
        List {
            NavigationLink("detail", isActive: $showDetail) {
                Text("Detail")
            }
        }
    }
}

struct MyList: View {
    @Binding var levels: [Bool]

    var body: some View {
        List {
            NavigationLink("Detail", isActive: $levels[0]) {
                DetailView(showDetail: $levels[1])
            }
        }
        .overlay(
            Button("Navigate To Detail") {
                levels = [true, false]

                // slow down due to the opening of the first screen
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                    levels = [true, true]
                }
            }
        )
    }
}

struct ContentView: View {
    @State var levels = [false, false]

    var body: some View {
        NavigationView {
            MyList(levels: $levels)
        }
        .navigationViewStyle(.stack)
        .overlay(Text(levels.map { $0 ? "t" : "f"}.joined()), alignment: .bottom)
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment