Swift 5.1 @propertyWrapper - 'self' used in property access before all stored properties are initialized

Viewed 3446

I am trying to use Swift 5.1 property wrappers but every time I think I have a cool use case for it, I eventually hit the problem where I can't use them inside of my View Model's initializer.

Take this extremely simple example.

class NoProblem {
  var foo = "ABC"
  let upperCased: String

  init(dependencies: AppDependencies) {
    self.upperCased = foo.uppercased()
  }
}
@propertyWrapper
struct Box<Value> {
  private var box: Value

  init(wrappedValue: Value) {
    box = wrappedValue
  }

  var wrappedValue: Value {
    get { box }
    set { box = newValue }
  }
}

class OhNoes {
  @Box var foo = "ABC"
  let upperCased: String

  init(dependencies: AppDependencies) {
    self.upperCased = foo.uppercased()
  }
}

In NoProblem, everything works as expected. However in OhNoes I get this error: 'self' used in property access 'foo' before all stored properties are initialized.

Of course this is an extremely simplified example, but I get the same problem when doing an @Property wrapper for observable properties, or an @Injected wrapper like in this article, etc.

And no, sadly making it a lay property won't work either: Property 'foo' with a wrapper cannot also be lazy.


This is also a pretty big problem in SwiftUI, see this example:

class AppStore: ObservableObject {
  let foo = "foo"
}

struct ContentView: View {
  @EnvironmentObject private var store: AppStore
  private let foo: String

  init() {
    foo = store.foo // error: 'self' used before all stored properties are initialized
  }

  var body: some View {
    Text("Hello world")
  }
}
2 Answers

EDIT:

Actually a better workaround would be to directly use _foo.wrappedValue.uppercased() instead of foo.uppercased().

This solves also the other issue with the double initialization.

Thinking deeper about this, that's definitely the intended behavior.

If I understand it correctly, in OhNoes, foo is just short for:

var foo: String {
  get {
    return self._foo.wrappedValue
  }
  set {
    self._foo.wrappedValue = newValue
  }
}

So there is no way this could work in any other way.


I was facing the same issue you had and I actually think that it is some sort of bug/unwanted behavior.

Anyway the best I could go out with is this:

@propertyWrapper
struct Box<Value> {
  private var box: Value

  init(wrappedValue: Value) {
    box = wrappedValue
  }

  var wrappedValue: Value {
    get { box }
    set { box = newValue }
  }
}

class OhNoes {
  @Box var foo : String
  let upperCased: String

  init() {
    let box = Box(wrappedValue: "ABC")
    _foo = box
    self.upperCased = box.wrappedValue.uppercased()
  }
}

That is quite good (I mean, it works with no side effects, but is ugly).

The problem of this solution is that it doesn't really work (without side effects) in case your property wrapper has an empty initializer init() or if the wrappedValue is Optional.

If, for example, you try with the code below, you will realize that Box is initialized twice: once at the definition of the member variable and once in the OhNoes' init, and will replace the former.


@propertyWrapper
struct Box<Value> {
  private var box: Value?

  init(wrappedValue: Value?) { // Actually called twice in this case
    box = wrappedValue 
  }

  var wrappedValue: Value? {
    get { box }
    set { box = newValue }
  }
}

class OhNoes {
  @Box var foo : String?
  let upperCased: String?

  init() {
    let box = Box(wrappedValue: "ABC")
    _foo = box
    self.upperCased = box.wrappedValue?.uppercased()
  }
}

I think this is definitely something we shouldn't have, (or at least we should be able to opt out of this behavior). Anyway I think it's related to what they say in this pitch:

When a property wrapper type has a no-parameter init(), properties that use that wrapper type will be implicitly initialized via init().

PS: did you find some other way of doing this?

The most frictionless workaround is to make upperCased a var instead of a let. Okay, that might not be desirable, but at least it means you can keep all of your code and use the resulting OhNoes instance immediately:

struct AppDependencies {}
@propertyWrapper struct Box<T> {
    private var boxed: T
    init(wrappedValue: T) {
        boxed = wrappedValue
    }
    var wrappedValue: T {
        get { boxed }
        set { boxed = newValue }
    }
}
class OhNoes {
    @Box var foo = "abc"
    var upperCased: String = "" // this is the only real change
    init(dependencies: AppDependencies) {
        self.upperCased = foo.uppercased()
    }
}

If you really don't like that, then refer directly to _foo.wrappedValue as suggested by the other answer.

Related