TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

If you can't write to @ObservedObject, how do you update an object across multiple views?

Forums > 100 Days of SwiftUI

If you instantiate a class that implements ObservableObject and mark it as @StateObject in your parent view (eg. ContentView) and then pass it in to a child view through the initializer where it is marked with @ObservedObject, you can then read its properties in the child view. However, what if you want to update the object in the child view?

If I take my object in the child view and try to update it like this:

myObject.myArray[0] = anArrayElement

Xcode won't complain, but it also doesn't work. Neither does this:

myObject.myArray.remove(at: 0)

Oddly append works:

myObject.myArray.append(newElement)

So how do we share an object across views with full read/write access everywhere?

2      

@Bnerd  

Does your class have any method to "save" the changes that you are trying to pass to the @Published var ?

2      

@Bnerd what would that look like? It's a class, so a reference type. The object is shared across views via the @StateObject/@ObervedObject wrappers.

If I have:

myObject.myIntProperty = 1

That's all there is to do. The assignment is the "save". What extra step are you alluding to?

2      

@Legion , is this what you are trying to achieve, change @Published from using @ObservedValue ?

class Player: ObservableObject {
    @Published var name = "Taylor"
    @Published var age = 26
}

struct PlayerNameView: View {
    @ObservedObject var player: Player
    var body: some View {
        Text("Hello, \(player.name)")
            .onTapGesture {
                changePlayer()
            }
    }

    func changePlayer() {
        var newName = player.name = "Gomez"
        print("\(newName)")
    }
}

struct ContentView: View {
    @StateObject var player = Player()
    var body: some View {

        NavigationView {
            NavigationLink(destination: PlayerNameView(player: player)) {
                       Text("Show detail view")
                   }
               }
    }

}

2      

This is for the Day 47 challenge. I'm trying to update the timesCompleted property of one of the elements in the array of activities/habits.

For some reason I can append new elements in the child view, but I can't remove elements or replace them. Xcode provides no error messages.

2      

@Legion , you have to post the code and tell where the issue is, i will give it a try

2      

Activity Struct:

struct Activity : Codable, Identifiable, Equatable {
var id = UUID()
var name: String
var description: String
var timesCompleted: Int
}

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
    }
}
}

ContentView:

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
                let _ = print("add activity")
            }
        label: {
            Image(systemName: "plus")
        }
        }
    }
    .sheet(isPresented: $showingAddActivity) {
        AddActivityView(activities: activities)
    }
}
}

ActivityDetailView - This is where the problem is. As soon as I try to do anything other than append to activities.activityList this view crashes and returns to ContentView. You can see in updateTimesCompleted() a bunch of my different attempts which all fail.

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: {
                if timesCompleted > 0 {
                    timesCompleted -= 1
                    updateTimesCompleted()
                }
            }
        }
        .navigationTitle("Activity Details")
    }
}

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

    let _ = print("count: \(activities.activityList.count)")

    let index = activities.activityList.firstIndex(of: activity)
    let _ = print(index ?? -666)
    if let index = index {
        activities.activityList[index] = Activity(name: activity.name, description: activity.description, timesCompleted: timesCompleted)

        //activities.activityList.swapAt(index, activities.activityList.count - 1)
        //activities.activityList[index].incrementTimesCompleted()
        //activities.activityList.append(newActivity)
        //activities.activityList.remove(at: index)
        //activities.activityList.removeAll()
        //activities.activityList.append(newActivity)
    }
}
}

AddActivityView - This works fine, but this is consistent with what I'm experiencing in ActivityDetailView as it only uses append and append is the only thing that works.

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      

I'm copying this from another thread so the solution is present here also.

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")
        }
    }
}

3      

Hacking with Swift is sponsored by String Catalog.

SPONSORED Get accurate app localizations in minutes using AI. Choose your languages & receive translations for 40+ markets!

Localize My App

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.