Search code examples
swiftuiswiftui-navigationlinkios16swiftui-navigationstack

Make Custom SwiftUI Views with ObservedObjects compatible with NavigationLink iOS 16


I don't feel that the following example is exhaustive (from Hacking With Swift). A lot of online examples tend to keep it quite elementary like this, where NavigationLinks are implemented based off of simple Hashable models.

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List(1..<50) { i in
                NavigationLink(value: i) {
                    Label("Row \(i)", systemImage: "\(i).circle")
                }
            }
            .navigationDestination(for: Int.self) { i in
                Text("Detail \(i)")
            }
            .navigationTitle("Navigation")
        }
    }
}

I have views which take arguments for ObservedObjects. For example ViewA(firstViewModel: self.firstViewModel) and ViewB(secondViewModel: self.secondViewModel. My ObservedObjects are instantiated in my root view where NavigationStack is defined. How can I use the new NavigationStack and NavigationLink paradigm in iOS 16 to support navigating to my custom SwiftUI views? This is something that was easily done in iOS 15 by just doing

NavigationLink(destination: someView(viewModel: self.viewModel) { Text("Test Link") } 

Now it's all programmatic. Can I wrap some properties in an enum which point to my SwiftUI Views? If so, can I still keep references to the correct runtime objects I want to pass in to the views? A little lost on implementing this navigation hierarchy beyond the elementary examples.

Please see my minimally producible example below to exercise the exact issue I am facing:

struct ContentView: View {

    var body: some View {
        NavigationStack {
            ProfileView()

            // there is other logic for other root views, but
            // this problem can just focus on ProfileView in NavigationStack
        }
    }

}

struct ProfileView: View {

    @StateObject var progressViewModel = ProgressViewModel()

    @StateObject var challengesViewModel = ChallengesViewModel()

    @StateObject var subscriptionViewModel = SubscriptionViewModel() 

    var body: some View {
        VStack {

            // Below is the code that is broken, and I'm not sure how to adapt it 
            // to the new iOS 16 navigation patterns since I am not reusing the same view
            // as in the above example
            NavigationLink(destination: ProgressView(progressViewModel: self.progressViewModel)) {
                Text("User progress nav link")
            }
            
            NavigationLink(destination: ChallengesView(challengesViewModel: self.challengesViewModel)) {
                Text("Challenges nav link")
            }

            NavigationLink(destination: SubscriptionView(subscriptionViewModel: self.subscriptionViewModel)) {
                Text("Subscription nav link")
            }
        }
    }

}

struct ProgressView: View {
    @ObservedObject var progressViewModel: ProgressViewModel

    var body: some View {
        VStack {
            Text(progressViewModel.overallUserProgressString)
        }
    }
}

struct ChallengesView: View {
    @ObservedObject var challengesViewModel: ChallengesViewModel

    var body: some View {
        VStack {
            Text(challengesViewModel.numberOfChallengesPerformedString)
        }
    }
}

struct SubscriptionView: View {
    @ObservedObject var subscriptionViewModel: SubscriptionViewModel

    var body: some View {
        VStack {
            Text(subscriptionViewModel.localSubscriptionPriceString)
        }
    }
}

Solution

  • iOS 16 & Xcode 14

    Here is a minimum answer which satisfies the design pattern for basic programmatic Navigation stack management with custom views.

    enum CustomNavTypes: String, Hashable {
        case viewA = "View A"
        case viewB = "View B"
        case viewC = "View C"
    }
    
    struct CustomView: View {
        @State var navigationViewStack: [CustomNavTypes] = []
        var body: some View {
            NavigationStack {
                VStack {
                    Button(action: {
                        navigationViewStack.append(.viewA)
                    }) {
                        Text("View A")
                    }
                    Button(action: {
                        navigationViewStack.append(.viewB)
                    }) {
                        Text("View B")
                    }
                    Button(action: {
                        navigationViewStack.append(.viewC)
                    }) {
                        Text("View C")
                    }
                }
                .navigationDestination(for: ProfileNavTypes.self) { value in
                    switch value {
                        case .viewA: ViewA() // can still pass in view models objects if needed
                        case .viewB: ViewB()
                        case .viewC: ViewC()
                    }
                }
            }
        }
    }
    

    I appreciate @malhal for showing the strength of the pure MV approach, but realistically I don't expect all developers or apps to be leveraging a codebase that is purely MV (where the SwiftUI view specifically is treated as the intended ViewModel, which it is...). While MV improves runtime performance and eliminates a lot of boilerplate code, I still imagine a lot of people (including myself) using viewmodel objects out of convenience or to manage generic paradigms, even if we are migrating to a more strict MV setup.

    I hope this helps, I am certainly learning. I'm sure this isn't perfect but it's a start.