4.5 KiB
State
The user interface is treated as a function of its state. Instead of directly modifying the UI, modify its state to update views.
Reactive Programming
When using Meta, not only the structure of the user interface is developed in a declarative way (doc:DeclarativeDesign), the updating system is as well. You define all the information used for processing the user interface in the app's structure or custom view structures. Whenever information gets updated, the user interface re-renders automatically.
State
An app is built around data it can read and modify. This data is called state. As an example, if you have a simple counter app, there is one piece of state: the count.
@main
struct CounterApp: App {
let id = "io.github.david_swift.CounterApp"
var app: SomePlatformApp!
@State private var count = 0 // Initialize state
var scene: Scene {
Window("Awesome App") {
HStack {
Button(.minus) { count -= 1 } // Modify state
Text("\(count)") // Get state
Button(.plus) { count += 1 } // Modify state
}
}
}
}
Whenever one of the buttons gets pressed and updates the state variable, the UI elements automatically get updated (see doc:DeclarativeDesign for more information about the elements). As this happens for all the state variables (properties of the view marked with State), and because manual synchronization is challenging, the following rule is very important:
Each state variable creates a new source of truth.
You should have one source of truth for one piece of data. It might help to think about the UI being a function of the state: in the same way as you want to minimize the number of parameters of a function and compute as much as possible, you want to minimize the number of state variables. Therefore, if you want to display double the count, implement the operation while rendering the view:
Text("\(2 * count)")
For more complex operations, computed variables can be helpful.
@main
struct CounterApp: App {
let id = "io.github.david_swift.CounterApp"
var app: SomePlatformApp!
@State private var count = 0
var doubleCount: Int {
2 * count
}
var scene: Scene {
Window("Awesome App") {
HStack {
Button(.minus) { count -= 1 }
Text("\(doubleCount)")
Button(.plus) { count += 1 }
}
}
}
}
State in Views
State cannot only be defined for an app definition, but also for a view definition. In order not to violate the "single source of truth" concept, the state should be defined in the least common ancestor of all the views accessing this state.
You can pass state to child views using two different methods.
Read the State in Child Views
Whenever you want to read, but not modify the state in a child view, use a regular property in the view.
struct CountView: View {
var count: Int // The property
var view: Body {
Text("\(count)") // Get the property
}
}
struct ContentView: View {
@State private var count = 0
var view: Body {
CountView(count: count) // Pass state to the child view
}
}
Read and Modify the State in Child Views
If you want to modify the state in a child view, you need to establish two-way data traffic.
Meta provides the property wrapper Binding for this.
struct IncreaseButton: View {
@Binding var count: Int // The binding property
var view: Body {
Button(.plus) {
count += 1 // Modify the binding
}
}
}
struct ContentView: View {
@State private var count = 0
var view: Body {
IncreaseButton(count: $count) // Pass state or a binding as a binding to the child view using the dollar sign ($)
}
}
Observation
State can manage either value types (as seen in the examples above), or observable reference types.
@Observable
class TaskModel {
var tasks: [String] = []
}
struct ContentView: View {
@State private var model = TaskModel()
var view: Body {
TaskList(tasks: $model.tasks)
}
}
Observable reference types can be handy when, e.g., synchronizing state with a server.
State in Backends
When creating a backend, you do not have to worry about state. State is fully managed by the Meta package. More information on how to create a backend can be found in the doc:CreateBackend tutorial.