In this part you will learn how to pass data between components and views.
Table of contents
- Introduction
- State property
- State binding
- Observable object
- State object
- Environment object
- Environment variables
- Summary
SwiftUI implement a data driven approach to app development. The views that make up a user interface layout are never updated directly within code. Instead, the views are updated in response to changes in the underlying data without the need to write handling code. This update is fully automatic based on the state of objects to which views have been bound and is "fired" as they change over time.
You have to remember that all of your views are simply functions of their state. This means that you don’t change the views directly, but instead manipulate the state and let that cause changes. A change of state results in a change to view and very often also other views in the layout.
This approach to interact with UI is much different than offerd by UIKit. Rather than focusing on how to update the UI which is an imperative style of working, you focus on what to update adopting a declarative programming style, allowing SwiftUI to handle all boring details.
State properties are used exclusively to store state that is local to a view layout such text updated according to slider position as you saw in Slider section of User interface controls part. State properties are used for storing simple data types such as a string or an integer value and are declared using the
@State
property wrapper. Note that since state values are local to the enclosing view they should be declared as private properties.
Analyzing the example with a slider:
1 2 3 4 5 6 7 8 9 10 |
struct ContentView: View { @State var value: Float = 0 var body: some View { VStack(alignment: .center) { Slider(value: $value, in: -10...10) Text("\(value)") }.padding() } } |
note that:
- in one statement:
1Slider(value: $value, in: -10...10)
value
property is preceded with$
prefix to establish binding between state propertie and the view which allows to change value of this property; - in the other statement:
1Text("\(value)")
value
property is without the$
prefix, because in this case you are only referencing the value assigned to the state property which allows to read value of this property.
Bidirectional nature of the state proprty is easier to see if you add one more component to your view – the button:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct ContentView: View { @State var value: Float = 0 var body: some View { VStack { Slider(value: $value, in: -10...10) Text("\(value)") Button("Reset"){ value = 0.0 } } .padding() } } |
Now, every time you change slider position, value
property is automatically updated by SwiftUI. On the other side, if you somehow change value
property (in this case by pressing Reset button) component binded to this variable reacts.
A state property is local to the view in which it is declared. If you want to change it in subviews you have to declare it in subview as a "bindable" state property with
@Binding
property wrapper as I did in this example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
struct ContentSubview: View { @Binding var sliderPosition: Float var body: some View { VStack { Slider(value: $sliderPosition, in: -10...10) Text("\(sliderPosition)") } .background(Color.black.brightness(0.95)) } } struct ContentView: View { @State var value: Float = 0 var body: some View { VStack(alignment: .center) { ContentSubview(sliderPosition: $value) Text("\(value)") }.padding() } } |
State properties are local to the view but can be accessed by other views if you use a state binding in subviews. The drawback of state property is that it is limited to simple data types such as a string or an integer. For more complex properties for which you want to use custom type that might have multiple properties and methods you may use
@ObservedObject
instead.
An observable object takes the form of a class or structure that conforms to the ObservableObject
protocol. It works in publish-subscriber model. The observable object publishes the data values for which it is responsible as published properties – you simply use the @Published
property wrapper when declaring a property. Observer then subscribe with @ObservedObject
property wrapper to the publisher to be informed every time changes to the published properties occur.
The following structure declaration shows a simple observable object in action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
class CustomData: ObservableObject { @Published var value: Float = 0.0 var id = UUID() } struct ContentSubview: View { @Binding var sliderPosition: Float @ObservedObject var customData = CustomData() var body: some View { VStack { Slider(value: $sliderPosition, in: -10...10) {_ in customData.value = sliderPosition } Text("\(sliderPosition)") Text("\(customData.value)") Text("\(customData.id)") } .background(Color.black.brightness(0.95)) } } struct ContentView: View { @State var value: Float = 0 var body: some View { VStack(alignment: .center) { ContentSubview(sliderPosition: $value) Text("\(value)") }.padding() } } |
This example shows how to define the equivalent of @State
allowing you to operate on complex types. Apart that it also shows one more very important thing. Note that text displayed as value of id
property of CustomData
class constantly changes while you move slider. This means that along with refreshing the view a customData
property is constantly reinitialized (which results in new id
values). An indispensable companion of @ObservedObject
is @StateObject
I explain in next section.
Before I will say anything please make a tiny change in previous code and replace:
1 |
@ObservedObject var customData = CustomData() |
with:
1 |
@StateObject var customData = CustomData() |
When executed you will see something similar to this:
Can you see the difference? Now text displayed as value of id
property of CustomData
class stays unchanged while you move slider.
To use @StateObject
you must conform to the ObservableObject
protocol as you do for @ObservedObject
.
The key difference is that an @ObservedObject
reference is not owned by the view in which it is declared, and in consequence there is at risk of being destroyed or reinitialized while it is still in use, for example as the result of the view refreshment.
Clearly speaking, difference between @ObservedObject
and @StateObject
is an object ownership and distinction which view created the object, and which view is just watching it.
The rule is simple: the view which create your object is the owner of the data and is responsible for keeping it alive so it must use @StateObject
. All other views must use @ObservedObject
as they only want to use the object (watch for changes and change it) but don’t own it directly. Check the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
class CustomData: ObservableObject { @Published var value: Float = 0.0 var id = UUID() } struct ContentSubview: View { @Binding var sliderPosition: Float @ObservedObject var customData: CustomData var body: some View { VStack { Slider(value: $sliderPosition, in: -10...10) {_ in customData.value = sliderPosition } Text("\(sliderPosition)") Text("\(customData.value)") Text("\(customData.id)") } .background(Color.black.brightness(0.95)) } } struct ContentSubviewSecond: View { @State var sliderPosition: Float = 0.0 @ObservedObject var customData: CustomData var body: some View { VStack { Slider(value: $sliderPosition, in: -10...10) {_ in customData.value = sliderPosition } Text("\(sliderPosition)") Text("\(customData.value)") Text("\(customData.id)") } .background(Color.red.brightness(0.4)) } } struct ContentView: View { @State var value: Float = 0 @StateObject var data = CustomData() var body: some View { VStack(alignment: .center) { ContentSubview( sliderPosition: $value, customData: data) Text("\(value)") ContentSubviewSecond(customData: data) }.padding() } } |
When executed you will see something similar to this:
In this case you really share data between views.
In SwiftUI, the environment is a store for variables and objects that are shared between a view and its children.
From previous section you know that it makes sense to use observed objects when a particular data needs to be used by a few views within an app. To do this the view which is the owner of an object must pass a reference to the object to the destination view. Situation complicates when hierarchy of object is deep and you want to pass reference from top view to the view at the very bottom view – you must pass this reference from every parent to every child which are on the path from owner to "destination" view.
With an environment object, declared in the same way as an observable object (in that it must conform to the ObservableObject
protocol and appropriate properties must be published) but with @EnvironmentObject
property wrapper, this process is much simpler. In this case the object is stored in the environment of the view in which it is declared and can be accessed by all child views without needing to be passed from view to view.
You can think about an @EnvironmentObject
as a smarter, simpler way of using @ObservedObject
on lots of (dependent) views. Rather than creating some data in view A, then passing it to view B, then view C, then view D to be able finally to use it, you can create it in view A and put it into the environment so that all other (dependent) views B, C, and D will automatically have access to it.
In case of @ObservedObject
you would have had to pass and define the value within each child:
1 2 3 4 |
View A – defines the object to pass it into B. View B – reads the injected object from A, defines the object to pass it into C. View C – reads the injected object from B, defines the object to pass it into D. View D – reads the injected object from C and finally use it. |
Now there is no need to forward any objects from one view to another only because you want to use it a few levels deep in hierarchy of objects:
1 2 3 4 |
View A – defines the environment object View B - don’t do anything with the environment object. View C - don’t do anything with the environment object. View D – reads the environment object and use it |
Consider the same example as before but with an environment object:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
class CustomData: ObservableObject { @Published var value: Float = 0.0 var id = UUID() } struct ContentSubview: View { @Binding var sliderPosition: Float @EnvironmentObject var customData: CustomData var body: some View { VStack { Slider(value: $sliderPosition, in: -10...10) {_ in customData.value = sliderPosition } Text("\(sliderPosition)") Text("\(customData.value)") Text("\(customData.id)") } .background(Color.black.brightness(0.95)) } } struct ContentSubviewSecond: View { @State var sliderPosition: Float = 0.0 @EnvironmentObject var customData: CustomData var body: some View { VStack { Slider(value: $sliderPosition, in: -10...10) {_ in customData.value = sliderPosition } Text("\(sliderPosition)") Text("\(customData.value)") Text("\(customData.id)") } .background(Color.red.brightness(0.4)) } } struct ContentView: View { @State var value: Float = 0 let data = CustomData() var body: some View { VStack(alignment: .center) { ContentSubview( sliderPosition: $value) Text("\(value)") ContentSubviewSecond() } .padding() .environmentObject(data) } } |
When executed you will see something similar to this:
Notice in the above example how I use the environmentObject()
modifier to pass the observable object to a view subhierarchy.
The
@Environment
property wrapper is similar to @EnvironmentObject
: both allows to share data across a view hierarchy. The difference is that you can add anything as an environment object, but environment values are more like key-value pairs and offer you an access to predefined set of properties (but it is possible to define your own custom keys/values).
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct ContentView: View { @Environment(\.colorScheme) var colorScheme: ColorScheme var body: some View { VStack(alignment: .center) { if colorScheme == .dark { Text("Dark") } else { Text("Light") } } } } |
Now you can check how your app will react on changing Color Scheme:
- Step 1: create the environment key
123private struct SensitiveDataVisible: EnvironmentKey {static let defaultValue: Bool = false} - Step 2: extend the environment
123456extension EnvironmentValues {var sensitiveVisible: Bool {get { self[SensitiveDataVisible.self] }set { self[SensitiveDataVisible.self] = newValue }}} - Step 3: add a dedicated view modifier (optional)
12345extension View {func sensitiveVisible(_ value: Bool) -> some View {environment(\.sensitiveVisible, value)}}Now you have a choice to set your key either directly:
12Text("Secret 123").environment(\.sensitiveVisible, true)or with modifier:
12Text("Secret 123").sensitiveVisible(true) - Step 4: application
1234567891011121314151617181920212223242526272829303132333435363738394041424344struct ViewOne: View {@Environment(\.sensitiveVisible) private var showSensitivevar body: some View {VStack(alignment: .center) {if showSensitive {Text("Secret 123")} else {Text("**********")}}.padding()}}struct ViewTwo: View {@Environment(\.sensitiveVisible) private var showSensitivevar body: some View {VStack(alignment: .center) {if showSensitive {Text("Secret ABC")} else {Text("**********")}}.padding()}}struct ContentView: View {@State private var showSensitive = falsevar body: some View {VStack(alignment: .center) {Toggle("Show sensitive data", isOn: $showSensitive)ViewOne().sensitiveVisible(showSensitive)ViewTwo().sensitiveVisible(showSensitive)}.padding()}}
As you saw in this part, SwiftUI offers you many ways to bind data to the views.
The key points of this part are:
- Use state with
@State
and@Binding
to pass a simple data types, such as a string or an integer, between views. This values are local to the current content view and you will lost them when the view goes away. - Use observable object with
@ObservedObject
and@StateObject
to pass object between views with dependency injection. - Use environment object to pass data between parent and any of its direct and indirect child without a need of dependency injection.
- Use environment variable to control global behaviour of your user interface components.