WWDC24 SALE: Save 50% on all my Swift books and bundles! >>

Project 9 - Day 46 Solution

Forums > 100 Days of SwiftUI

Solved Day 46, but with some doubts, posting my solution here and looking to see if I can get some answers/improvement suggestions. In particular, I have a doubt about the last part.

This is what I implemented so far based on the challenge questions

  1. Change project 7 (iExpense) so that it uses NavigationLink for adding new expenses rather than a sheet. (Tip: The dismiss() code works great here, but you might want to add the navigationBarBackButtonHidden() modifier so they have to explicitly choose Cancel.)

  2. Try changing project 7 so that it lets users edit their issue name in the navigation title rather than a separate textfield. Which option do you prefer?

struct ExpensesChallengeView: View {
    @State private var expenses = Expenses()

    var body: some View {
        VStack {
            NavigationStack {
                List {
                   // Display expenses code, code removed for easier reading
                }
                .navigationTitle("iExpense")
                .toolbar {
                    NavigationLink(destination: AddViewChalllengeView(expenses: expenses)) {
                        Image(systemName: "plus")
                    }
                }
            } 
        }
    }

AddView

struct AddViewChalllengeView: View {
    @State private var name = "New Expense"
    @State private var type = "Personal"
    @State private var amount = 0.0

    @Environment(\.dismiss) var dismiss

    var expenses: Expenses

    let types = ["Business", "Personal"]

    var body: some View {
        NavigationStack {
            Form {      
                Picker("Type", selection: $type) {
                    ForEach(types, id: \.self) {
                        Text($0)
                    }
                }

                TextField("Amount", value: $amount, format: .currency(code: Locale.current.currency?.identifier ?? "USD"))
                    .keyboardType(.decimalPad)

            }
            .navigationTitle($name)
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .confirmationAction) {
                    Button("Save") {
                        let item = ExpenseItem(name: name, type: type, amount: amount)
                        expenses.items.append(item)
                        dismiss()
                    }
                }

                ToolbarItem(placement: .cancellationAction) {
                    Button("Cancel") {
                        dismiss()
                    }
                }
            }
        }
        .navigationBarBackButtonHidden()
    }

}

Return to project 8 (Moonshot), and upgrade it to use NavigationLink(value:). This means adding Hashable conformance, and thinking carefully how to use navigationDestination().

This is the part i'm confused, I used NavigationLink(value:) in my grid view and had to conform Mission to Hashable, but in doing that, my Mission Struct had a 'does not conform to equatable' error and thus had to make the Mission struct conform to equatable by implementing a custom == function. Why is this the case? This happens after I change MoonshotGridView (shown below) to use NavigationLink(value:) and I just can't see where it's performing an equality comparison.

struct MoonshotGridView: View {
    let astronauts: [String: Astronaut]    
    let missions: [Mission] 
    let columns = [GridItem(.adaptive(minimum: 150))]
    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns) {
                ForEach(missions) { mission in
                    NavigationLink(value: mission) {
                        VStack {
                            Image(mission.image)
                                .resizable()
                                .scaledToFit()
                                .frame(width: 100, height : 100)
                                .padding()

                            VStack {
                                Text(mission.displayName)
                                    .font(.headline)
                                    .foregroundStyle(.white)

                                Text(mission.formattedDate)
                                    .font(.caption)
                                    .foregroundStyle(.gray)
                            }
                            .padding(.vertical)
                            .frame(maxWidth: .infinity)
                            .background(.lightBackground)
                        }
                        .clipShape(.rect(cornerRadius: 10))
                        .overlay(RoundedRectangle(cornerRadius:  10)
                            .stroke(.lightBackground)
                        )
                    }
                    .navigationDestination(for: Mission.self) { mission in
                        MissionView(mission: mission, astronauts: astronauts)
                    }
                } 

            }

            .padding([.horizontal, .bottom])
            .background(.darkBackground)
        }
    }
}

import Foundation

struct Mission: Hashable, Codable, Identifiable, Equatable {

    static func == (lhs: Mission, rhs: Mission) -> Bool {
        return lhs.id == rhs.id
            && lhs.launchDate == rhs.launchDate
            && lhs.crew == rhs.crew
            && lhs.description == rhs.description
    }

    struct CrewRole: Codable, Equatable, Hashable {
        let name: String
        let role: String
    }

    let id: Int
    let launchDate: Date?
    let crew: [CrewRole]
    let description: String

    var displayName: String {
        "Apollo \(id)"
    }

    var image: String {
        "apollo\(id)"
    }

    var formattedDate: String {
        launchDate?.formatted(date: .abbreviated, time: .omitted) ?? "N/A"
    }

}

2      

I tried to reproduce your issue and was unable.

I simply implemented Hashable on my Mission struct and it did not require the Equatable.

Most likely some configuration issue. Check your iOS version, and restart XCode

2      

Hi swiftasroy

Hashable requires ALL elements to conform to Hashable.

struct Mission: Codable, Hashable, Identifiable {
    struct CrewRole: Codable {
        let name: String
        let role: String
    }

    let id: Int
    let launchDate: Date?
    let crew: [CrewRole] // <- CrewRole is not `Hashable`
    let description: String

    // Rest of your code
}

You will get this Type 'Mission' does not conform to protocol 'Equatable' is one of those helpful/unhelpful errors. By making CrewRole also Hashable the error should go away.

struct Mission: Codable, Hashable, Identifiable {
    struct CrewRole: Codable, Hashable {
        let name: String
        let role: String
    }

    let id: Int
    let launchDate: Date?
    let crew: [CrewRole]
    let description: String

    // Rest of your code
}

3      

Save 50% in my WWDC sale.

SAVE 50% To celebrate WWDC24, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

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.