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 class Donut {
var name: String
}
struct DonutView: View {
@Bindable var donut: Donut
var body: some View {
TextField("Name", text: $donut.name)
}
}
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에 대한 일반적인 규칙은 사용되는 프로퍼티가 바뀌면 뷰가 업데이트 된다는 것이다.
이 때는 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
// ...
}