In traditional imperative UI frameworks, such as UIKit, the developer manually updates the UI by calling specific methods whenever data changes. SwiftUI, however, leverages a declarative model where the framework itself monitors changes in the UI's data model and triggers re-renders automatically. This concept is known as automatic redraws.
It's important to understand that a re-rendering resets our view, even the init
is executed again, so in order to keep the current value of our variables we can use the @State
property wrapper.
This means that we can have regular constants since they won't change their value.
State property wrapper
@State
will be the default property wrapper to use for the values that will change over time. For the values that won't change, we'll use constants:
struct ContentView: View {
let title: String = "EducaSwift example"
@State var count = 0
var body: some View {
VStack {
Text(title)
.font(.title)
Text("Count: \(count)")
.font(.headline)
.foregroundStyle(.gray)
Button("Increment") {
count += 1
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
}
We can use @State
only for structs, like primitive types (String
, Int
, Bool
...) or custom types. However, in case we want to use a class
, it needs to be marked as @Observable
.
Let's see an example where we create our own ViewModel which will store our constant and our count
variable.
@Observable
class ViewModel {
let title: String = "EducaSwift example"
var count: Int = 0
}
Then we will be able to use our ViewModel as a @State
variable, so any ViewModel property change will trigger a re-rendering in our view.
struct ContentView: View {
@State var viewModel = ViewModel()
var body: some View {
VStack {
Text(viewModel.title)
.font(.title)
Text("Count: \(viewModel.count)")
.font(.headline)
.foregroundStyle(.gray)
Button("Increment") {
viewModel.count += 1
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
}
Binding variable
Constants and @State
variables can be passed as a parameters to a child view, then that child view can make changes to its own @State
variable. Both variables will keep different values:
struct ContentView: View {
let title: String
@State var count: Int
var body: some View {
VStack {
Text(title)
.font(.title)
Text("Parent Count: \(count)")
.font(.headline)
.foregroundStyle(.gray)
Button("Increment Parent") {
count += 1
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
ChildView(title: "Child Title", count: count)
.padding(.top, 32)
}
}
}
struct ChildView: View {
let title: String
@State var count: Int
var body: some View {
VStack {
Text(title)
.font(.title)
Text("Child Count: \(count)")
.font(.headline)
.foregroundStyle(.gray)
Button("Increment Child") {
count += 1
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
}
However, we can bind the values of these two variables, meaning that if any of their values change, then the other one will update with the same value. To do this we just need to do 2 changes to our previous example:
- Mark child view property as
@Binding
. - Use
$
symbol on the parent.
struct ContentView: View {
let title: String
@State var count: Int
var body: some View {
VStack {
Text(title)
.font(.title)
Text("Parent Count: \(count)")
.font(.headline)
.foregroundStyle(.gray)
Button("Increment Parent") {
count += 1
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
ChildView(title: "Child Title", count: $count) // <-- Use $ here
.padding(.top, 32)
}
}
}
struct ChildView: View {
let title: String
@Binding var count: Int // <-- Marked as @Binding
var body: some View {
VStack {
Text(title)
.font(.title)
Text("Child Count: \(count)")
.font(.headline)
.foregroundStyle(.gray)
Button("Increment Child") {
count += 1
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
}
Be the first to comment