SwiftUI: Handling State in your Views

SwiftUI: Handling State in your Views

In SwiftUI there are multiple ways to handle state in your views.

@State

@State is used when your view own this variable. When the variable changes its value, SwiftUI updates the views automatically.
Because of this, it makes sense to use @State for value types (i.e. strings, booleans), where when they are overwritten, they are completely overwritten.

Let's look at an example of @State:

struct ContentView: View {
	@State private var counter = 0
	
	var body: some View {
		Button(action: {
			counter = counter + 1
		}) {
			Circle()
				.frame(width: 200, height: 200, alignment: .center)
				.foregroundColor((counter % 2 == 0) ? Color.red : Color.green)
				.overlay (
					Text("\(counter)")
						.font(.system(size: 100, weight: .bold, design: .rounded))
						.foregroundColor(.white)
				)
		}
	}
}

You see here that there is a @State variable, counter, of type Int (in fact, a value type).
When you press on the button, the value of counter is incremented. This has two effects:

  • updating the overlay text with the number itself;
  • updating also the background color: note that the counter variable is used in an expression, and as the variable changes, because it is marked with @State, also the expression is re-evaluated.

@Binding

@Binding is used when your view is dependent on some values that it doesn't own.
A typical example of this would be:

  • a parent view containing a @State variable; so the parent view owns the variable;
  • a child view containing a @Binding variable; as the child view doesn't own the variable, it receives it as parameter from the parent view.
    Note that a @Binding variable is passed from the parent to the child view with the dollar ($) sign.

Let's look at an example of @Binding:

struct ContentView: View {
	@State private var counter = 0
	
	var body: some View {
		VStack {
			ChildView(counter: $counter)
			ChildView(counter: $counter)
		}
	}
}

struct ChildView: View {
	@Binding var counter: Int
	
	var body: some View {
		Button(action: {
			counter = counter + 1
		}) {
			Circle()
				.frame(width: 200, height: 200, alignment: .center)
				.foregroundColor((counter % 2 == 0) ? Color.red : Color.green)
				.overlay (
					Text("\(counter)")
						.font(.system(size: 100, weight: .bold, design: .rounded))
						.foregroundColor(.white)
				)
		}
	}
}

As discussed before, the parent view owns the @State variable; it passes it to its subview(s) using the dollar ($) symbol, and the subview(s) declare the variable with @Binding because they are not the owner of the variable, but they received it from their callers.

@EnvironmentObject

@EnvironmentObject is used to share your view across different views.
In this case no-one is the owner of the view.

Let's look at an example of @EnvironmentObject:

final class UserSettingsStore: ObservableObject {
	init() {
		UserDefaults.standard.register(defaults: [
			"view.preferences.counter" : 0
		])
	}
	
	@Published var counter: Int = UserDefaults.standard.integer(forKey: "view.preferences.counter") {
		didSet {
			UserDefaults.standard.set(counter, forKey: "view.preferences.counter")
		}
	}
}

struct ContentView: View {
	@EnvironmentObject var userSettingsStore: UserSettingsStore
	
	var body: some View {
		VStack {
			ChildView()
			ChildView()
		}
	}
}

struct ChildView: View {
	@EnvironmentObject var userSettingsStore: UserSettingsStore
	
	var body: some View {
		Button(action: {
			userSettingsStore.counter = userSettingsStore.counter + 1
		}) {
			Circle()
				.frame(width: 200, height: 200, alignment: .center)
				.foregroundColor((userSettingsStore.counter % 2 == 0) ? Color.red : Color.green)
				.overlay (
					Text("\(userSettingsStore.counter)")
						.font(.system(size: 100, weight: .bold, design: .rounded))
						.foregroundColor(.white)
				)
		}
	}
}

struct ContentView_Previews: PreviewProvider {
	static var previews: some View {
		ContentView().environmentObject(UserSettingsStore())
	}
}

You can see that here we have defined a class that will container the state of our object.
Both ContentView and ChildView receive the @EnvironmentObject during instantiation.
In this case I have also copied the preview-view, to show how to pass the @EnvironmentObject.

@ObservedObject

@ObservedObject is conceptually very similar to @State, with the difference that it is applied to reference objects rather than value objects.
I.e. it can listen to changes of individual properties of an object, rather than replacing the full instance.
To make it work, you need to declare the classes of the objects you want to listen to conform to the @ObservableObject Combine protocol, and tagging the properties you want to listen to with @Published.

Let's look at an example with @ObservedObject:

final class UserSettingsStore: ObservableObject {
	init() {
		UserDefaults.standard.register(defaults: [
			"view.preferences.counter" : 0
		])
	}
	
	@Published var counter: Int = UserDefaults.standard.integer(forKey: "view.preferences.counter") {
		didSet {
			UserDefaults.standard.set(counter, forKey: "view.preferences.counter")
		}
	}
}

struct ContentView: View {
	@ObservedObject var userSettingsStore = UserSettingsStore()
	
	var body: some View {
		VStack {
			ChildView(counter: $userSettingsStore.counter)
			ChildView(counter: $userSettingsStore.counter)
		}
	}
}

struct ChildView: View {
	@Binding var counter: Int
	
	var body: some View {
		Button(action: {
			counter = counter + 1
		}) {
			Circle()
				.frame(width: 200, height: 200, alignment: .center)
				.foregroundColor((counter % 2 == 0) ? Color.red : Color.green)
				.overlay (
					Text("\(counter)")
						.font(.system(size: 100, weight: .bold, design: .rounded))
						.foregroundColor(.white)
				)
		}
	}
}

You see that in this last example we change a property of an object but we don't replace the whole object at each tap. The change of the counter is pushed thats to the @Published property wrapper.