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

SOLVED: Day 47 challenge - How many times activity was completed

Forums > 100 Days of SwiftUI

I completed this successfully. However as i refactored my code on the Activity View file (which shows the name of the habit, its description and allows you to increase how many times you've completed it), i ran into the below error:

ForEach<Array<Habit>, UUID, NavigationLink<Text, Never>>: the ID xyz occurs multiple times within the collection, this will give undefined results!

This will happen if I increase the completion count on the activity before getting its index (required in order to insert a copy of the updated activity struct with the new completion count). The effect is that if I run the app this way, once i go into the detail view and increase completion count, the above error shows in the console. Then if i go back to the main content view i might run into another copy of the same detail view, and now the top entry on my list has the same as the one i just modified

Relevant code

ContentView:


  NavigationStack{
            List{
                ForEach(habitList.list) {habit in
                    NavigationLink(value: habit) {
                        Text("\(habit.name) id: \(habit.id)")
                    }
                }
                .onDelete(perform: removeItems)
            }

Activity View that works:

        Button("Completed this today") {
            let index = habits.list.firstIndex(of: habit)
            habit.completions += 1
            let habitCopy = habit
            habits.list[index ?? 0] = habitCopy          
        }

Activity View that doesn't work:

       Button("Completed this today") {
            habit.completions += 1
            let index = habits.list.firstIndex(of: habit)
            let habitCopy = habit
            habits.list[index ?? 0] = habitCopy
        }

Struct:

  struct Habit: Identifiable, Hashable, Codable, Equatable {
    var id = UUID()
    var name: String
    var description: String
    var completions: Int
}

I sort of understand that the issue is a result of modifying the original Habit and breaking how the id: works in the ForEach, something goes wrong with its UUID, but was hoping someone here can explain the issue more concretely. Thanks!

   

I have been known to provide not-quite-accurate information. But maybe this can help?

Hash

Look at the defined properties in your struct. Think how you might combine all those values into some kind of fingerprint that helps differentiate all the habits you might be tracking.

Your habits array contains the fingerprints of all the habit objects in your application. However, if you CHANGE one of the PROPERTIES, you also change its fingerprint. So how do you find the original habit in your collection, if you can't match the new fingerprint to an old fingerprint?

Look at your code. In the example that works, you find the matching finger print first, then you change it, then insert the change back into the collection.

In the code that "doesn't work" you change the completions property, which changes the fingerprint. Then you try to find the new fingerprint in your collection. News Flash: It Doesn't Exist.

Paste this code into Playgrounds and run it.

struct Habit: Identifiable, Hashable, CustomStringConvertible {
    var id = UUID()
    var name: String
    var completions: Int
    var description: String {
        name + " (" + completions.description + ") " + self.hashValue.description
    }
}

var walking = Habit( name: "Walking", completions: 3 )
var running = Habit( name: "Running", completions: 1 )
var coding  = Habit( name: "Coding",  completions: 4 )

var habits = [ walking, running, coding ]

// select one activity at random
var randomActivity = habits.randomElement()!

print("This is a random activity. Find its match below.")
print("Random is: " + randomActivity.description) // Find the hash
print("\n")
if let foundIndex = habits.firstIndex(of: randomActivity) {
    print("Found \(randomActivity.name) at index: \(foundIndex)")
}
print(walking.description) // ๐Ÿ‘ˆ๐Ÿผ Notice the hash
print(running.description) // ๐Ÿ‘ˆ๐Ÿผ Notice the hash
print(coding.description ) // ๐Ÿ‘ˆ๐Ÿผ Notice the hash

randomActivity.completions += 1 // Update the completions for the random selection
print("\nAfter updating the count, notice the hash is different?!")
print("Random is: " + randomActivity.description) // ๐Ÿ‘ˆ๐Ÿผ Here! Notice the hash is different
if let foundIndex = habits.firstIndex(of: randomActivity) {
    print("Found \(randomActivity.name) at index: \(foundIndex)")
} else {
    print("No index for modified activity.") // ๐Ÿ‘ˆ๐Ÿผ This is printed because the new hash does not exist in your collection.
}

Keep Coding

1      

Thank you so much Obelix!!! I really appreciate your taking the time to show me this through all that code, I ran it and it showed me exactly where i was going wrong.

Also going back to my code I added a printout which showed me how in the wrong sequence, the index result is indeed coming back as nil, and how my nil coalesce handling (index ?? 0) explains why my first item on the list was getting 'duplicated' over the first entry of the list

   

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.