• What is Observation?

    • 프로퍼티의 변화를 추적할 수 있는 새로운 Swift 기능

    • 모델을 만들어서 @Observable 매크로만 붙이면 SwiftUI에서 즉시 사용할 수 있다. 이 과정에서 프로퍼티 래퍼는 들어가지 않는다.

      @Observable class FoodTruckModel {    
          var orders: [Order] = []
          var donuts = Donut.all
      }
      
    • SwiftUI body가 실행되면 Observable 타입에서 사용된 모든 프로퍼티를 확인해서 이 변화를 추적하게 된다.

      • 이 코드에서는 body에서 donut만 참조하기 때문에 donut이 변하면 view가 재평가된다.

      • orders가 변할 때는 뷰가 재평가되지 않는다.

        @Observable class FoodTruckModel {    
          var orders: [Order] = []
          var donuts = Donut.all
        }
        
        struct DonutMenu: View {
          let model: FoodTruckModel
            
          var body: some View {
            List {
              Section("Donuts") {
                ForEach(model.donuts) { donut in
                  Text(donut.name)
                }
                Button("Add new donut") {
                  model.addDonut()
                }
              }
            }
          }
        }
        
      • 계산 프로퍼티도 값이 변경되면 뷰가 invalidate 된다.

        @Observable class FoodTruckModel {    
          var orders: [Order] = []
          var donuts = Donut.all
          var orderCount: Int { orders.count }
        }
        
        struct DonutMenu: View {
          let model: FoodTruckModel
            
          var body: some View {
            List {
              Section("Donuts") {
                ForEach(model.donuts) { donut in
                  Text(donut.name)
                }
                Button("Add new donut") {
                  model.addDonut()
                }
              }
              Section("Orders") {
                LabeledContent("Count", value: "\\(model.orderCount)")
              }
            }
          }
        }
        
  • SwiftUI property wrappers

    • 뷰가 자체적인 데이터를 저장할 모델이 필요한 경우 State를 써라

      • State와 StateObject가 통합된 것

        struct DonutListView: View {
            var donutList: DonutList
            @State private var donutToAdd: Donut?
        
            var body: some View {
                List(donutList.donuts) { DonutView(donut: $0) }
                Button("Add Donut") { donutToAdd = Donut() }
                    .sheet(item: $donutToAdd) {
                        TextField("Name", text: $donutToAdd.name)
                        Button("Save") {
                            donutList.donuts.append(donutToAdd)
                            donutToAdd = nil
                        }
                        Button("Cancel") { donutToAdd = nil }
                    }
            }
        }
        
    • 글로벌하게 전파되는 값을 사용하고 싶으면 Environment

      • Environment와 EnvironmentObject가 통합

        @Observable class Account {
          var userName: String?
        }
        
        struct FoodTruckMenuView : View {
          @Environment(Account.self) var account
        
          var body: some View {
            if let name = account.userName {
              HStack { Text(name); Button("Log out") { account.logOut() } }
            } else {
              Button("Login") { account.showLogin() }
            }
          }
        }
        
    • Binding을 만들고 싶으면 Bindable PropertyWrapper를 쓴다.

      • Observable 타입 모델을 그냥쓰면 binding을 만들 수 없기 때문에 binding이 필요할때만 선택적으로 쓴다.
      @Observable class Donut {
        var name: String
      }
      
      struct DonutView: View {
        @Bindable var donut: Donut
      
        var body: some View {
          TextField("Name", text: $donut.name)
        }
      }
      

    스크린샷 2023-06-14 오후 5.37.43.png

  • Advanced uses

    • SwiftUI는 인스턴스 단위로 필드 접근을 추적한다.

      • 즉, Observable인 타입을 포함하는 Array, Optional, 커스텀 타입등에도 모두 대응한다.

        @Observable class Donut {
          var name: String
        }
        
        struct DonutList: View {
          var donuts: [Donut]
          var body: some View {
            List(donuts) { donut in
              HStack {
                Text(donut.name)
                Spacer()
                Button("Randomize") {
                  donut.name = randomName()
                }
              }
            }
          }
        }
        
    • Observable에 대한 일반적인 규칙은 사용되는 프로퍼티가 바뀌면 뷰가 업데이트 된다는 것이다.

      • computed property도 해당 프로퍼티가 접근하는 stored property에 따라서 자동으로 갱신된다.
      • 하지만 stored property를 사용하지 않음에도 바뀌어야 하는 computed property에 대해서는 수동으로 Observation을 걸어줘야 할 수도 있다.
        • 이 때는 Observable 매크로가 프로퍼티에 대한 접근 경로를 만는 과정을 수동으로 해주면 된다.

          @Observable class Donut {
            var name: String {
              get {
                access(keyPath: \\.name)
                return someNonObservableLocation.name 
              }
              set {
                withMutation(keyPath: \\.name) {
                  someNonObservableLocation.name = newValue
                }
              }
            } 
          }
          
  • ObservableObject

    • 모델: ObservableObject 채택 없애고 Published떼고

      // as-is
      public class FoodTruckModel: ObservableObject {
      	@Published public var truck = Truck()
      	@Published public var orderes: [Order] = []
      	@Published public var donuts = Donut.all
      
      	var dailyOrderSummaries: [City.ID: [OrderSummary]] = [:]
      	var monthlyOrderSummaries: [City.ID: [OrderSummary]] = [:]
      	// ...
      }
      
      // to-be
      @Observable public class FoodTruckModel {
      	public var truck = Truck()
      	public var orderes: [Order] = []
      	public var donuts = Donut.all
      
      	var dailyOrderSummaries: [City.ID: [OrderSummary]] = [:]
      	var monthlyOrderSummaries: [City.ID: [OrderSummary]] = [:]
      	// ...
      }
      
    • 뷰

      // as-is
      struct AccountView: View {
       @ObservableObject var model: FoodTruckModel
      
       @EnvironmentObject private var accountStore: AccountStore
       @Environment(\\.authorizationController) private var authorizationController
      
       @State private var isSignUpSheetPresented = false
       @State private var isSignOutAlertPresented = false
      
        // ...
      }
      
      // to-be
      struct AccountView: View {
       var model: FoodTruckModel
      
       @Environment(AccountStore.self) private var accountStore
       @Environment(AuthorizationController.self) private var authorizationController
      
       @State private var isSignUpSheetPresented = false
       @State private var isSignOutAlertPresented = false
      
        // ...
      }