0

TL;DR

It seems that the ContentView below evaluates the body's if statement before init has run. Is there a race condition, or is my mental model out-of-order?

Kudos

A shout-out to Asperi, who provided the state-initializer equivalent that solves today's problem.

Code

Why does ContentView display "dummy is nil"? It seems something gets closed over before the initializer sets dummy. What is it about the second assignment that fixes things?

class Dummy {  }

struct ContentView: View {
    @State private var dummy : Dummy?

    init() {
        print("Init") // In either case, is printed before "Body"

        // Using this assignment, "dummy is nil" shows on screen.
        self.dummy = Dummy()

        // Using this, "dummy is non-nil" shows on screen.
        // From https://stackoverflow.com/questions/61650040/swiftui-initializer-apparent-circularity
        // self._dummy = State(initialValue: Dummy())
    }

    var body: some View {
        print("Body")
        return ZStack {
            if dummy == nil {              // Decision seems to be taken
                Text("dummy is nil"    )   // before init() has finished.
            } else { 
                Text("dummy is non-nil") 
            }
        }
    }
}
Andrew Duncan
  • 3,553
  • 4
  • 28
  • 55
  • It is just a nature of `@State` property wrapper... accept it :) – Asperi May 07 '20 at 15:39
  • I don't have much choice... but still it is weird. This came up in a commercial app, not just a toy example. I tried [long list of things] and just this morning saw your answer (elsewhere) and thought "that shouldn't make a difference... but I'll try it." – Andrew Duncan May 07 '20 at 15:45
  • 1
    It is not `before init() has finished`. It is just that `self.dummy = Dummy()` does nothing, skipped. If you need initialise State then you have to do it either directly in property declaration or in init via `self._dummy`, or change when view already alive, say in `.onAppear` and later. – Asperi May 07 '20 at 15:50
  • I don't quite understand what you mean by "does nothing". Is the compiler entitled to optimize out a state assignment? In my case, the dummy was an instance of `PlayerViewModel` that you so helpfully explained two months ago. I did try setting it in `.onAppear` but it was already non-nil at that point. It just didn't show up on the screen, because the `if` clause in the declarative layout had excluded it. – Andrew Duncan May 07 '20 at 22:47
  • I'm seeing the same problem (with the same fix) in `@State` vars for images that are set in initializers. I.e. on first appearance of the `View`, there is no image. It really looks to me like a race-condition in the dependency management. – Andrew Duncan May 09 '20 at 23:07

1 Answers1

2

Consensus is that it's a feature that looks like a bug.

Helpful discussion in Swift Forum. Also here. Highlights (edited for clarity) include:

Good reasons not to do this in the first place:

  • You're not suppose to mutate during View init since that'd be in the body call of the parent view

  • @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.

  • Don't try overriding @State's initial value during init. It will only work once during very first view creation (important: not value initialization) and only if that view in the view tree gets re-created / replaced with a different view of the same type but internally different id.

The "it's a misfeature" position (close to mine):

  • This is due to an artificial limitation in the compiler's implementation of property wrappers. The workaround is to initialize the backing storage directly via _value

Explanations of how and why:

  • The value in @State will always be initialized with the value you pass in init, this is simple Swift. However before next body call SwiftUI will call update method and reinject a value into it if there was a previous value which will override your value from init.

  • This is NOT a bug! :man_facepalming: It works as expected/advertised. ... @State property wrapper adds one possible way of mutation to the view, which also is meant to be private to the view, not initialized from parent, etc.

It's pilot error (probably true):

  • There is NO problem here except people misunderstanding what State is build for.
Andrew Duncan
  • 3,553
  • 4
  • 28
  • 55