SwiftUIThe data flow

With the arrival of SwiftUI’s new UI framework, there are new paradisms and tools for data management, such as @State, @Binding, ObservedObject. Before explaining the attributes provided by SwiftUI, it makes more sense to understand why data management is changing and what the problem is being solved by the change in data management.

UIKit framework with developers gone through 10 years of wind and rain, in the use of UIKit development, need to manually maintain view data synchronization, with the increase of business, maintaining the view – data dependencies is a lot of work, if there is no a set of reasonable management methods, then there will be a view – data sync problem. While you can use notifications, KVO and other technologies to keep data and views in sync, this is not a good way, and UIKit does not provide the technology to solve these problems at all. UIKit itself is an imperative development mode, requiring controller to handle various events and coordinate the synchronization between View and Model, so it is very easy to have bloated controller.

SwiftUI has been designed with these considerations in mind and addresses these issues at the root, so that the SwiftUI data management tools will be easier to use and more to the point.

Data is all the information that drives the interface and SwiftUI has laid down two principles for data flow:

  • Data Access as a Dependency: Once the data is used by the View, it will become dependent on the View. When the data changes, SwiftUI will automatically synchronize and update the View.
  • A single source of truth: Single data source principle: There can only be one data source in a view.

There is only one source of data on the view hierarchy, and it should be. Data can come from inside the view, such as the highlighted state of a button, or from outside, such as user data for the login interface. In fact, no matter where the data comes from, there should be only one data source, because multiple data may cause data to be out of sync. As you probably know, when multiple views have multiple copies of data, it can be cumbersome to maintain information synchronization between the data.

An example of maintaining two data sources, the Feed list and the Feed detail page, using different data. The list of feeds does not change synchronously after the favorites status of the details page changes.

In SwiftUI we can divide data transformation into two categories, internal and external changes. Let’s take a look at the internal data, such as the state of the play button below, and the state of the filter favorites switch in the list. These data only need to be saved inside the View.


Using the podcast player as an example, take a look at the use of the data stream tool

@State

@state is designed for internal use within views, and Apple recommends using it with private to emphasize that the @state property is for internal use only within views. The @State variable’s memory is managed by SwiftUI, the framework allocates a chunk of memory for the variable, and the framework rerenders the view when the variable changes.

First of all, implement the following playback interface,

The code is as follows:

import SwiftUI

struct Episode {
    var title: String
    var showTitle: String
}

struct PlayerView : View {
    let episode = Episode(title: "WWDC 2019", showTitle: "Data Flow Throght SwiftUI")

    private var isPlaying: Bool = false

    var body: some View {
        VStack {
            Text(episode.title).foregroundColor(isPlaying ? .white: .gray)
            Text(episode.showTitle).font(.caption).foregroundColor(.gray)
            Button(action: {
                print("Hello WWDC 2019")
            }) {
                Image(systemName: isPlaying ? "pause.circle" : "play.circle")
            }
        }
    }
}
Copy the code

We can’t change the UI directly by taking the controls, we can only change the UI indirectly by modifying the properties. In the code above, isPlaying relies on the Text access to control the color of the title, as well as the button color to control the icon of the play button. If you want to change the value of isPlaying by clicking on the button, simply write self.isplaying. Toggle () in the button click event. The PlayerView is a structure, and the structure attributes cannot be modified according to Swift syntax, otherwise an error will be reported:

Cannot use mutating member on immutable value: ‘self’ is immutable

The right thing to do is to add @state on isPlaying,

struct PlayerView : View {
    let episode = Episode(title: "WWDC 2019", showTitle: "Data Flow")
    @State private var isPlaying: Bool = false  / / modify

    var body: some View {
        VStack {
            Text(episode.title).foregroundColor(isPlaying ? .white: .gray)
            Text(episode.showTitle).font(.caption).foregroundColor(.gray)
            Button(action: {
                self.isPlaying.toggle() / / modify
            }) {
                Image(systemName: isPlaying ? "pause" : "play")}}}}Copy the code

The scope of @state is this View, used with private. Changing the @state property refreshes the screen.

@binding (Shared Binding)

Binding is used when parent and child controls synchronize data. If we extract the play button as a separate control, we can change the code to look like this

struct PlayerView : View {
    let episode = Episode(title: "WWDC 2019", showTitle: "Data Flow Throght SwiftUI")
    @State private var isPlaying: Bool = false

    var body: some View {
        VStack {
            Text(episode.title).foregroundColor(isPlaying ? .blue : .red)
            Text(episode.showTitle).font(.caption).foregroundColor(.gray)
            PlayerButton(isPlaying: $isPlaying)
        }
    }
}

struct PlayerButton : View {
    @Binding var isPlaying: Bool

    var body: some View {
        return Button(action: {
            self.isPlaying.toggle()
        }) {
            Image(systemName: isPlaying ? "pause.circle" : "play.circle")
        }
    }
}
Copy the code

When creating a PlayerButton, use $before isPlaying to pass a dependency. Binding is used to show that a dependency is defined and does not hold parent control data. You can read and write to any variable with the @binding tag. Whenever the @Binding property of the child control is changed, the PARENT control’s UI is updated.

From the above example, we can see that SwiftUI has made great changes in data management and UIKit. We only need to follow SwiftUI’s data management principles to manage data well. The framework will do a good job in data-view synchronization, and developers only need to focus on business logic.

The figure above shows the SwiftUI event and data flow process. SwiftUI no longer requires the ViewController to host the View control and handle various events.

So far, we’ve only talked about data changes inside the View. @state modifies private properties inside the View. @Binding can declare a dependency on the @state property.

@ObservedObject

@State simply marks the internal State of the view. Normally, the UI is separated from the business code and data. In this case, you can use ObservableObject to transfer external data to the view.

3 things to do when using:

  • Data must be complied withObservableObjectClass type of.
  • In a custom data model, for properties that need to refresh the UI after changes@PublishedThe tag.
  • Inside the view@ObservedObjectThe symbol modifies an object instance.

Example:

class UserSettings: ObservableObject {
    @Published var score = 0
}
Copy the code

There isn’t much code in the model above because SwiftUI does a lot of things for us.

  • Comply with theObservableObjectAfter the protocol, the view can be refreshed when the properties of the instance change.
  • @PublishedSwiftUI: A view refresh is triggered when the Score property changes.

To use in view:

struct ContentView: View {
    @ObservedObject var settings: UserSettings
    
    var body: some View {
        VStack {
            Text("Your score is \(settings.score)")
            Button(action: {
                self.settings.score += 1
            }) {
                Text("Increase Score")
            }
        }
    }
}
Copy the code

@EnvironmentObject

We need to use @environmentobject when we need to pass the @observedobject property to multiple subviews, or when it is impractical to pass data far from the current view.


EnvironmentObject is like a global @state. The scope of the @environmentobject data is the entire View hierarchy and can be accessed anywhere in the View or subview using @environmentobject.

Example:

class GlobalState: ObservableObject {
    @Published var currentTopic: String = "Default GlobalState"
}

struct EnvironmentView: View {
    @EnvironmentObject var globalState: GlobalState
    
    var body: some View {
        VStack {
            Text("EnvironmentView")
            Text(globalState.currentTopic)
            
            HStack {
                EnvironmentOneView()
                EnvironmentTwoView()
            }
        }
    }
}

struct EnvironmentView_Previews: PreviewProvider {
    static var previews: some View {
        EnvironmentView().environmentObject(GlobalState())
    }
}



struct EnvironmentOneView: View {
    @EnvironmentObject var g: GlobalState
    
    var body: some View {
        VStack {
            Text("EnvironmentOneView")
            Text(g.currentTopic)
        }
    }
}

struct EnvironmentTwoView: View {
    @EnvironmentObject var globalState: GlobalState
    
    var body: some View {
        VStack {
            Text("EnvironmentTwoView")
            Text(globalState.currentTopic)
            Button("changeTopic") {
                globalState.currentTopic = "Changed Topic"
            }
        }
    }
}

Copy the code

@StateObject

This is a modifier added to WWDC 2020. Why add this modifier?

@state can only modify basic value types. For more complex internal data that needs to be class, you can only modify it with @observedobject. The @observedobject property exists in the View and is created multiple times as the View is created. There are cases where a View release causes @ObServedobject data to be lost.

@StateObject is the class version of @State and can be used to modify objects. The life cycles of @State and @StateObject properties are taken over by SwiftUI. The appearance of @stateObject solves the problem of using @observedobject that causes data loss.

Look at an example of @observedobject causing data loss:

struct StateObjectView: View {
    @State var showName: Bool = false
    
    var body: some View {
        VStack {
            Button("name:\(showName ? "Jaly" : "Null")") {
                showName.toggle()
            }
            StateSubview().padding()
        }
    }
}

struct StateSubview: View {
    @ObservedObject var settings: UserSettings = UserSettings()
    
    var body: some View {
        VStack {
            Text("Your score is \(settings.score)")
            Button(action: {
                self.settings.score += 1
            }) {
                Text("Increase Score")
            }
        }
    }
}
Copy the code

Click the name button to show and hide the name, click Increase Score to Increase the Score, now there is no problem, finally click name to hide the name, the Score does disappear. After clicking the Name button, the View refresh causes the StateSubview to be re-created. The Settings in StateSubview is managed by the View. The Settings will generate new data with each refresh, causing the previous data to be lost. Modifying Settings with @stateObject solves this problem.

You can see the difference in memory management between @StateObject and @Observedobject in the example above. The use of the decorator depends on the business scenario.

conclusion

Management Tool Features

  • @State
    • When the value changes, it will be reloadedViewDisplay the latest information;
    • Memory managed by SwiftUI, as long asViewProperties are kept in memory if they exist;
    • Can only be used for primitive data types, struct equivalent pass type, cannot be used for reference type;
    • Private, only originalViewIn use;
  • @Binding
    • Data source sharing between parent and child views, bidirectional binding, and generally only processing value types are accepted
  • ObservedObject
    • Use external class types (complyObservableObjectProtocol) instead of base type;
    • ViewManage your own memory;
    • Non-private, multipleViewData sharing between;
  • StateObject
    • The class version of @State, memory managed by SwiftUI, can be seen as a combination of @State and @observedobject
  • @EnvironmentObject
    • Global data binding mechanism, binding data can be arbitrarily accessed in View hierarchy

Using @state as an example, take a look at the implementation of the SwiftUI data management tool. SwiftUI uses @state to maintain status and automatically updates the UI when the status changes. Similar syntax is @binding, @@environment, etc.

The State definition:

@propertyWrapper
public struct State<Value> : DynamicProperty {
    public init(wrappedValue value: Value)
    public init(initialValue value: Value)
    public var wrappedValue: Value { get nonmutating set }
    public var projectedValue: Binding<Value> { get }
}
Copy the code

@state is an @propertyWrapper wrapper structure, and the @state property is converted to pseudocode like this:

@state private var isPlaying: Bool = false public var isPlaying: Bool {get {... Return $text.value // return a reference} set {$text.value = newValule notify(to: Swiftui) // Notify swiftui of data change... }}Copy the code

Create a dependency between the View and the data when the data is retrieved. If the @state property changes, the View can be redrawn.

@propertyWrapper is a feature of Swift 5.1, explained separately later.

In this paper,demo