SwiftUI.State: .init(wrappedValue:) vs. .init(initialValue:): what's the difference?

The doc:

init(wrappedValue:)
Creates the state with an initial wrapped value.

init(initialValue:)
Creates the state with an initial value.

why these two init's, they seem to be the same, but there must be some reason for this?

The documentation for init(wrappedValue:) indicates that you should never call it. It is used by the declaration of state property:

Discussion

You don’t call this initializer directly. Instead, declare a property with the @State attribute, and provide an initial value; for example, @State private var isPlaying: Bool = false .

2 Likes

I saw the "discussion". I still like to know what's the difference between the two. Despite "don't call this initializer directly", you can call it anyway and I see code that calls this.

Anyway, init(wrappedValue:) "don't call directly", init(initialValue:) does not say this. There must be some difference? What is it?

init(initialValue:) was the first name chosen when the original Property Wrappers proposal was released. It was changed to init(wrappedValue:) but the original one was left for backwards compatibility.

7 Likes

That's my initial thought, but doesn't the SE-0258 review ends a good while before the announcement of SwiftUI, and most definitely long before the beta test ends?

1 Like

Yeah, SwiftUI could have removed the original one before shipping. But now that it's shipped, it's ABI and cannot be removed.

1 Like

Should init(initialValue:) be "deprecated"?

And the discussion "don't call directly" is not good because you need to call it directly sometimes.

I don't know where you got this from as this isn't correct. You can and must use it if you need to initialize the property wrapper by hand. If your PW has an init(initialValue:) overload, you should just ignore it and prefer init(wrappedValue:), that's all.

In Combine framework, they had this issue for a whole year. Basically they shipped with only init(initialValue:) but added init(wrappedValue:) a year later and also back ported it using @_alwaysEmitIntoClient. :man_facepalming:

According to Swift Programming Language Guide, Swift can use init(wrappedValue:) to initialize a property wrapper when you apply the wrapper to a property. For example, if you created a property wrapper named SmallNumber, you can do this in code:

    struct BigRectangle {
        @SmallNumber var width: Int = 1
        @SmallNumber var height: Int = 1 
    }

In the code above, when you write = 1 on a property with a wrapper, that’s translated into a call to the init(wrappedValue:) initializer by Swift.

According to the documentation, you should use
init(initialValue:) to create a state with an initial value.

1 Like

Apple Documentation: Apple Developer Documentation

That is an API contract not a general PW thing. State has some special framework specific behavior which they communicate through documentation and WWDC videos. If you use the init to inject value from a parent, you will tap into bugs.

The following example is totally valid:

@propertyWrapper
struct Wrapper {
  let wrappedValue: Int // `init(wrappedValue:)` synthesized
}

struct T {
  @Wrapper
  var a: Int = 0

  @Wrapper
  var b: Int

  init(b: Int) {
    self._b = Wrapper(wrappedValue: b * 2)
  } 
}

Long story short, ignore init(initialValue:) on property wrapper types that has been already shipped. It's likely you never will see such an init on future property wrappers unless it provides the user some totally different semantics.

1 Like

Except we have to use direct initialization if we want to provide an initial value. Initializing just the wrapped value doesn't trigger the wrapper's effects. Is there an alternate solution here?

I don‘t understand the question, do you have a small example of what you want to achieve and how it‘s not possible/working?!

Say you have a View with a Picker inside and want to set the initial selection from outside the view. Just passing the selection and setting it on the state property doesn’t work, but initializing the State wrapper directly does.

I don't understand what you're saying...show sample code would clear things for me.

That‘s an anti pattern anyway because of the re-injection for State. You should create a model from a different place and pass it down. Again, that‘s not a property wrapper issue.

1 Like

Whatever model I pass down would have to be translated into a binding for the Picker, so I'm not sure what you're suggesting here.

struct MyView: View {
  class Model: ObservableObject {
    @Published
    var selection: Int
    init(selection: Int) {
      self.selection = selection
    }
  }
  
  @ObservedObject
  var model: Model

  var body: some View {
    Picker(selection: $model.selection, ...)
  }
}

Initialize the model from the parent for example using StateObject to persist it, pass it down and use the property wrappers projected value to obtain a binding.

1 Like

So to merely set the default value of a Picker we need to create a separate type, own an instance outside the view that cares about it, inject it into the view, and use that binding? Even with the new @StateObject, that doesn't seem right.

That‘s the way to do it if you really want to provide a different initial value than a constant default value for MyView.

1 Like