@State messing with initializer flow

I'm trying to use initializer injection for my custom SwiftUI view:

struct SignInView {
  @State private var viewModel: SignInViewModel
}

extension SignInView: View { ... }

The private modifier on the model forces me to explicitly expose the initializer:

init(viewModel: SignInViewModel) {
  self.viewModel = viewModel
}

I get the error self used before all stored properties are initialized. If I delete @State, it'll work fine. Anyone know what's going on here?

init(viewModel: SignInViewModel) {
  self.viewModel = viewModel
  $$self.viewModel = State<SignInViewModel>(initialValue: viewModel) // It should be this. @State use delegateValue, so it's $$self.viewModel. Unfortunately it dose not work.
}

Although that will compile, @State variables in SwiftUI should not be initialized from data you pass down through the initializer; since the model is maintained outside of the view, there is no guarantee that the value will really be used. The correct thing to do is to set your initial state values inline:

@State private var viewModel = SignInViewModel(...)

5 Likes

A requirement to set it inline prevents dependency injection by constructor and impacts testability. I want to be able to inject a mock state in my view tests.

2 Likes

An @ObjectBinding might work better then; you can then bind to any class conforming to the BindableObject protocol, and provide a mock implementation for your tests. State is primarily intended for small-scale UI state.

For dependency injection, it might be better to do it at the type level, something like this:

struct MyView<Model: MyModelProtocol>: View {
  @state private var injectedModel = Model(...)
  ...
}
2 Likes

For more context, here's how I am conforming to the View:

var body: some View {
    VStack {
        TextField($viewModel.email, placeholder: Text("Email"))
        SecureField($viewModel.password, placeholder: Text("Password"))
        Button(action: viewModel.signInTapped) { Text("Sign In") }
    }
}

@State is allowing me to bind the TextField changes and Button actions to my view model. Here's what my view model looks like:

final class SignInViewModel {
  var email = ""
  var password = ""
  func signInTapped() { print("sign in tapped") }
}

It's working nicely - typing in the text fields would update the stored properties in the view model, and the button action maps perfectly to the signInTapped function.

Came up with another way of doing this:

protocol SignInModel {
  var email: State<String> { get }
  var password: State<String> { get }
  
  func signInTapped()
}

The view can depend on any type that conforms to this protocol. In the view's body:

var body: some View {
  VStack {
    TextField(viewModel.email.binding)
    SecureField(viewModel.password.binding)
  }
}

@Joe_Groff how does the above look?

Nvm. Above code compiles, but I get a runtime error:

Accessing State<String> outside View.body

Updated to:

protocol SignInViewModel {
    var email: Binding<String> { get }
    var password: Binding<String> { get }
    
    func signInTapped()
}

I'm running into the same issue. Anyone have any thoughts on how to fix this? In particular I'm trying to do a binding at the tail end of a publisher:
// .assign(to: \StoryListView.stories, on: listView).
I get the error:

Accessing State ... outside View.body

hi @Joe_Groff,
Is there a chance that you can provide a simple functional code snippet that demonstrates initialising a custom structure with a State set from outside? This is the first stumbling block as there is not a lot of Good basic documentation on how to use and understand SwiftUI and Bindings with State or Binding, etc.

Thanks,

You shouldn't be doing that. You can use a Binding to provide access to state that isn't local to your view.

1 Like

Quick update on this thread. I came up with a satisfying pattern that allows views to depend on an abstract definition of the view model that allows you to inject from the outside:

protocol SignInModel: ObservableObject {
  var email: String { get set }
  var password: String { get set }
  
  func signIn() -> Future<Void, Error>
}

struct SignInView<Model: SignInModel>: View {
  @ObservedObject private(set) var model: Model
}

Everything defined in the protocol has the ability to signal the dependents to re-render their view, assuming that's what the view subscribes to.

Terms of Service

Privacy Policy

Cookie Policy