Demystifying data flow through SwiftUI
--
The newly released SwiftUI allows you to write your interface declaratively. And with the new data flow mechanisms, you can write your data flow declaratively as well — and everything will update as you would expect it to, automatically. The concepts are powerful, but they can be a little confusing.
In this post, I’ll break down all the ways to move data declaratively through our SwiftUI views. Let’s start with a cheat sheet, and then we’ll move into descriptions and examples of each.
Property
Summary: read-only values provided by the parent view.
- A property is how a view defines what it expects its parent view to give to it in order for it to render, like a
prop
in React. - These values are read-only. But when they change, SwiftUI automatically re-renders your view.
- Can have a default value, allowing a parent view to not pass its own value for the property.
Use when: Your view needs to simply render using a piece of data or state which the parent has, and that it does not need to modify.
Example: Here is an example, where a ContactAvatar
component is made to reuse the code that formats the image, and different images can be passed to it using a property.
@State
Summary: Internal, read-write values.
- A @State value is a source of truth managed internally to a view.
- Should be marked as private for idiomatic accuracy.
- Must have a default value which is used initially.
- When it is updated, the view will be re-rendered accordingly.
Use when: Your view component needs its own state; a piece of data which it can change, and which none of its parent views care about, such as a hover state.
Example: In this example, we have a NotificationCard
component which displays a long line of text. When it is clicked or tapped, it expands. This is managed with a state variable isExpanded
.
@Binding
Summary: read-write values provided by the parent view.
- A @Binding values is a value provided by a parent view, like a property, but that can be written to, like state.
- When it is updated, whether by the current view or another in the hierarchy, SwiftUI re-renders the views accordingly.
- Parent views provide a binding rather than just a value. Bindings might come from state, other bindings, or ObjectBinding (covered below).
- Does not take an initial or default value.
Use when: Your view needs to modify a piece of data or state which its parent view cares about, such as a toggle button’s pressed state.
Example: In this example, we have a PlayButton
which accepts a binding for isPlaying
. This allows the view to toggle the value when the user taps or clicks the button, and since the value is a binding, all other views which used that value to render are automatically updated by SwiftUI.
@ObjectBinding
Summary: a reference to a binding which is provided by a parent view, but originates from outside of the view hierarchy.
- An @ObjectBinding value is provided by a parent view from a data source that is outside your view hierarchy.
- The value is provided by an object (class) that conforms to the
BindableObject
protocol. - It is read-write, and SwiftUI updates the view hierarchy automatically when it is changed.
Use when: Your view needs to access or modify data held in an external data source provided by the parent view, such as a list that can add and remove items.
Example: In this example, we have an external source of data, a class called ViewPreferences
. We’ve made it conform to the BindableObject
protocol. A PasscodeViewer
view can receive a reference to ViewPreferences
as an @ObjectBinding
provided by its parent view. Updates to the value cause all views which use it to re-render accordingly.
@EnvironmentObject
Summary: a reference to a binding which is provided by any ancestor view, and originates from outside of the view hierarchy.
- An @EnvironmentObject value is provided by any ancestor view from a data source that is outside your view hierarchy.
- The value is provided by an object (class) that conforms to the
BindableObject
protocol. - It is read-write, and SwiftUI updates the view hierarchy automatically when it is changed.
Use when: Your view needs to access or modify data held in an external data source which is more cleanly made available to an entire view hierarchy at once, like using an object that defines the color scheme for all components in entire window or view.
Example: In this example, we have an external source of data, a class called ViewPreferences
. We’ve made it conform to the BindableObject
protocol. A PasscodeViewer
view can receive a reference to ViewPreferences
as an @EnvironmentObject
provided by any ancestor view. Updates to the value cause all views which use it to re-render accordingly.
In the next post in this series, we’ll cover how to use Combine to write your asynchronous data flow declaratively, and bind to this data within your SwiftUI view code with the same constructs as above. Being able to write your async data flow declaratively is just as big of a deal as writing just your interface declaratively, and again — everything will update as you expect.