Skip to content

Data flow (SwiftUI)


Prepared and tested with Xcode 13.4 and Swift 5.3 (last update: 2022-05-29)

In this part you will learn how to pass data between components and views.

Table of contents


Introduction

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 property

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:

note that:

  • in one statement:

    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:

    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:

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.


State binding

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:


Observable object

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:

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.


State object

Before I will say anything please make a tiny change in previous code and replace:

with:

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:

When executed you will see something similar to this:

In this case you really share data between views.


Environment object

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:

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:

Consider the same example as before but with an environment object:

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.


Environment variables

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).

Now you can check how your app will react on changing Color Scheme:


Custom environment variables

  • Step 1: create the environment key
  • Step 2: extend the environment
  • Step 3: add a dedicated view modifier (optional)

    Now you have a choice to set your key either directly:

    or with modifier:

  • Step 4: application


Summary

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.