1

Is there a more natural way (now that it's 2022) of initializing some internal variables that depend on an @EnvironmentObject (or other @ObservedObject) within a view's init() function?

For example, the following shows what I'm trying to do (commented out) versus what works. Unfortunately the "what works" code is considerably more unwieldly (bulky, repetitive). Instead of just using a diary var I have to sprinkle my code with try? log.readDiary(for: state.now) or wrap it all in a subviews. Wondering what the best practice is.

struct NutritionView: View {
   let log: LogProvider
   @ObservedObject private var state: StateService

//   @StateObject private var diary: DiaryReader? // init depends on using state (above)

   init(log: LogProvider) {
//      self.diary = try? log.readDiary(for: state.now) // would like to init here
        // unfortunately `self.state` not available inside init()
   }
   
   var body: some View {
//    let remaining = remainingCalories(diary: diary, goals: goals) // and use `diary` here
      let remaining = remainingCalories(diary: try? log.readDiary(for: state.now), goals: goals)
      
      VStack {
         ...
      }
   }
}

There's a related post here: Swiftui - How do I initialize an observedObject using an environmentobject as a parameter?, but I'd rather not create internal subviews to accomplish what seems to be a simple initialization. That just seems... wasteful, repetitive, unwieldly. Hoping the API has evolved a bit since then.

Zaphod
  • 1,387
  • 2
  • 17
  • 33
  • 1
    Use a computed property, e.g. [here](https://stackoverflow.com/a/59411527/9607863) – George Feb 19 '22 at 19:53
  • 1
    Or you could pull the logic out of the view and put it in a ViewModel. Then send the view an intialized ViewModel. – Yrb Feb 19 '22 at 19:59
  • Thanks for the suggestions. First (using computing prop) won't work. The example I posted is simplified; the actual implementation is expecting an `@ObservedObject` which can't be put into a computed property as far as I know. I'll update the code to reflect this. Second option... well yes – but that's a different flavor of complexity. This is a data-driven view right now, so there's no view model. I can add one but... wish it wasn't necessary. – Zaphod Feb 19 '22 at 20:04
  • Bottom line - it sounds like simply using an `@EnvironmentObject` (or other similar type) in an `init()` is not possible. Which is what I was driving at – before going in another direction, wanted to confirm that. – Zaphod Feb 19 '22 at 20:07
  • you could either initialize `.onAppear` or pass the `@EnvironmentObject`in from the parent view – ChrisR Feb 19 '22 at 21:03
  • @Zaphod The environment exists at the time the `body` is rendered, so you cannot access `EnvironmentObject`s in the `init`. – Patrick Wynne Feb 19 '22 at 21:41

1 Answers1

0

There is nothing wasteful about creating multiple View data structs which are super fast value types. We are supposed to create many small View structs that only have a small number properties used by the body. SwiftUI is recomputing these constantly and using the result of the diff to update the UILabels on the screen.

In any case, something appears to have gone wrong with your design. EnvironmentObject isn't for state, it is used for data model which is an ObservableObject class, so a reference type so its lifetime outlasts the views. Inside that we are supposed to model our data using value types, e.g. structs. We pass these into Views as lets or use @Binding for write access. We can also have funcs in our model object that can do some work and then mutate the value types (usually looking it up by ID) which results in SwiftUI detecting the change and recomputing all the View bodys that depend on the data.

malhal
  • 26,330
  • 7
  • 115
  • 133
  • Thanks for that. Quite right, that should have been `@ObservedObject`, c/p error I suppose. Appreciate the note about creating views. I do realize they are lightweight... it's more the repeated boilerplate, versus efficiency. I imagine it would be easy to come up with a simple generic that eliminates the boilerplate through, and keeps the code clean... and easy to read. – Zaphod Feb 22 '22 at 19:15