Been away from the swift-ing for a good 3 years now. Getting back into it now and trying to learn Combine and SwiftUI.
Making a test Workout app. Add an exercise, record reps and weights for 3 sets. Save data.
I'm having issues moving some data around from views to data store. I think I'm confusing all the different property wrappers. Summary at the bottom after code.
Views:
struct ContentView: View {
@EnvironmentObject var store: ExerciseStore
var body: some View {
List {
ForEach(store.completedExercises) { exercise in
ExerciseView(exercise: exercise)
}
}
}
}
struct ExerciseView: View {
@ObservedObject var exercise: CompletedExercise
var body: some View {
VStack {
Text(exercise.exercise.name)
SetView(set: exercise.sets[0])
SetView(set: exercise.sets[1])
SetView(set: exercise.sets[2])
}
}
}
struct SetView: View {
@ObservedObject var set: ExerciseSet
var body: some View {
HStack {
TextField(
"Reps",
value: $set.reps,
formatter: NumberFormatter()
)
TextField(
"Weight",
value: $set.weight,
formatter: NumberFormatter()
)
}
}
}
Store:
class ExerciseStore: ObservableObject {
@Published var completedExercises: [CompletedExercise] = [CompletedExercise(Exercise())]
init() {
if let data = UserDefaults.standard.data(forKey: "CompletedExercise") {
if let decoded = try? JSONDecoder().decode([CompletedExercise].self, from: data) {
completedExercises = decoded
return
}
}
}
func save() {
if let encoded = try? JSONEncoder().encode(completedExercises) {
UserDefaults.standard.set(encoded, forKey: "CompletedExercise")
}
}
}
Models:
class CompletedExercise: Codable, Identifiable, ObservableObject {
var id: String
var exercise: Exercise
var sets: [ExerciseSet] = [
ExerciseSet(),
ExerciseSet(),
ExerciseSet()
]
init(exercise: Exercise) {
self.id = UUID().uuidString
self.exercise = exercise
}
}
struct Exercise: Codable, Identifiable {
var id: Int
var name: String
init() {
id = -1
name = "Bench Press"
}
}
class ExerciseSet: Codable, ObservableObject {
var reps: Int?
var weight: Int?
}
Thats more or less the current code.
I've added a bunch of print statements in the save function in ExerciseStore
to see what gets saved.
No matter what I've tried, I can't get the reps/weight via the SetView
text fields to persist through the ExerciseStore
and get saved.
I've played around with @Binding and such as well but can't get it working.
What am I missing/messing up with the new SwiftUI data flows.
CodePudding user response:
You're code is great, but the values in your ExerciseSet
& CompletedExercise
aren't marked with @Published
.
In order to access the ObservableObject
capabilities you need to publish
those values enabling your Views
to listen
, and your class
to bind
to the changes made.
Also substitute ObservedObject
with StateObject
, same case for EnvironmentObject
.
CodePudding user response:
There are a couple of things I note about your code. First you declare the store as an @EnvironmentObject
but you don't show the code where you set up the EnvironmentKey
and EnvironmentValues
to use it. It may be that you chose to leave that code out, but if you don't have it, it will be worth looking at. See the docs for EnvironmentKey
, I think it explains both. https://developer.apple.com/documentation/swiftui/environmentkey.
@EnvironmentObject
just declares that you want to take a property from the environment and observe it. You have to put the property in the environment (usually from a top-level view) using the environmentObject
View modifier (https://developer.apple.com/documentation/swiftui/image/environmentobject(_:)/). You don't show that code either. You may have put it on your app (or whichever view instantiates ContentView
).
Secondly you have @ObservableObjects
but no @Published
properties on those objects. @Published
is how Combine knows which properties you want notifications about.
Thirdly you use @ObservedObject
in a lot of your views. @ObservedObject
declares that "someone else is going to own an Observable object, and they are going to give it to this view at runtime and this view is going to watch it". At some point, however, someone needs to own the object. It will be the "source of truth" that gets shared using @ObservedObject
. The view that wants to own the object should declare that ownership using @StateObject
. It can then pass the observed object to children and the children will use @ObservedObject
to watch the object owned by the parent. If you have something that's not a observable object you want a view to own, you declare ownership using the @State
modifier. Then if you want to share that state to children and let them modify it, you will use a Binding
.
CodePudding user response:
It's best to just have one class which is the store and make all the other model types as structs. That way things will update properly. You can flatten the model by using the IDs to cross reference, e.g.
class ExerciseStore: ObservableObject {
@Published var exercises = [Excercise]
@Published var sets
@Published var completedIDs
You can see an example of this in the Fruta sample project where there is a favourite smoothies array that is IDs and not duplicates of the smoothie structs.