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

SOLVED: Day 47 - Question about modifying data in an ObservableObject

Forums > 100 Days of SwiftUI

Hi everyone. I recently finished the project on Day 47, but I'm still failing to understand how SwiftUI treats data between views. Some explanation would be appreciated.

In my ActivityDetail view, I was trying to update the timesCompleted property of the Activity struct directly, by incrementing the count with a button and then setting the new value directly with this function. The code checked out, but it wouldn't let me change the timesCompleted property because it stated that "activity" was a constant (even though I declared it with var).

Essentially, I tried to modify one property in the struct that the class contained and it didn't work.

func updateTimes(count: Int, name: String) {
        for activity in activities {
            if activity.activityName == name {
                activity.timesCompleted = count
            }
        }
    }

Eventually I found that I needed to swap the Activity item in the Activities array completely, using this function:

    func updateActivity() {
        if let position = activities.activities.firstIndex(where: {$0.id == activity.id}) {
            activities.activities[position] = activity
        }
    }

My question is: why isn't it possible to modify only the timesCompleted property? Why does Swift require a new instance of Activity to replace the old one?

Full code here of code that didn't work (error message cannot assign to property: 'activity' is a 'let' constant:

struct Activity: Identifiable, Codable {
    let id = UUID()
    let activityName: String
    let activityDetails: String
    let measureUnit: String
    var timesCompleted: Int

    mutating func incrementTimes() {
        self.timesCompleted += 1
    }
}

class Activities: ObservableObject {
    @Published var activities = [Activity]() {
        didSet {
            let encoder = JSONEncoder()

            if let encoded = try? encoder.encode(activities) {
                UserDefaults.standard.set(encoded, forKey: "Activities")
            }
        }
    }

    init() {
        if let items = UserDefaults.standard.data(forKey: "Activities") {
            let decoder = JSONDecoder()

            if let decoded = try? decoder.decode([Activity].self, from: items) {
                self.activities = decoded
                return
            }
        }
    }

    func updateTimes(count: Int, name: String) {
        for activity in activities {
            if activity.activityName == name {
                activity.timesCompleted = count
            }
        }
    }

}

struct ContentView: View {

    @ObservedObject var activities = Activities()
    @State private var showingAddActivity = false

    var body: some View {
        NavigationView {
            List(activities.activities) { activity in
                NavigationLink(destination: ActivityDetails(activity: activity, activities: self.activities)) {
                    VStack(alignment: .leading) {
                        Text(activity.activityName)
                        Text("Times Completed: \(String(activity.timesCompleted))")
                    }

                }

            }
                //.onDelete(perform: self.removeItems)
                .navigationBarTitle("Be Better")
                .navigationBarItems(leading: EditButton(), trailing: Button(action: {
                    self.showingAddActivity = true
                }) {
                    Image(systemName: "plus")
                })
                .sheet(isPresented: $showingAddActivity) {
                    AddActivity(activities: self.activities)
            }
        }
    }
    func removeItems(at offsets: IndexSet) {
        activities.activities.remove(atOffsets: offsets)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct ActivityDetails: View {

    @Environment(\.presentationMode) var presentationMode
    @State var activity: Activity
    @ObservedObject var activities: Activities

    var body: some View {
        NavigationView {
            VStack {
                Text(activity.activityName)
                    .font(.title)
                    .padding()
                HStack {
                    Button(action: {
                        self.activity.incrementTimes()
                    }) {
                        Image(systemName: "arrow.left")
                    }
                    Text(String(activity.timesCompleted))
                    Button(action: {
                        self.activity.incrementTimes()
                        self.activities.updateTimes(count: self.activity.timesCompleted, name: self.activity.activityName)
                    }) {
                        Image(systemName: "arrow.right")
                    }
                }
                .padding()
                Text(activity.activityDetails)
                .padding()
                Spacer()
            }

            navigationBarTitle("Activity information")
            .navigationBarTitle("Add a new activity")
            .navigationBarItems(leading: Button("Back") {
                self.presentationMode.wrappedValue.dismiss()
            })

        }
    }

2      

hi,

when you write this code:

    func updateTimes(count: Int, name: String) {
        for activity in activities {
            if activity.activityName == name {
                activity.timesCompleted = count
            }
        }
    }

you need to recognize that the activity variable in the for activity in activites loop is not a reference to an item in the activities array, but a copy of an item in the activities array. it's value semantics in action. and it's treated as a let constant, as the error message indicates. (On the other hand, if Activity were a class, the situation would be different.)

you can, of course, update individual items of an array, so replacement does work.

hope that helps,

DMG

3      

Thank you very much for the explanation! I understand now how my original code wouldn't have worked.

2      

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!

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.