@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(...)

4 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(...)
  ...
}

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
Terms of Service

Privacy Policy

Cookie Policy