Pradyot
Pradyot

Reputation: 3059

Passing environment object between non-view classes in SwiftUI

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?

Upvotes: 10

Views: 4435

Answers (4)

Patrick
Patrick

Reputation: 2373

@Finley's answer leads to an optional property.
@Jason Bobier's answer requires an additional class and still leads to an optional property.

The SwiftUI approach is to inject your StateObject as an EnvironmentObject which avoids the problems with the previous answers:

struct ParentView: View {
  @EnvironmentObject var session: Session
  var body: some View {
    CreditDetailsView()
      .environmentObject(TransactionsModel(token: session.token))
  }
}

struct CreditDetailsView: View {
  @EnvironmentObject var session: Session
  @EnvironmentObject var transactionsModel: TransactionsModel
}

Upvotes: 0

Jason Bobier
Jason Bobier

Reputation: 412

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

Upvotes: 1

Kyokook Hwang
Kyokook Hwang

Reputation: 2762

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.

Upvotes: -1

Finley
Finley

Reputation: 176

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.

Upvotes: 4

Related Questions