Joe G
Joe G

Reputation: 95

SwiftUI - Unable to navigate to next View from List/NavigationLink with navigationDestination

My first View represents a login screen, which then takes you to the home View. I've read through the documentation and watched WWDC22 - The SwiftUI cookbook for navigation which got me on the right track, but I'm unable to navigate past the home View (ProductFamilyView). I'm not getting any errors but I'm clearly doing something wrong. I've tried using NavigationSplitView instead of NavigationStack but that led to more challenges. I would appreciate some insight to what I'm doing wrong here. Thanks.

import SwiftUI

@main
struct ProductFinderTest5App: App {

    @StateObject var navModel = NavigationModel.shared
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(navModel)
        }
    }
}

class NavigationModel: ObservableObject {

    @Published var path = NavigationPath()
    static var shared = NavigationModel()
    
    func popToRoot() {
        path.removeLast(path.count)
    }
}

struct ContentView: View {
    
    @EnvironmentObject var navModel : NavigationModel
    @State private var navigated = false
    
    var body: some View {
        
        NavigationStack(path: $navModel.path) {
            Button(action: {
                self.navigated.toggle()
            }, label: {
                Text("LOGIN")
                    .bold()
                    .padding()
                    .foregroundColor(.white)
                    .background(
                        Capsule()
                            .foregroundColor(.green)
                            .frame(width: 200, height: 50)
                    )
            })
            .navigationDestination(isPresented: $navigated) {
                ProductFamilyView()
            }
        }
        .environmentObject(navModel)
    }
}

struct ProductFamilyView: View { //This is my home view
    
    @EnvironmentObject var navModel : NavigationModel
    @State private var productFamilies = ProductFamilyModel.loadData()
    @State private var selectedFamily : ProductFamilyModel?
    
    var body: some View {
        
        List(productFamilies, selection: $selectedFamily) { productFamily in
            NavigationLink(productFamily.name, value: productFamily)
        }
        .navigationDestination(for: ProductFamilyModel.self) { productFamily in
            PartNumberView(productFamily: productFamily)
        }
        .navigationTitle("Product Families")
        .navigationBarTitleDisplayMode(.inline)
        .environmentObject(navModel)
    }
}

struct PartNumberView: View {
    
    @EnvironmentObject var navModel : NavigationModel
    @State var productFamily: ProductFamilyModel
    @State private var selectedPartDetail : ProductModel?
        
    var body: some View {
        let partDetails = productFamily.partNumbers.compactMap( {$0} )
        
        List(partDetails, selection: $selectedPartDetail) { partDetail in
            NavigationLink(partDetail.partNumber, value: partDetail)
        }
        .navigationDestination(for: ProductModel.self) { partDetail in
            PartNumberRow(productDetail: partDetail)
        }
        .navigationTitle("\(productFamily.name)")
        .navigationBarTitleDisplayMode(.inline)
        .navigationBarItems(trailing: HomeButtonView() )
        .environmentObject(navModel)
    }
}
struct PartNumberRow: View {
    
    @EnvironmentObject var navModel : NavigationModel
    @State var productDetail : ProductModel
    
    var body: some View {
        
        List {
            Text("Description: \(productDetail.description)")
            Text("Product Family: \(productDetail.productFamily)")
        }
        .navigationTitle("\(productDetail.partNumber)")
        .navigationBarTitleDisplayMode(.inline)
        .navigationBarItems(trailing: HomeButtonView() )
        .environmentObject(navModel)
    }
}

struct HomeButtonView: View {
    
    @EnvironmentObject var navModel : NavigationModel
    
    var body: some View {
        
        Button(action: {
            self.navModel.popToRoot()
        }, label: {
            Image(systemName: "house")
        })
        .environmentObject(navModel)
    }
}

class ProductFamilyModel: ObservableObject, Identifiable {
    
    var id = UUID().uuidString
    var name : String
    var partNumbers : [ProductModel?]
    
    init(name: String) {
        self.name = name
        self.partNumbers = []
    }
    
    static func loadData() -> [ProductFamilyModel] {
        
        let productFamilyNames = ["Product Family 1",
                                  "Product Family 2",
                                  "Product Family 3"]
        let partDetails = ProductModel.loadData()
        var productFamilies : [ProductFamilyModel] = []
        
        for productFamilyName in productFamilyNames {
            
            let productFamily = ProductFamilyModel(name: productFamilyName)
            
            for partDetail in partDetails {
                
                if partDetail.productFamily == productFamilyName {
                    productFamily.partNumbers.append(partDetail)
                }
            }
            productFamilies.append(productFamily)
        }
        return productFamilies
    }
}

extension ProductFamilyModel : Hashable {
    
    static func == (lhs: ProductFamilyModel, rhs: ProductFamilyModel) -> Bool {
        return lhs.id == rhs.id && lhs.name == rhs.name
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(name)
    }
}

struct Product {
    
    var partNumber    : String = "part number"
    var productFamily : String = "product family"
    var description   : String = "description"
}

class ProductModel: ObservableObject, Identifiable  {
    
    var id = UUID().uuidString
    var partNumber    : String
    var productFamily : String
    var description   : String
    
    init(model: Product) {
        self.partNumber    = model.partNumber
        self.productFamily = model.productFamily
        self.description   = model.description
    }
    static func loadData() -> Set<ProductModel> {
        
        let productDetails : Set<ProductModel> = [
            ProductModel(model: Product(
                partNumber: "100-1111-101",
                productFamily: "Product Family 1",
                description: "Part Number 100-1111-101"
            )),
            ProductModel(model: Product(
                partNumber: "100-1111-102",
                productFamily: "Product Family 1",
                description: "Part Number 100-1111-102"
            )),
            ProductModel(model: Product(
                partNumber: "100-1111-103",
                productFamily: "Product Family 1",
                description: "Part Number 100-1111-103"
            )),
            ProductModel(model: Product(
                partNumber: "200-1111-101",
                productFamily: "Product Family 2",
                description: "Part Number 200-1111-101"
            )),
            ProductModel(model: Product(
                partNumber: "200-1111-102",
                productFamily: "Product Family 2",
                description: "Part Number 200-1111-102"
            )),
            ProductModel(model: Product(
                partNumber: "200-1111-103",
                productFamily: "Product Family 2",
                description: "Part Number 200-1111-103"
            )),
            ProductModel(model: Product(
                partNumber: "300-1111-101",
                productFamily: "Product Family 3",
                description: "Part Number 300-1111-101"
            )),
            ProductModel(model: Product(
                partNumber: "300-1111-102",
                productFamily: "Product Family 3",
                description: "Part Number 300-1111-102"
            )),
            ProductModel(model: Product(
                partNumber: "300-1111-103",
                productFamily: "Product Family 3",
                description: "Part Number 300-1111-103"
            ))
        ]
        return productDetails
    }
}

extension ProductModel : Hashable {
    
    static func == (lhs: ProductModel, rhs: ProductModel) -> Bool {
        return lhs.id == rhs.id && lhs.partNumber == rhs.partNumber && lhs.productFamily == rhs.productFamily && lhs.description == rhs.description
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(partNumber)
        hasher.combine(productFamily)
        hasher.combine(description)
    }
}

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

I've looked at multiple examples but most of them start with a List, which contains the NavigationLink followed by the .navigationDestination but my project starts with a button before I show my first List. It wasn't clear to me how to navigate to the home View (ProductFamilyView) without starting with a NavigationStack or NavigationSplitView. My real project will have a couple different Views to get through the login process before I get to the Home View.

Upvotes: 1

Views: 422

Answers (1)

Joe G
Joe G

Reputation: 95

I found a solution to the problem. The modified ContentView, ProductFamilyView and PartNumberView structs are below. Instead of using an isPresented Binding in .navigationDestination to navigate to my root view, I use a Bool to see if the user is logged in or not. Then I define my .navigationDestination for the two views I want to navigate to. These modifiers need to be in the same view as where the NavigationStack is defined. I also removed "selection: $selectedFamily" from the List of ProductFamilies and "selection: $selectedPartDetail" from the list of PartDetails. I realized these were not needed.

struct ContentView: View {

@EnvironmentObject var navigationModel : NavigationModel

@State private var productFamilies : [ProductFamilyModel]? = ProductFamilyModel.loadData()
@State private var isLoggedIn = false

var body: some View {
    
    NavigationStack(path: $navigationModel.path) {
        
        if isLoggedIn {
            
            if let productFamilies {
                
                ProductFamilyView(productFamilies: productFamilies)
                    .navigationDestination(for: ProductFamilyModel.self) { productFamily in
                        PartNumberRowView(productFamily: productFamily)
                    }
                    .navigationDestination(for: ProductModel.self) { partDetail in
                        PartNumberView(productDetail: partDetail)
                    }
            }     
        }
        else {
            Button(action: {
                self.isLoggedIn.toggle()
            }, label: {
                Text("LOGIN")
                    .bold()
                    .padding()
                    .foregroundColor(.white)
                    .background(
                        Capsule()
                            .foregroundColor(.green)
                            .frame(width: 200, height: 50)
                    )
            })
        }
    }
}

struct ProductFamilyView: View { // Show a list of Product Families in the sidebar or root view
    
    var productFamilies: [ProductFamilyModel]

    var body: some View { //This is the home or root view
        
        List(productFamilies) { productFamily in
            NavigationLink(productFamily.name, value: productFamily)
        }
        .navigationTitle("Product Families")
        .navigationBarTitleDisplayMode(.inline)
    }
}

struct PartNumberRowView: View { // Show a list of part numbers associated with a product family
    
    var productFamily: ProductFamilyModel
        
    var body: some View {
        
        let partDetails = productFamily.partNumbers!.sorted(by: { $0.partNumber < $1.partNumber })
        
        List(partDetails) { partDetail in
            NavigationLink("\(partDetail.partNumber)", value: partDetail)
        }
        .navigationTitle("\(productFamily.name)")
        .navigationBarTitleDisplayMode(.inline)
        .navigationBarItems(trailing: HomeButtonView() )
    }
}

Upvotes: 1

Related Questions