10

I understand that EnvironmentObject property wrapper can be used to pass around objects to views. I have a session object which I am passing around to my views. Now I have a requirement to pass this into one of my model classes (i.e., non-view). Ideally, this model (receiving the session object) is instantiated as a StateObject.

struct CreditDetailsView: View {
  @EnvironmentObject var session: Session
  @StateObject var transactionsModel = TransactionsModel(token: session.token)

The code above will not work (understandably) because:

cannot use instance member 'session' within property initializer; property initializers run before 'self' is available

Any suggestions on how I can pass in the session into TransactionsModel?

Paulo Mattos
  • 18,845
  • 10
  • 77
  • 85
Pradyot
  • 2,897
  • 7
  • 41
  • 58

3 Answers3

4

Try initializing the StateObject in an .onAppear() prop to a child view, like this:

struct CreditDetailsView: View {
  @EnvironmentObject var session: Session
  @StateObject var transactionsModel: TransactionModel?
  
  var body: some View {
    SomeChildView()
      .onAppear(perform: {
        transactionModel = TransactionModel(token: session.token)
      })
  }
}

This way, the variable is initialized when the view renders on the screen. It doesn't matter much which child view you add the onAppear prop to, as long as it is rendered as soon as the parent does.

aheze
  • 24,434
  • 8
  • 68
  • 125
Finley
  • 166
  • 1
  • 8
  • Thanks! In another situation I was able to pass in the token in the onAction part of button press. So I guess the approach Identified by you above as well as the button press are 2 examples of getting this to work. – Pradyot May 27 '21 at 04:31
  • No problem, hope it helps you :) – Finley May 27 '21 at 04:36
  • How would you pass a reference to the environment object itself rather than just to one of its properties (in this case the token property)? I have tried to do it, and I get a plethora of errors. – Soferio Jan 16 '22 at 08:20
1

The best way that I've found to do this (because you cannot have an optional StateObject) is:

struct CreditDetailsView: View {
  @EnvironmentObject var session: Session
  @StateObject var localModel = LocalModel()
  
  var body: some View {
    SomeChildView()
      .onAppear {
        localModel.transactionModel = TransactionModel(token: session.token)
      }
  }

  class LocalModel: ObservableObject {
    @Published transactionModel: TransactionModel?
  }
}
Jason Bobier
  • 332
  • 2
  • 6
-1

This is an incorrect answer. Please check out the chosen answer above.

You can access session object in init. In this case, transactionsModel should be done to be already initialized in any ways.

@EnvironmentObject var session: Session
@StateObject var transactionsModel = TransitionalModel(token: "")

init() {
    let token = self.session.token
    _transactionsModel = StateObject(wrappedValue: TransitionalModel(token: token))
}

Although it's out of the question, I am not sure if it's good way to pass something between them who look like being in different levels in the level of View.

Kyokook Hwang
  • 2,682
  • 21
  • 36
  • I haven't actually seen any code supplying init on a View. But to think of it there is nothing stopping you from doing it. thanks for pointing this out. I can just invoke a method on model and supply the token via init. – Pradyot May 27 '21 at 04:35
  • Regarding supplying something in `init`. it's common pattern as far as I know. When you need to pass some value to a view and wanna use it as an initial value of specific @State/@StateObject, it's usually used like that. That case usually uses a designated init in order to receive the value from outside though. – Kyokook Hwang May 27 '21 at 04:45
  • 1
    The environment is not available in a `View`'s `init` method. This will give you a fatal error that no ObservableObject of that type can be found. – Patrick Wynne May 27 '21 at 07:55
  • Oh. didn't test it. sorry for incorrect answer. I will put a comment on top of the answer. – Kyokook Hwang May 27 '21 at 08:57