State & Binding
Source: https://developer.apple.com/tutorials/app-dev-training/managing-data-flow-between-views
State and Binding
For SwiftUI, to manage a single source of truth, you can use @State.
@ denotes a property wrapper, which encapsulates a common property-initialization pattern, helping you add behaviors to your properties efficiently. When a @State property value changes, the system automatically redraws the view using the updated values of the property.
Declare @State properties as private so that they can be accessed only within the view in which you define them.
A property that you wrap with @Binding shares read and write access with an existing source of truth.
@Binding does not store the property directly.
The system establishes dependencies between the data in @State and the child view that contains the @Binding.
You don’t need to write code to observe data because the system automatically updates the relevant views to reflect changes made to the source of truth.
State
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct DetailEditView: View {
@State private var scrum = DailyScrum.emptyScrum
var body: some View {
Form {
Section(header: Text("Meeting Info")) {
TextField("Title", text: $scrum.title)
HStack {
Slider(value: $scrum.lengthInMinutesAsDouble, in: 5...30, step: 1) {
Text("Length")
}
Spacer()
Text("\(scrum.lengthInMinutes) minutes")
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.sheet(isPresented: $isPresentingEditView) {
NavigationStack {
DetailEditView()
.navigationTitle(scrum.title)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
isPresentingEditView = false
}
}
}
}
}
}
For write access, you need to use $. For sheet, isPresentingEditView value decides whether to present a view or not. In this case, you need to bind the value using $ so that SwiftUI can track changes and updates the view accordingly. For text, the system can automatically update the view when the value it displays changes because SwiftUI tracks state changes inside the view.
Binding
Constant Binding
1
2
3
4
5
6
7
8
9
10
11
struct ThemePicker: View {
@Binding var selection: Theme
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
#Preview {
ThemePicker(selection: .constant(.periwinkle))
}
a binding to a hard-coded, immutable value
Projected Value
1
2
3
4
5
6
List($scrums) { $scrum in
NavigationLink(destination: DetailView(scrum: scrum)) {
CardView(scrum: scrum)
}
.listRowBackground(scrum.theme.mainColor)
}
“$scrum” The $ prefix accesses the projectedValue of a wrapped property. The projected value of the scrums binding is another binding.
Source: https://developer.apple.com/tutorials/app-dev-training/making-classes-observable
Class for $State, Source of Truth
1
2
3
4
5
6
@Observable public final class ScrumTimer {
public var activeSpeaker = ""
public var secondsElapsed = 0
public var secondsRemaining = 0
// ...
}
Make the class @Observable. By doing so, all the properties of that class automatically trigger UI updates when they change.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct ParentView: View {
@State var scrumTimer = ScrumTimer()
// ...
}
struct ChildView: View {
@Bindable var timer: ScrumTimer
// ...
}
struct MeetingView: View {
var scrumTimer = ScrumTimer()
var body: some View {
VStack {
ChildView(timer: scrumTimer)
}
}
}
Use State and Bindable(instead of Binding)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct ParentView: View {
@State var scrumTimer = ScrumTimer()
var body: some View {
VStack {
ChildView()
.environment(scrumTimer)
}
}
// ...
}
struct ChildView: View {
var body: some View {
GrandchildView()
}
}
struct GrandchildView: View {
@Environment(ScrumTimer.self) private var timer
}
To share an observable object in a complex view hierarchy, use @Environment. In any descendent of Childview, you can access the timer, even if the intermediate view don’t have refereces to the object.
More tutorial: https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app