Home > Mobile >  How @State variables in SwiftUI are modified and rendered in different Views?
How @State variables in SwiftUI are modified and rendered in different Views?

Time:07-26

I was playing with Swift Playground with the following code. What I was doing is to modify a @State variable in a Button action, and then show its current value in a full-screen sheet.

The problem is that, notice the code line I commented, without this line, the value displayed in the full-screen sheet will be still 1, with this line, the value will be 2 instead, which is what I expected it to be.

I want to know why should this happen. Why the Text matters.

import SwiftUI

struct ContentView: View {
    @State private var n = 1
    @State private var show = false
    
    var body: some View {
        VStack {
            // if I comment out this line, the value displayed in
            // full-screen cover view will be still 1.
            Text("n = \(n)")
            Button("Set n = 2") {
                n = 2
                show = true
            }
        }
        .fullScreenCover(isPresented: $show) {
            VStack {
                Text("n = \(n)")
                Button("Close") {
                    show = false
                    // UPDATE(1)
                    print("n in fullScreenCover is", n)
                }
            }
        }
    }
}

Playground Version: Version 4.1 (1676.15)

Update 1:

As Asperi answered, if n in fullScreenCover isn't captured because it's in different contexts (closure?), then why does the print line prints n in fullScreenCover is 2 when not put Text in body?

CodePudding user response:

The fullScreenCover (and similarly sheet) context is different context, created once and capturing current states. It is not visible for view's dynamic properties.

With put Text dependable on state into body just makes all body re-evaluated once state changed, thus re-creating fullScreenCover with current snapshot of context.

A possible solutions:

  1. to capture dependency explicitly, like
    .fullScreenCover(isPresented: $show) { [n] in // << here !!
        VStack {
            Text("n = \(n)")
  1. to make separated view and pass everything there as binding, because binding is actually a reference, so being captured it still remains bound to the same source of truth:

     .fullScreenCover(isPresented: $show) {
         FullScreenView(n: $n, show: $show)
     }
    

and view

struct FullScreenView: View {
    @Binding var n: Int
    @Binding var show: Bool
    var body: some View {
        VStack {
            Text("n = \(n)")
            Button("Close") {
                show = false
            }
        }
    }
}

both give:

demo

Tested with Xcode 13.4 / iOS 15.5

CodePudding user response:

For passing data into fullScreenCover we use a different version which is fullScreenCover(item:onDismiss:content:). There is an example at that link but in your case it would be:

struct FullscreenCoverTest: View {
    @State private var n = 1
    @State private var coverData: CoverData?
    
    var body: some View {
        VStack {
            Button("Set n = 2") {
                n = 2
                coverData = CoverData(n: n)
            }
        }
        .fullScreenCover(item: $coverData) { item in
            VStack {
                Text("n = \(item.n)")
                Button("Close") {
                    coverData = nil
                }
            }
        }
    }
}

struct CoverData: Identifiable {
    let id = UUID()
    var n: Int
}

Note when it's done this way, if the sheet is open when coverData is changed to one with a different ID, the sheet will animate away and appear again showing the new data.

  • Related