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

Day 47 - NavigationLink returning on increment/decrement. Completion count not saving

Forums > 100 Days of SwiftUI

I have the NavigationLink load a the activity details view. The view loads fine, but as soon as I use the stepper to increment or decrement the completion count the details view exits and I'm back at the main ContentView. Furthermore, the completion count does not save.

Any ideas as to what I'm missing here?

This is ContentView.swift:

import SwiftUI

struct ContentView: View {
    @StateObject var activities = Activities()
    @State private var showingAddActivity = false

    var body: some View {
        NavigationView {
            List {
                ForEach(activities.activityList) { activity in
                    NavigationLink {
                        ActivityDetailView(activity: activity, activities: activities)
                    } label: {
                        Text(activity.name)
                    }
                }
            }
            .navigationTitle("Habits")
            .toolbar {
                Button {
                    showingAddActivity = true
                }
            label: {
                Image(systemName: "plus")
            }
            }
        }
        .sheet(isPresented: $showingAddActivity) {
            AddActivityView(activities: activities)
        }
    }
}

And this is ActivityDetailView.swift:

import SwiftUI

struct ActivityDetailView: View {
    @State private var timesCompleted = 0
    let activity: Activity

    @ObservedObject var activities: Activities

    var body: some View {
        NavigationView {
            Form {
                Text("Activity: \(activity.name)")
                Text("Description: \(activity.description)")

                Stepper {
                    Text("Times Completed: \(timesCompleted)")
                } onIncrement: {
                    timesCompleted += 1
                    updateTimesCompleted()
                } onDecrement: {
                    timesCompleted -= 1
                    updateTimesCompleted()
                }
            }
            .navigationTitle("Activity Details")
        }
    }

    func updateTimesCompleted() {
        let newActivity = Activity(name: activity.name, description: activity.description, timesCompleted: timesCompleted)

        let index = activities.activityList.firstIndex(of: activity)
        activities.activityList[index!] = newActivity
    }
}

struct ActivityDetailView_Previews: PreviewProvider {
    static var previews: some View {
        ActivityDetailView(activity: Activity(name: "Test Activity", description: "Description", timesCompleted: 3), activities: Activities())
    }
}

2      

After some experimentation the problem seems to be that I can't overwrite or remove any elements from activities.activityList. I can append, but if I try to assign over top of an element or use .Remove(), the view silently crashes and reverts to the main ContentView.

Not sure how to fix it.

2      

Can you show the rest of your code? Like, the Activites class itself and the AddActivityView?

2      

Here's the Activities class:

class Activities: ObservableObject {
@Published var activityList = [Activity]() {
    didSet {
        if let encoded = try? JSONEncoder().encode(activityList) {
            UserDefaults.standard.set(encoded, forKey: "activityList")
        }
    }
}

init() {
    if let savedList = UserDefaults.standard.data(forKey: "activityList") {
        if let decodedList = try? JSONDecoder().decode([Activity].self, from: savedList) {
            activityList = decodedList
            return
        }
    }
    activityList = []
}

init(activityList: [Activity]) {
    self.activityList = activityList
}

subscript(index: Int) -> Activity {
    get {
        assert(index < activityList.count, "Index out of range")
        return activityList[index]
    }

    set {
        assert(index < activityList.count, "Index out of range")
        activityList[index] = newValue
    }
}
}

AddActivityView works, but that's consistent with what's happening in ActivityDetailView since append is all that's used in AddActivityView.

Here it is:

struct AddActivityView: View {
    @State private var activityName = ""
    @State private var activityDescription = ""

    @ObservedObject var activities: Activities

    @Environment(\.dismiss) var dismiss

    var body: some View {
        NavigationView {
            Form {
                VStack {
                    TextField("Name", text: $activityName)
                    TextField("Description", text: $activityDescription)
                }
            }
            .navigationTitle("Add Activity")
            .toolbar {
                Button("Save") {
                    let activity = Activity(name: activityName, description: activityDescription, timesCompleted: 0)
                    activities.activityList.append(activity)

                    dismiss()
                }
            }
        }
    }
}

struct AddActivityView_Previews: PreviewProvider {
    static var previews: some View {
        AddActivityView(activities: Activities(activityList: [Activity(name: "Example", description: "Ex Desc", timesCompleted: 0)]))
    }
}

2      

Got some help in r/iosprogramming. I think the hints for the challenge are perhaps out of date and misleading based on this article which explained how to do it: https://www.swiftbysundell.com/articles/bindable-swiftui-list-elements/

You don't even have to pass the class holding the collection to the view or mess around with indices.

Here's the solution:

struct ContentView: View {
    @StateObject var activities = Activities()
    @State private var showingAddActivity = false

    var body: some View {
        NavigationView {
            List {
                ForEach($activities.activityList) { $activity in  //This line is key
                    NavigationLink {
                        ActivityDetailView(activity: $activity)  //So is this
                    } label: {
                        Text(activity.name)
                    }
                }
            }
            .navigationTitle("Habits")
            .toolbar {
                Button {
                    showingAddActivity = true
                }
            label: {
                Image(systemName: "plus")
            }
            }
        }
        .sheet(isPresented: $showingAddActivity) {
            AddActivityView(activities: activities)
        }
    }
}
struct ActivityDetailView: View {
    @Binding var activity: Activity //Now with @Binding we can modify the array element directly

    var body: some View {
        NavigationView {
            Form {
                Text("Activity: \(activity.name)")
                Text("Description: \(activity.description)")

                Stepper {
                    Text("Times Completed: \(activity.timesCompleted)")
                } onIncrement: {
                    activity.timesCompleted += 1
                } onDecrement: {
                    if activity.timesCompleted > 0 {
                        activity.timesCompleted -= 1
                    }
                }
            }
            .navigationTitle("Activity Details")
        }
    }
}

2      

@sly3  

This does work for me after building but Im curious what youre passing into the preview provider below in your detail view. In the moonshot lessons, we passed in a default key of 'armstrong' into the astronaut view to satisfy the call to the previewer.

Similarly, I try passing some default data from my habit struct (HabitItem) where we define the properties for a habit (Activity in your case) but am met with: Cannot convert value of type 'HabitItem' to expected argument type 'Binding<HabitItem>'

struct HabitView_Previews: PreviewProvider {
    static let habits = HabitItem(name: "test", description: "asd", amount: 1)

    static var previews: some View {
        HabitView(habit: habits)  // error here
    }
}

So far Iver tried using @Binding static var habits and HabitView(habit: $habits) but that causes several errors related to generics. So just wondering how you got it to preview. Thanks!

2      

@EP2nd  

This does work for me after building but Im curious what youre passing into the preview provider below in your detail view.

You can satisfy it with a constant binding. It has fixed values, it can't be changed in the UI, but it comes in handy for previews.

struct HabitView_Previews: PreviewProvider {
    static var previews: some View {
        HabitView(habit: .constant(HabitItem(name: "test", description: "asd", amount: 1)))
    }
}

2      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

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.