Data Flow & State in SwiftUI

Siamak (Ash) Ashrafi
Jun 5 · 8 min read

Understanding SwiftUI State and Data Flow

Pro tip: When searching for topics on SwiftUI:
Do not put a space between Swift and UI. Use SwiftUI not Swift UI.

SwiftUI

→ To see when to use what please jump to the bottom of the post. ←


I am not going to argue if SwiftUI is the future, just like I am done arguing that Swift is the future … I still have people telling me Apple is dumping Swift and going back to Objective-C(rap).

Just watch this video if you doubt the power of Swift Protocol Programming.

Note: Crusty is using the latest advancements in Swift so to say he is an “old time programmer stuck in the past” is just a farce.

Yes, SwiftUI is amazing because it is declarative and not imperative programming. That means that when you add padding it will adapt to the Apple Human Guidelines (HIG) for that device.

This video has one of the best explanations of why we need to move to a declarative approach for UI. Leading to a data driven UI and not an event driven UI.

But that is only the frosting on the cake. The real power of SwiftUI is that we are now MVVM and no longer use a Controller (MVC). In this post we will detail the what that means for our iOS programs. No longer do we have the viewController trying to own multiple views, manually setting up target action, defining delegates, responding to events and no longer having to keep data in sync with the view.

Old way of storing data in ViewController.
SwiftUI does not use a ViewController.

iOS MVC:
The ViewController is the bridge between the Model and the View. The controller must keep track of the variables and state for isPlaying producing multiple copies.

SwiftUI does not use a ViewController to keep variables or maintain state. Only one copy of isPlaying.

Single Source of Truth!


SwiftUI uses features from Swift 5.1

These features provide a very DSL-like API but only run on iOS13.

  • Opaque return types — generic protocols can now be used as return types.

Some keyword allows us to return any value conforming to the specified protocol, allowing for easy refactoring of our views.

struct ContentView: View {     
var body: some View {
Image(uiImage: image) // Function builders
Text(title) // ...
Text(subtitle) // Function builders
} // omitted return
}
  • Omitted return keywords — return keyword can now be omitted for single-expression functions.
  • Function builders — enables the builder pattern to be implemented using closures.
  • Property Wrapper — access with additional behavior. Property values can be automatically wrapped using specific types such as views. Also makes it much easier to define various kinds of bindable properties.

Working with managed data / state:

SwiftUI gives us Property, @State, @Binding, BindableObject, and @Environment Property Wrappers.

From WWDC Data Flow through SwiftUI

The days of Strong, Weak and Unowned are over. Let’s dig deep into state and variable management in SwiftUI.

With SwiftUI you just used these tools to describe the relationship and the framework takes care of the rest.

State and variable management require only two steps:

  1. Define your data (single source of truth) as your model.
    * For local managed storage, Use @State
    * For data that you manage, just conform to the BindableObject Protocol and setup the publisher (from Combine Framework)
  2. Setup a dependency on the model
    * When you build your view, pass in the reference to the model.

SwiftUI will keep track of the synchronization for you.

A few major key points to remember:

  1. Any time we access data that is a dependency.
  2. We should only have one single source of truth. Sometimes to accomplish this we need to lift the data up to a common ancestor.
  3. Views have lots of different functions: Layout, Visual Effects, Navigation, Views are for Data, Gestures, Drawing and Animation.
  4. All data flow is in one direction.
All data flows in one direction

Property

Data passed into the constructor

  • Holds data that is observed by the UI

Code:

struct HikeBadge: View {
var name: String
var body: some View {
Text(name)
}
}

The caller sets the name and UI renders it.


@State

Property Wrapper that holds private data for the View and its children. Managed storage for the variable.

Major Points:

  • Every @State is a source of truth
  • Views are a function of state, NOT a result of a sequence of events
  • Any change rebuilds the View and all its children
  • Mark @State private so you remember it is bound to the current view.

Code:

struct HikeView: View {
var hike: Hike
@State private var showDetail = false
...
Button(action: {
withAnimation {self.showDetail.toggle()}
}
...
}

The showDetail is just an internal state used by the button.


@Binding

Property wrapper read and write without ownership.

Major Points:

  • derivable form @State passed from the parent
  • use the “$” to pass the binding reference otherwise we will get a copy.
  • Framework lets you decide where you keep your data

Code:

Build the view:

struct myView : View {
@Binding var passInData: String
var body: some View {
Text(passInData)
}
}

Pass the data to the view:

@State private var someName: String = "hi" // create your data
// pass the data to the view
MyView(someName : $someName) // create a binding & pass it down

Working with External Changes and Data

Watch external changes.

When an alarm or notification fire they act just like another Action. We use the Combine Framework to handle them.

External interaction like alarms and notifications use the Combine Framework

Combine Framework:

The Combine Framework is like RxSwift comprising of three components.

  1. Publishers: is a struct value type that registers a subscriber and declaratively sets how to handle values & errors.
  2. Subscriber: is a reference type that receives values .
  3. Operators: Is a value type that connects the publisher with the subscriber and describes a behavior for changing values (i.e. from a type to an Int)

This must run on the main thread.

The below diagram from the WWDC 2019 Combine talk shows the flow.

WWDC 2019 Combine talk

Working with External Data we have two steps.

1. Add some state / Create a dependency which calls the closure

struct PlayerView: View {
@State private var somethingThatChages : Double = 0.0
var body: some View{
Text("\somethingThatChanges")
.onReceive(MyPublisher.myDataChange { whatChanged in
self.somethingThatChages = whatChanged
} // Closure called when things change.

}
}

2. Create a single source of truth to external data with the BindableObject Protocol. This is your data, SwiftUI just needs to know how to react to changes.

@ObjectBinding Protocol

  • External data you own and manage
  • Reference type
  • Great for the model you already have
  1. Create a source of truth by taking our models and conforming them to bindableObject Protocol
Class foo : BindableObject {
Var didChange = PassthroughSubject<Void, Never>() //← Publisher()
Func adv() {
didChange.send() //← send changes
}

SwiftUI will subscribe to this and it just needs to know how to react to data changing.

2) When you access the data you create a dependency.

Just use the BindableObject in our view.

struct MyView : View {
@ObjectBinding var model: MyModelObject
...
}

When you call the view just pass the model instance.

MyView(model : modelInstance) // view has dependency on model
WWDC 2019 Data Flow talk

→ Create a Dependency Indirectly ← Any view with a dependency will automatically update.

Because Views in SwiftUI are value types, anytime you are using a reference type we should be using the @ObjectBinding property wrapper. This way the framework will know when the data changes and can keep our view hierarchy up to date.


@EnvironmentObject — Creating Dependencies Indirectly

Write our bindable object into the environment.

Creating independent dependencies the @Environment is great to push data down the view hierarchy.

You can use this one environment model in lots of different views and they all get updated. Once we have defined the model in the environment we can just use it.

@ObjectBinding vs @EnvironmentObject

When do you use @ObjectBinding vs @EnvironmentObject

@ObjectBinding has to passed down the entire view stack

@EnvirnmentObject is a convenience

@Environment

Great way to pass lots of data

  • Data applicable to the entire hierarchy
  • Convenience for indirection
  • Good for: accent color, right-to-left, and more

When to use what

For every source of truth, we have two options for managing these.

@State

  1. View-local
  2. Value Type
  3. Framework Managed

→ Some notes on using @State effectively:

  • limit use of @State if possible
  • instead use derived binding or value
  • prefer @BindableObject for persistence.

Always question — does this state need to be owned by this view? Could it be lifted up or placed in the environment. There are only a few good examples of @State something like button highlighting.

@BindableObject

  1. External — data you control like a on device database
  2. Reference
  3. Developer Managed

— — Building Reusable Components — —

  • Read-only: Swift property, Environment (these are value types).
  • Read-write: @Binding without owning it.

Prefer immutable access

@Binding

Can bind to @State or @ObjectBinding or another @Binding

  • First class reference to data
  • Great for reusability
  • Use $ to derive from source

SwiftUI provides correctness and separation of conserve.

Final Takeaway: Data flow is built into the framework!

Siamak (Ash) Ashrafi

Written by

Bio: https://sessionize.com/Ash

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade