6

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")
  }
}
Kevin Renskers
  • 5,156
  • 4
  • 47
  • 95
  • 3
    You're trying to access `self.foo` before `upperCased` is initialised. This have nothing to do with property wrappers. – Claus Jørgensen Oct 06 '19 at 12:40
  • Use `lazy var` for your properties instead of `let` – Mumtaz Hussain Oct 06 '19 at 13:29
  • 1
    @ClausJørgensen Well, as you can see in the `NoProblem` example, the same works fine when you don't use a property wrapper. – Kevin Renskers Oct 06 '19 at 13:35
  • 1
    @MumtazHussain That is not really possible. Sure, I can make `upperCased` a lazy var, but then it complains that `Lazy properties must have an initializer`. Which is not always an option in more real-world examples. – Kevin Renskers Oct 06 '19 at 13:37
  • @KevinRenskers you should just assign both value in the constructor instead. Your entire approach is what is wrong here. – Claus Jørgensen Oct 06 '19 at 15:19
  • @KevinRenskers anyway, you could probably do `self.upperCased = $foo.upperCased()` since you need to refer to the backing variable when using property wrappers. – Claus Jørgensen Oct 06 '19 at 15:25
  • 1
    "Your entire approach is what is wrong here" well thank you for that. Again, this is an extremely simplified example just to show the compiler error. Why is it complaining when using the property wrapper but not completing when not using a property wrapper - that is the question. Like I said, the same problem happens when trying to use property wrappers for observable properties or injected properties, or whatever else. – Kevin Renskers Oct 06 '19 at 15:30
  • The problem with an oversimplified approach is that your question can't be answered. The solution to whatever you're trying to do is most likely something else entirely (which does away with the error at the same time) – Claus Jørgensen Oct 06 '19 at 16:47
  • 1
    Added two more examples. – Kevin Renskers Oct 06 '19 at 20:32
  • Please give a simple self-contained example that someone can copy out of the browser and paste into Xcode and attempt to compile to get exactly the error you are getting. – matt Oct 06 '19 at 20:38
  • 1
    That is literally what the first example is for :) – Kevin Renskers Oct 06 '19 at 20:39

2 Answers2

3

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?

Community
  • 1
  • 1
Enricoza
  • 1,101
  • 6
  • 18
  • 1
    No, and in fact it's a real problem in SwiftUI as well, see my edited question for a new example. – Kevin Renskers Feb 14 '20 at 15:23
  • 1
    @KevinRenskers I've updated my answer with a better workaround, maybe you can use it for the time being. I think I'll also open an issue to the swift team if you haven't already. – Enricoza Feb 14 '20 at 15:30
  • 1
    @KevinRenskers re-edited actually. While I was writing to the swift forum I realized it's actually the intended behavior. I think the only way to do this is in the way I suggested in my edit. (I'm gonna use this method anyway) – Enricoza Feb 14 '20 at 15:46
  • Great workaround using the underscored variable's wrappedValue. – Kevin Renskers Feb 14 '20 at 16:57
0

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.

matt
  • 515,959
  • 87
  • 875
  • 1,141