💾 Archived View for dyn.fussycoder.ninja › wwdc2020 › data_essentials_in_swiftui.gmi captured on 2022-07-16 at 14:23:37. Gemini links have been rewritten to link to archived content

View Raw

More Information

-=-=-=-=-=-=-

Data Essentials in SwiftUI

This video is at Apple at:

https://developer.apple.com/videos/play/wwdc2020/10040

This video uses the example of an app that represents a book they are reading.

(Ie, which book, how much of it they've read, etc).

Data Flow in the SwiftUI App

The video walks through writing an app that tracks how much of a book you've been reading.

Whenever we implement a new View in SwiftUI, there are three key questions it needs to have resolved:

For the example application, the answer to these questions for the view we're calling "BookCard", respectively, are:

The superview will pass the true data whenever it instantiates a "BookCard", a new "BookCard" is instntiated every time the superview's body is instantiated, and then it is thrown away once SwiftUI has rendered it.

The implementation from the video is therefore as follows:

struct BookCard: View {
    let book: Book
    let progress: Double

    var body: some View {
        HStack {
            BookCover(book.coverName)
            VStack(alignment: .leading) {
                TitleText(book.title)
                AuthorText(book.author)
            }
            Spacer()
            RingProgressView(value: progress)
        }
    }
}

The below diagram shows how data flows inside "BookCard":

dataflow_for_bookcard.png

State and Binding - Allowing progress to be recorded

What we actually want is to allow the user to tap on a book, and then record the new progress, like the following Sheet presents:

sheet_with_progress.png

Lets consider the data flow for this sheet, lets consider the three main questions we need to ask ourselves, firstly:

What data does it need?

It requires:

We could put them directly into the view, but it is much better to extract them as out a struct called "EditorConfig" since, for the following two reasons:

Next, we need to know:

How will the view manipulate the data?

We will have a button that toggles the "presentation state", and because we extract the data to a struct, we can make this EditorConfig responsible for those updates, so we will have the BookView ask the EditorConfig to do the work.

Where will the data come from?

The EditorConfig is local to this view, so there isn't soem parent view that can pass it in.

The Source of Truth, therefore, needs to be established: The simplest option is to use "@State"

As a diagram of the new view we need to write, we have the following:

book_view_editor.png

The thick border around the orange "EditorConfig" represents state that is managed by SwiftUI.

Why is that important? The views are transcient - once rendered, they're thrown away. But the EditorConfig, because it is a Source Of Truth, remains. Next time a view is rendered, SwiftUI will reconnect the state to the existing "storage".

The code for all this is as follows:

struct EditorConfig {
    var isEditorPresented = false
    var note = ""
    var progress: Double = 0
    mutating func present(initialProgress: Double) {
        progress = initialProgress
        note = ""
        isEditorPresented = true
    }
}

struct BookView: View {
    private var EditorConfig = EditorConfig()
    func presentEditor() { editorConfig.present(...) }
    var body: some View {
        ...
        Button(action: presentEditor) { ... }
        ...
    }
}

That was the BookView, which presents the editor, but now we need to focus on the BookEditor itself - again, the three questions, which we will answer as follows:

The video has a very good explanation - first it explains why ProgressEditor's EditorConfig can't be a simple var, then it explains that it can't be a "@State" either, because that would create an additional sourse of truth.

WWDC2020 - Data Essentials - Explaining why we use Binding

We want the one source of truth, otherwise the values in the ProgressEditor's EditorConfig won't make it through to the parent BookView's EditorConfig.

So we use a Binding. In SwiftUI, these are indicated by using dollarsigns:

struct BookView: View {
    @State private var editorConfig = EditorConfig()
    var body: some View {
        ...
        ProgressEditor(editorConfig: $editorConfig)
        ...
    }
}

This is an intriguing point made in the video:

The dollar sign in the call creates a Binding from the State because the projected value of the State property wrapper is a Binding.

Summary (Copied verbatim)

The Binding property wrapper creates a data dependency between the ProgressEditor and the EditorConfig state in BookView. Many built in SwiftUI controls also take Bindings. For example, the new TextEditor control used for our notes takes a Binding to a String value. Using the dollar sign projected value accessor, we can get a new Binding to the note within the EditorConfig.

SwiftUI lets us build a new Binding from an existing Binding. Remember, Bindings aren't just for State.

Designing the Data Model: Introducing ObservableObject

Because state is not the whole story.

This section of the talk starts at:

Luca starts talking about Designing the Data Model.

'ObservableObject' allows managing data life cycles, including persisting it, sync'ing it, etc - and allows integrating the data model of an application with that of the UI.

It implements only one property: "objectWillChange", which is a publisher, and allows you to define a new "Source Of Truth" when you implement an "ObservableObject".

Luca talks about how an "ObservableObject" represents the "data dependency surface" - that is, the part of your data model that is exposed to SwiftUI, but it's not neccessarily the full data model used by the application.

There are a few different possibilities:

Diagram of a single observed object.

Diagram of multiple observed objects, each exposing a different part of your data model.

Luca gives the following example of using an "ObservedObject", using the book example:

class CurrentlyReading: ObservableObject {
  let book: Book
  @Published var progress: ReadingProgress
  ...
}

That is, "CurrentlyReading" represents the part of the model that is exposed to the view to present data and respond to changes.

The "Three Questions" still apply:

The example above already covers the first of those questions.

Integrating the Data Model into the app

To actually integrate it into the UI, SwiftUI provides the following property wrappers:

All these are used by SwiftUI to declare a dependency on that data by the View.

A common question is "why 'will' change? Why not 'did' change?

And that's because SwiftUI needs to know what "will" change so that it can coalesce all the changes into a single update.

Many SwiftUI components will accept a "Binding" to a source of truth which allows the component to read and write data whilst preserving a single "Source of truth".

An example of such a component is the "Toggle", as shown:

struct BookView: View {
  @ObservedObject var currentlyReading: CurrentlyReading

  var body: some View {
    ...
    Toggle(
      isOn: $currentlyReading.isFinished
    ) {
      Label(
        "I'm done",
        systemImage: "checkmark.circle.fill")
    }
    ...
  }
}

As shown above, a Binding can be created by prefixing with a dollar sign, and allows the Toggle component to read, and write, to the source of truth.

Life Cycle of a SwiftUI app

Section of the talk where Luca beings to talk about the life cycle of the app.

Very interesingly, when using a "StateObject", for example in the following code below, the variable is not instantiated when the view is created - it is instantiated instead just before the view's body is run. So lifetime of the StateObject is very much tied to the presentation of the view.

struct SomethingView: View {
    @StateObject var something = SomethingLoader()

    var body: some View { ... }
}

In the above example, SomethingLoader is instantiated when the view appears, and will be removed when the view stops being shown. There is no need to fiddle with onDisappear anymore either.

This is quite different to what would happen if @ObservedObject was used instead, because @ObservedObject would instantiate the property every time the view is created, and this can become slow.

Remember in SwiftUI, Views are cheap. We encourage you to make them your primary abstracting mechanism and create small views that are simple to understand and reuse.

However, when you have many small and reused views, you will end up with a hierachy, and you may often find that you need to share the same ObservedObject down the heirachy, sometimes even in a distant view, even through views that don't need data.

EnvironmentObject makes that easy to support. Declare the environmentObject high up in the heirachy using a viewModifier, and then declare it as a data dependency in the specific views lower in the heirachy that need it, by using the property modifier.

Luca summarizes these as:

Deep Dive into Application Livecycle with Raj

Raj starts here with a "deep dive into the SwiftUI's lifecycle"

Understanding this helps with understanding the performance of the application.

SwiftUI manages the identity and lifetime of your views, and has the following life cycle:

SwiftUI update lifecycle

However, if any step is slow, then performance suffers. Some solutions:

Next is data lifetime, however the video only goes through the bit that's relevant to the application lifecycle as far as performance is concerned, tying data to Views, Scenes, and the App.

However, for more on Scenes and the App, refer to "App Essentials in SwiftUI"

App Essentials in SwiftUI