Home > Net >  SwiftUI Combine Data Flow
SwiftUI Combine Data Flow

Time:06-23

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.

  • Related