Data flow in SwiftUI

SwiftUI is a whole new way to develop Apple apps. This time, we'll explore the data flow and lifecycle in SwiftUI. There are WWDC19 and WWDC20 videos from Apple about SwiftUI dataflow. Apple's official documentation for SwiftUI dataflow also exists.

Lifecycle in SwiftUI

In SwiftUI, there are only two functions that represent the state of a View, namely, its lifecycle. Instead, there are various property wrappers that indicate the state, which can respond to different states of the data flow.

.onAppear {
    print("View appeared")
}

.onDisappear {
    print("View disappeared")
}

@State

In general, structs are value types, so the values in the struct cannot be changed. SwiftUI provides @State to allow you to change values within a struct.

Views in SwiftUI are structs, which are destroyed or recreated at any time. That's why we use @State to create persistently mutable variables. However, @State is recommended only for value types such as String, Int, and Bool. Typically, @State variables are declared private and are not shared with other views. If you want to share values with other views, you can use @Binding or @ObservedObject.

struct ContentView: View {
  @State private var number = 0
}

@Binding

@Binding allows bidirectional binding of values such as @State of the parent view. In the code below, isPresented binds showAddView to change the value.

struct ContentView: View {
  @State private var showAddView = false

  var body: some View {
    VStack {
      Text("Hello World.")
    }
  }
  .sheet(isPresented: $showAddView) {
    AddView(isPresented: self.$showAddView)
  }
}

struct AddView: View {
  @Binding var isPresented: Bool

  var body: some View {
    Button("Dismiss") {
      self.isPresented = false
    }
  }
}

ObservableObject

ObservableObject is a Protocol and is part of the Combine framework. To use it, just follow the Protocol and use @Published. Using @Published lets the View know that a variable's value has been added or deleted. ObservableObject is a good protocol to apply to the ViewModel of the MVVM architecture.

class MyViewModel: ObservableObject {
  @Published var dataSource: MyModel
  
  init(dataSource: MyModel) {
    self.dataSource = dataSource
  }
}

@StateObject

At WWDC 2020, Apple further revealed @StateObject. It works in much the same way as @ObservedObject. However, when SwiftUI re-renders the View, it prevents it from being canceled.

struct ContentView: View {
  @StateObject var user = User()
}

@ObservedObject

SwiftUI allows views to detect external objects via @ObservedObject. In the code below, the User class conforms to ObservableObject and has a @Published variable. @ObservedObject the user variable holds this User class object. SwiftUI refreshes the view when the value of the @Published variable of these user objects changes.

class User: ObservableObject {
  @Published var name = "Hohyeon Moon"
}

struct ContentView: View {
  @ObservedObject var user = User()

  var body: some View {
    VStack {
      Text("Your name is \(user.name).")
    }
  }
}

@EnvironmentObject

@EnvironmentObject is usually used for data shared across apps. @EnvironmentObject can pass values through .environmentObject(). The object you pass must conform to the ObservableObject protocol. As in the code below, if you provide a root view, it can be used in any view.

// MySettings.swift
class Settings: ObservableObject {
  @Published var version = 0
}

// SceneDelegate.swift
var settings = UserSettings() 
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(settings))

// MyViews.swift
struct ContentView: View {
  @EnvironmentObject var settings: UserSettings

  var body: some View {
    NavigationView {
      VStack {
        Button(action: {
          self.settings.version += 1
        }) {
          Text("Increase version")
        }

        NavigationLink(destination: DetailView()) {
          Text("Show Detail View")
        }
      }
    }
  }
}

struct DetailView: View {
  @EnvironmentObject var settings: UserSettings

  var body: some View {
    Text("Version: \(settings.version)")
  }
}

Overall

Image by Chris Eidhof at objc.io

swiftui-data-flow