Reputation: 95
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
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