NEW: Learn to build the incredible iOS 15 Weather app today! >>

How does NavigationView work internally?

Forums > SwiftUI

@rjo  

I'm trying to write a simple replacement for NavigationView/NavigationLink as an experiment. I've seen a couple of projects on Github that do this, but they both fail in the same way that my code fails, but where NavigationView works.

In a NavigationView, the pushed views maintain the value of their @State variables. Say you have a TextEditor and you edit the text, then push a new view on the stack. After going back, the editor will maintain whatever changes you had made. Quick iOS example:

import SwiftUI

struct MyView: View {
    @State var text = "default text"
    var body: some View {
        VStack {
            TextEditor(text: $text)
            NavigationLink(destination: MyView()) {
                Text("Push")
            }
        }
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView {
            MyView()
        }
    }
}

I'm new to SwiftUI so my code is not great but this is what I'm doing for my simple experiment for a macos build

import SwiftUI

typealias Push = (AnyView) -> ()
typealias Pop = () -> ()

struct PushKey: EnvironmentKey {

    static let defaultValue: Push = { _ in }

}

struct PopKey: EnvironmentKey {

    static let defaultValue: Pop = {() in }

}

extension EnvironmentValues {

    var push: Push {
        get { self[PushKey.self] }
        set { self[PushKey.self] = newValue }
    }

    var pop: Pop {
        get { self[PopKey.self] }
        set { self[PopKey.self] = newValue }
    }
}

struct ContentView: View {
    @State private var stack: [AnyView]

    var body: some View {
        currentView()
            .environment(\.push, push)
            .environment(\.pop, pop)
            .frame(width: 600.0, height: 400.0)
    }

    public init() {
        _stack = State(initialValue: [AnyView(AAA())])
    }

    private func currentView() -> AnyView {
        if stack.count == 0 {
            return AnyView(Text("stack empty"))
        }
        return stack.last!
    }

    public func push(_ content: AnyView) {
        stack.append(content)
    }

    public func pop() {
        stack.removeLast()
    }
}

struct AAA : View {
    @State private var data = "default text"
    @Environment(\.push) var push

    var body: some View {
        VStack {
            TextEditor(text: $data)
            Button("Push") {
                self.push(AnyView(BBB()))
            }
        }
    }
}

struct BBB : View {
    @Environment(\.pop) var pop

    var body: some View {
        VStack {
            Button("Pop") {
                self.pop()
            }
        }
    }
}

With that code, if you edit the text, click Push, then Pop back, you will see the editor state has been reset to its default value.

I've read in numerous places that @State is only valid while the view is in the hierarchy, so how does NavigationView accomplish this? Obviously none of us have seen the source code for it but does anyone have an idea of how Apple might be storing the Views in its stack such that @State is retained?

   

Have you watched the "Demystify SwiftUI" session from WWDC21?

@State properties are retained somewhere outside of the View struct. The exact details of that are unknown to me.

   

@rjo  

Yes I've watched it, but it does not shed any light on this question. Perhaps it just means that NavigationView undocumented aspects of @State to achieve its functionality, which is just frustrating in the way this kind of thing always is.

   

NavigationViews at their core are UINavigationControllers, with hosting controllers. Navigation state is stored outside the SwiftUI view hierarchy (for all practical purposes, there is likely mirrored state), in UINavigationController.

1      

@rjo  

@ericlewis thats known, but does not explain how the child view state is maintained as a UINavigationController does not do any magic with respect to the state of UIViews in its hierarchy.

I suspect that the use of AnyView is the culprit somehow but can't find any documentation that would clearly ellucidate exactly why. It just seems mysterious and like NavigationView has some access to undocumented internals.

   

@rjo  

I wonder if @twostraws has any insight into this conundrum.

   

Hacking with Swift is sponsored by Sentry

SPONSORED With Sentry’s error and performance monitoring for iOS, you see mobile vitals that actually matter, can solve any latency issues quickly, and learn how each release is performing over time.

Learn More

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.