UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

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      

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

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.