What is Observation ?

1

기본 데이터가 변경될 때 프레젠테이션을 업데이트하는 반응형 앱을 만듭니다.

@Obserable 은 normal type의 property를 observable type으로 마킹하여 UI가 data의 변화에 대응할 수 있게 해주는 매크로

@Obserable 은 Swift 컴파일러에게 명령을 내려서 코드를 확장형 Observable type으로 바꿔 SwiftUI가 뷰를 동작하게 만듬

reference type, value type을 모두 지원 

@Published @State @Binding @ObservedObject 등 다른 프로퍼티 래퍼가 없어도 작동함

각 property마다 @Published @State @Binding @ObservedObject 등을 붙인것을 data model에 붙인다고 이해하면 됨

// Before
class FoodTruckModel {    
    var orders: [Order] = []
    var donuts = Donut.all
}

// After
@Observable class FoodTruckModel {    
    var orders: [Order] = []
    var donuts = Donut.all
}

Examples

SwiftUI는 body가 실행될 때 Observable 타입에서 사용된 property의 모든 접근을 추적함

이 추적 정보를 이용해 특정 인스턴스에서 property의 다음변화가 언제 일어날지를 예측

DonutMenu의 body에서 model.donuts에 접근이 이루어져 SwiftUI가 donuts를 추적

도넛 추가 버튼을 클릭해 donuts가 변경되면 DonutMenu View가 무효화 되고 변경사항에 맞춰 UI가 업데이트 됨

orders가 변경되었을 때는 DonutMenu View가 무효화 되지 않음

view의 body에서 접근하지 않고 있어 SwiftUI가 추적하지 않는 property이기 때문

연산 프로퍼티에도 같은 규칙이 적용 됨 프로퍼티가 변하면 UI는 업데이트

@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)")
      }
    }
  }
}

위의 코드에서 FoodTruckModel에 orderCount라는 연산 프로퍼티를 추가

view의 body에서 orderCount에 접근하여 orderCount를 추적

orderCount는 orders에 접근했으므로 orders가 변경되면 view가 업데이트

@Observable

  • @Observable 매크로를 사용하면 타입이 확장되어 Observation을 지원
  • SwiftUI는 프로퍼티의 접근을 추적, Observation에서 해당 프로퍼티가 언제 변할지 관찰 가능
  • 추적이 가능해지면 프로퍼티가 변할때 UI는 view의 body를 재계산 하면 끝

SwiftUI property wrappers

SwiftUI의 핵심 프로퍼티 래퍼 3가지는 State, Environment, Bindable

@Observable에서 SwiftUI의 프로퍼티 래퍼가 필요한 경우

@State

model안에 view 전용 상태를 저장해야할 때 사용

donutToAdd는 소속된 donutListView의 view의 수명 동안만 관리

@Environment

Environment는 각 값을 어디에서든 접근 가능하게 만들어 줌 → Global로 만들어 준다

observable 타입이 Environment와 잘 맞는데 observable은 접근을 추적하여 업데이트 하기 때문에 여러곳에서 공유하는 Environment와 잘 맞음

FoodTruckMenuView의 body를 불러올 때 Observable 타입의 Account의 userName에 접근하게 됨

userName이 변경되면 FoodTruckMenuView의 body도 업데이트

@Bindable

가장 최근에 생긴 프로퍼티 래퍼 굉장히 가볍다

해당 타입으로부터 바인딩이 생성되게 하는 기능만 있음

래핑된 Bindable 프로퍼티에서 바인딩을 만드는 방법은 굉장히 쉬운데 $구문을 사용해 프로퍼티에 바인딩을 만들면 됨

모델이 view로 있어야 한다면 → @State

모델이 전역(Global)으로 있어야 한다면 → @Environment

모델에 바인딩만 필요하다면 → @Bindable

셋 다 아니라면 var

Advanced uses

SwiftUI는 프로퍼티 접근을 인스턴스마다 추적하기 때문에 Observable 모델이 포함된 어떤 타입도 사용될 수 있음

Example


DonutList는 donuts 배열을 가지고 있는데 이 배열은 모두 @Observable

Donut의 name이 하나라도 변경되면 SwiftUI는 언제 뷰를 무효화할지를 파악하기 위해 해당 프로퍼티에 일어난 접근을 포착하여 추적함

Observable의 기본 규칙은 사용중인 프로퍼티가 변경되면 뷰도 업데이트 된다는 것

단 예외도 존재하는데 연산 프로퍼티에 사용된 저장 프로퍼티가 없다면 Observation에서 사용되기 전에 2단계를 더 거쳐가게 됨

이는 관찰 대상인 프로퍼티가 observable 타입에 저장된 구성에 의해 변경되지 않았을 때만 필요한 조치임

이 경우엔 프로퍼티에 접근과 변경이 언제 이루어졌는지 Observation에 전달해주기만 하면 됨

Manual Observation

access point를 수동으로 고쳐서 someNonObservableLocation의 name을 저장하게 해줌

이러한 직접 구현이 필요한 경우는 드문데 왜냐하면 보통 이런 모델의 프로퍼티는 다른 저장 프로퍼티처럼 구성되기 때문

하지만 고급 기능이 필요한 경우엔 직접 구현하면 됨

Computed properties (연산 프로퍼티)

  • SwiftUI는 구성의 변화를 바로 인식하는데 프로퍼티 접근을 바탕으로 Observable 타입을 추적하기 때문
  • 연산 프로퍼티에 다른 저장 프로퍼티를 사용하여도 Observation이 잘 작동하게 됨
  • 하지만 드물게 Observation이 작동하지 않으면 직접 구현하여 프로퍼티의 접근과 변경이 언제 일어나는지만 알려주면 됨

@ObservableObject

@ObservableObject → @Observable

@ObservableObject 에서 @Observable 로 넘어가려면 대부분의 어노테이션을 삭제하거나

@State @Environment @Bindable 이 세가지로 단순화하면 됨

// Before
class FoodTruckModel: ObservableObject {
    @Published var truck = Truck()

    @Published var orders: [Order] = []
    @Published var donuts = Donut.all

    var dailyOrderSummaries: [City.ID: [OrderSummary]] = [:]
    var monthlyOrderSummaries: [City.ID: [OrderSummary]] = [:]
    ...
}

struct AccountView: View {
    @ObservableObject var model: FoodTruckModel

    @EnvironmentObject private var accountStore: AccountStore
    @Environment(\.authorizationController) private var authorizationController

    @State private var isSingUpSheetPresented = false
    @State private var isSingOutSheetPresented = false
    ...
}

// After
@Observable
class FoodTruckModel {
    var truck = Truck()

    var orders: [Order] = []
    var donuts = Donut.all

    var dailyOrderSummaries: [City.ID: [OrderSummary]] = [:]
    var monthlyOrderSummaries: [City.ID: [OrderSummary]] = [:]
    ...
}

struct AccountView: View {
    var model: FoodTruckModel

    @Environment(AccountStore.self) private var accountStore: AccountStore
    @Environment(AuthorizationController.self) private var authorizationController

    @State private var isSingUpSheetPresented = false
    @State private var isSingOutSheetPresented = false
    ...
}

@ObservableObject와 @Observable 차이점

코드가 간결해짐

observable에서는 대부분의 어노테이션이 필요가 없어지기 때문

옵셔널 가능

observable은 옵셔널 가능

observableObject 은 옵셔널 불가능

// ObservableObject
class FoodTruckModel: ObservableObject {
    ...
}

struct AccountView: View {
    @ObservableObject var model: FoodTruckModel? // 컴파일 에러
}

// Observable
@Observable
class FoodTruckModel {
    ...
}

struct AccountView: View {
    var model: FoodTruckModel? // 가능
}

성능 향상

ObservableObject 에서는 published property 가 변하면 뷰의 body에서 해당 프로퍼티를 읽고 있지 않아도 다시 그려짐

final class Food: ObservableObject {

    let name = "Food Name"
    @Published var isAvailable = true

    var cancellable: Cancellable?

    func connectTimer() {
        cancellable = Timer.publish(every: 0.1, on: .main, in: .default)
            .autoconnect()
            .sink { [weak self] _ in
                self?.isAvailable.toggle()
            }
    }
}

struct FoodMenuView: View {
    @ObservedObject var food: Food

    var body: some View {
        Text(food.name)
            .background(.random)
            .onAppear {
                food.connectTimer()
            }
    }
}

FoodMenuView의 body에서 isAvailable를 접근하고 있지 않음에도 isAvailable가 변경되면 뷰가 계속 업데이트 됨

Observable은 뷰의 body에서 프로퍼티를 읽어야 업데이트 되기 때문에 불필요한 업데이트가 일어나지 않아 성능이 향상

@Observable
final class Food: ObservableObject {

    let name = "Food Name"
    var isAvailable = true

    var cancellable: Cancellable?

    func connectTimer() {
        cancellable = Timer.publish(every: 0.1, on: .main, in: .default)
            .autoconnect()
            .sink { [weak self] _ in
                self?.isAvailable.toggle()
            }
    }
}

struct FoodMenuView: View {
    var food: Food

    var body: some View {
        Text(food.name)
            .background(.random)
            .onAppear {
                food.connectTimer()
            }
    }
}

FoodMenuView의 body에서 isAvailable를 접근하고 있지 않기 때문에 isAvailable가 변경되어도 뷰가 업데이트 되지 않음

참고

https://developer.apple.com/videos/play/wwdc2023/10149/

https://developer.apple.com/videos/play/wwdc2023/10166

https://developer.apple.com/videos/play/wwdc2023/10167

https://developer.apple.com/documentation/observation

https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app

https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro

https://eunjin3786.tistory.com/580

https://eunjin3786.tistory.com/582

[WWDC - 2021] Demystify SwiftUI ‐ Identity (1/3)
[WWDC - 2021] Demystify SwiftUI ‐ Lifetime (2/3)
[WWDC - 2021] Demystify SwiftUI ‐ Dependency (3/3)

🤔 SwiftUI는 우리의 코드에서 어떤걸 볼까 ?

Identity

  • SwiftUI가 여러번의 업데이트 동안 요소(element) 가 동일한것인지 다른것인지 인식(recognize) 하는 방법

Lifetime

  • SwiftUI가 뷰와 데이터의 존재(existence) 를 시간에 따라 추적하는 방법

Dependency

  • SwiftUI가 인터페이스를 업데이트해야하는 시기(when) 와 이유 (why) 를 이해하는 방법

이 세 가지 개념을 통해 SwiftUI는 변경해야할 사항(what needs to change), 시기(when), 방법(how)결정할 수 있어 결과적으로 동적인 사용자 인터페이스가 화면에 표시

How SwiftUI updates the UI ?

The goal is to give you a better mental model for how to structure SwiftUI code.

Dependency

  • dependency는 단순히 view에 대한 input일 뿐임
  • dependency가 변경되면 view는 새로운 body를 생성해야 함
  • body는 뷰 계층 구조를 생성하는 곳
  • action은 뷰의 dependencies에 대한 변경의 trigger

위 코드를 도식화하면 아래와 같음

버튼을 탭하면 강아지에게 보상을 주는 액션이 전달됨

그 결과 강아지는 변화가 생김

dependecy가 변화되었기 때문에 DogView는 새로운 body를 그려야 함

 

 

뷰 계층 구조에 초점을 맞춰 그래프를 조금 더 단순화 하여 확인해보면 DogView에 dependecy를 추가해도 여전히 tree로 보임

 

DogView뿐 아니라 다른곳에서도 dependecy가 추가될 수 있음 SwiftUI는 자체 dependecy를 가질 수도 있음

 

또한 아래처럼 동일한 state나 data에 다수의 view가 의존할 수 도 있음

 

 

아직까진 tree처럼 보이지만 선이 겹치는 것을 피하기 위해 재배치 해보면 실제론 tree가 아니라 greaph 임을 알 수 있음

이 구조를 dependecy graph 라고 부름

이 구조는 SwiftUI가 새로운 body가 필요한 view만 효율적으로 업데이트 할 수 있도록 해주기 때문에 중요함

 

 

가장 하단의 dependency를 예로 들면 이 dependency에 영향을 받는 view는 2개가 있음

dependency graph의 비밀은 dependency가 변경되면 dependency에 영향을 받는 뷰만 무효화

SwiftUI는 무효화된 각 view의 body를 호출하여 각 view에 대한 새로운 body value를 인스턴스

이로 인해 무효화된 view와 연관된 더 많은 dependency가 변경될 수 있지만 항상 그런 것은 아님

SwiftUI에서 view는 value type이기 때문에 이를 효율 적으로 비교하여 view의 올바른 하위 집합만 업데이트

view의 value(struct)는 수명이 매우 짧아 값 비교에만 사용되지만 view 자체 수명은 이보다 더 김


Identity is the backbone of the graph

  • identity는 dependecy graph의 핵심(backbone)
  • 모든 view는 명시적(Explicit) 또는 구조적(Structural) identity를 가지고 있음
  • 이 identity를 통해 SwiftUI는 변경사항을 올바른 View에 전달하고 UI를 효율적으로 업데이트 할 수 있음

 

identity의 활용을 향상시키는 방법

Explicit Identity 사용 시 고려사항

Identifier의 안정성(stablilty)

뷰의 수명은 id의 지속시간이므로 identity의 안정성(stablilty)이 중요함

안정성이 강한 identity를 갖게 되면 SwiftUI는 view에 대한 storage를 지속적으로 생성하거나
dependency graph를 업데이트 하지 않아도 되기 때문에 성능(performance) 향상에 도움

또한 SwiftUI는 identity를 사용하여 persisted storage를 관리하기 때문에 state 손실 방지에도 도움

하지만 안정성이 좋다고 해서 유일한 것은 아님

Identifier의 고유성(uniqueness)

좋은 identity의 또 다른 조건은 고유성(uniqueness)

즉, 각각의 identity는 단 하나의 뷰(single view)에만 매핑 되어야 함 (1:1 매치)

 

Example of Explicit identity

이 코드에는 버그가 있는데 바로 id를 UUID()로 초기화 해주는 것임

데이터가 변경될때마다 새로운 id를 얻기 때문에 안정적이지 않음

 

 

index로 바꾸어도 안정적이지 않음 왜냐하면 index는 데이터의 추가나 삭제에 의해 변경될 수 있기 때문

 

 

그렇기 때문에 database에서 가져온 것처럼 안정적인 속성에서 파생된 안정적인 id를 사용해야 함

 

 

name은 unique하지 않기 때문에 id로는 적합하지 않음

 

 

serialNumber 같은 unique한 id를 사용하는 것이 더 나은 애니메이션과 퍼포먼스를 보장

 

Explicit identity의 주의점

  • 계산 속성에서 random identifier(ex: UUID())를 사용할 때는 주의
  • 일반적으로 모든 identity는 안정적이기를 원함
  • identity는 시간이 지나도 변경되어선 안됨
  • 새로운 identity는 새로운 lifetime을 가지는 새로운 item을 보여줌
  • identity는 고유해야함 identity를 공유하면 안됨

SwiftUI는 이러한 속성을 사용하여 버그없이 원할하게 실행되도록 함

 

Structural Identity 사용 시 고려사항

Example of Structural Identity

if 구문을 통해 분기 처리되었기에 content는 구조적으로 각각 다른 id를 가진 2개의 view로 취급함

하지만 단일 id를 가지는 하나의 content로 처리하고 싶다면 ??

 

 

modifier 내에서 컨디션에 따라 값을 변경하여 branch를 제거 해줌

 

 

참고로 결과값이 false면 불투명도가 1이기 때문에 아무 변화도 일어나지 않는데 이러한 modifier을 비활성화 수정자(Insert modifier)라고 부름

SwiftUI의 modifier은 저렴하기 때문에 Inser modifier의 비용은 거의 없다고 봐도 무방함

 

Structural Identity의 주의점

  • branch를 불필요하게 사용하면 성능 저하, 애니메이션 오류, state 손실 등이 발생
  • branch 도입 시에는 여러 view를 나타내는지 아니면 단일 view의 다른 state를 나타내는지 것인지 고려
  • 단일 view를 나타내는 것이라면 보통은 Insert modifier을 사용하는 것이 더 나음

참고

https://developer.apple.com/videos/play/wwdc2021/10022

[WWDC - 2021] Demystify SwiftUI ‐ Identity (1/3)
[WWDC - 2021] Demystify SwiftUI ‐ Lifetime (2/3)
[WWDC - 2021] Demystify SwiftUI ‐ Dependency (3/3)

🤔 SwiftUI는 우리의 코드에서 어떤걸 볼까 ?

Identity

  • SwiftUI가 여러번의 업데이트 동안 요소(element)가 동일한것인지 다른것인지 인식(recognize)하는 방법

Lifetime

  • SwiftUI가 뷰와 데이터의 존재(existence)를 시간에 따라 추적하는 방법

Dependency

  • SwiftUI가 인터페이스를 업데이트해야하는 시기(when)와 이유(why)를 이해하는 방법

이 세 가지 개념을 통해 SwiftUI는 변경해야할 사항(what needs to change), 시기(when), 방법(how)결정할 수 있어 결과적으로 동적인 사용자 인터페이스가 화면에 표시

Lifetime

  • 지난번 학습을 통해 SwiftUI가 identity를 사용하여 뷰를 식별하는 방법을 이해
  • 이번엔 identity가 뷰와 데이터의 lifetime과 어떻게 연결되는지를 학습

State(Value)는 identity가 아니다

테세우스라는 이름(identity)을 가진 고양이가 있을 때 상태(State)가 변할 수 있음

시간에 따라 테세우스는 졸고 있을 수도, 깨어 있을 수도 있고, 짜증을 낼 수도있음 (State가 변화)

상태가 변화해도 테세우스는 테세우스임

Why? State(Value)가 변화하는거지 identity가 변하는 것이 아니기 때문

이것이 view의 identity와 lifetime을 연결하는 본질(essence)

 

 

  • 테세우스 처럼 view는 identity가 동일하다면 state(value)가 달라도 동일한 view로 인식
  • identity를 사용하면 시간에 따라 변화하는 다양한 Value를 안정적으로 정의 가능
    • 연속성을 가진다

View value ≠ View Identity

처음 body가 실행되어 PurrDecibelView(intensity: 25) 가 생성되고 나중에 PurrDecibelView(intensity: 50) 이 생성되면 SwiftUI는 뷰가 변경되었는지 비교하기 위해 값의 복사본을 유지한 뒤 비교한 후에 소멸 시킴

 

 

여기서 주목해야할 점은 view value와 view identity는 다르다는 것

view value는 일시적이기 때문에 view lifetime에 의존해선 안됨

하지만 identity는 컨트롤 가능함 How?

 

A view’s lifetime is the duration of the identity

 

view가 처음 생성되고 onAppear될때 SwiftUI는 identity를 할당 (Explicit or Structural)

시간에 따라 view에 새로운 value들이 생성될 수 있지만(state의 변경) identity는 동일하기 때문에 SwiftUI는 동일한 view로 인식

view의 lifetime이 종료될때는 identity가 바뀌거나 view가 제거될 때

결과적으로 view의 lifetime은 view의 identity 지속시간이 됨

 

State lifetime = View lifetime

SwiftUI가 view에서 @state, @stateObject 를 보면 해당 데이터를 view의 lifetime동안 유지해야한다는것을 인식

@state, @stateObject 는 view의 identity와 연결된 영구적인 저장소(persistent storage)

view의 ID가 처음 생성될때 SwiftUI는 @state, @stateObject 의 초기값을 사용하여 메모리에 저장소(storage)를 할당

 

 

title state 를 예시로 보면 SwiftUI는 storage가 변경되어 view의 body가 재호출(re-evaluated)되어도 view의 lifetime동안 storage를 유지(persist)

 

 

지난 시간 학습을 통해 위 예제 코드에는 두개의 뷰에 다른 Structural Identity가 할당됨

지난번에는 이것이 애니메이션에 어떤 영향을 미치는지에 대해서만 학습했지만 이것은 state 지속성에도 영향을 미침

 

 

실제로 body가 처음 동작하게 되면 실제 분기에서 dayTimeTrue 일때 SwiftUI는 초기값을 사용해 영구 저장소를 할당함

SwiftUI는 view의 lifetime동안 다양한 작업에 의해 state가 바뀌어도 이 저장소를 유지함

 

 

하지만 dayTimeFalse 로 변경되어 분기가 바뀌게 되면 SwiftUI는 다른 identity를 가진 view라고 인식하게 됨

SwiftUI는 다른 view라고 인식했기 때문에 초기값을 사용하여 새로운 영구저장소를 할당하고 기존에 있던 영구 저장소는 할당 취소 됨

 

 

하지만 다시 원래 분기로 돌아간다면 ?? 아까와 같은 상황이 발생

다른 identity이므로 초기값을 사용하여 새로운 저장소를 할당하고 기존의 저장소를 해제

즉 identity가 바뀔때마다 state는 대체(replace)된다는 것

 

identity가 변경되면 state(storage)가 교체(replaced)

identity는 view의 lifetime에 연관되어 있기 때문에

state의 persistence는 view의 lifetime와 연관이 있다는 것

Lifetime

  • view value는 일시적이기 때문에 view value의 lifetime에 의존하면 안됨
  • view의 lifetime이 view의 identity의 지속시간임
  • view의 identity를 제어할 수 있기 때문에 identity를 사용하여 state lifetime의 범위를 명확히 지정 가능
  • SwiftUI는 데이터 기반 components에 대해 Identifiable 프로토콜을 최대한 활용하기 때문에 안정적인 identifier를 선택하는것이 매우 중요
  • A view's value is short-lived
  • A view's lifetime is the duration of its identity
  • Persistence of state is tied to lifetime
  • Provide a stable identity for your data

참고

https://developer.apple.com/videos/play/wwdc2021/10022

[WWDC - 2021] Demystify SwiftUI ‐ Identity (1/3)
[WWDC - 2021] Demystify SwiftUI ‐ Lifetime (2/3)
[WWDC - 2021] Demystify SwiftUI ‐ Dependency (3/3)

 

 

🤔 SwiftUI는 우리의 코드에서 어떤걸 볼까 ?

Identity

  • SwiftUI가 여러번의 업데이트 동안 요소(element)가 동일한것인지 다른것인지 인식(recognize)하는 방법

Lifetime

  • SwiftUI가 뷰와 데이터의 존재(existence)를 시간에 따라 추적하는 방법

Dependency

  • SwiftUI가 인터페이스를 업데이트해야하는 시기(when)와 이유(why)를 이해하는 방법

이 세 가지 개념을 통해 SwiftUI는 변경해야할 사항(what needs to change), 시기(when), 방법(how)결정할 수 있어 결과적으로 동적인 사용자 인터페이스가 화면에 표시

Identity

 

이 2개의 강아지 발바닥 뷰는 서로 다른 뷰(Different Views) ?

아니면 단지 위치와 배경색만 바뀐 하나의 뷰(Same View) ?

이러한 구별은 인터페이스가 한 State에서 다른 State로 전환되는 방식이 달라지기 때문에 매우 중요

만약 서로 다른 뷰(Different Views)라면? fade in/out 과 같은 독립적인 방식으로 전환

하지만 사실은 서로 같은 뷰(Same View)라면 ? 동일한 뷰이기 때문에 현위치에서 다른 위치로 화면을 가로질러 이동하는 방식으로 전환

SwiftUI는 View Identity에 따라 한 State에서 다른 State로 인터페이스가 전환되는 방식이 달라지게 되는데
이것이 View Identity의 핵심(key concept)




Type of Identity

  • Explicit Identity
    • 사용자 정의 또는 데이터 기반 identity를 사용
  • Structural Identity
    • view hierarchy 에서 타입 및 위치에 따라 뷰를 구분

Explicit Identity

Explicit Identity 의 한가지 예시는 UIKitAppKit에서 사용중인 Pointer identity

UIViewNSViewReference TypeClass 이기 때문에 각각 메모리에 할당에 대한 고유한 포인터가 있음

포인터를 사용하여 개별 뷰를 참조할 수 있으며 여려 뷰가 동일한 포인터를 공유하는 경우 실제로 같은 뷰임을 보장

 

 

하지만 SwiftUIViewValue TypeStruct 이기 때문에 고유한 포인터가 없음

SwiftUIUIKit 과 동작하는 방식이 다른 이유

SwiftUI에서 View 를 Value Type으로 표현하는 이유

  • Not allocated, no pointers
  • Efficient memory representation
  • Supports small, single-purpose components

 

 

대신 SwiftUI는 다른 형태의 Explicit Identity 를 사용

아래의 예시에서 사용된 dogTagID 매개변수는 Explicit Identity의 형태 중 하나로 List에서 View를 명시적으로 식별하는데 사용

List가 변경되면 해당 ID를 사용하여 무엇이 바뀐지를 파악하고 올바른 애니메이션을 생성

 

 

 

HeaderView.id(_ :) modifier을 사용해서 custom identifier 설정도 가능

여기서 주목해야 할 것은 모든 뷰를 명시적으로 식별할 필요가 없고 HeaderView 처럼 다른곳에서 참조해야 하는 뷰만 식별하면 된다는 것

 

 

 

다른곳에서 참조해야하는 HeaderViewExplicit Identity가 필요

 

 

다른곳에서 참조할 필요가 없는 ScrollViewReader ScrollView Text ButtonExplicit Identity가 필요 없음

 

 

명시적이지 않았다고 해서 그것이 뷰의 identity가 없다는 것을 의미하는 것은 아님

모든 뷰는 explicit identity 아니더라도 identity가 존재

여기서 등장하는 것이 바로 Structural Identity

SwiftUI는 view hierarchy를 사용하여 뷰에 대한 암시적 ID(implicit Identity)를 생성하기 때문에 사용자가 아이디를 생성할 필요가 없음

 

Structural Identity

Structural Identity상대적인 위치에 따라 결정

SwiftUI는 API 전반에서 Structural Identity 를 사용하는데 대표적인 예는 View 코드 내의 if문 처럼 조건부 로직을 사용하는 경우

아래 예시 처럼 조건문의 구조는 각 뷰를 식별하는 명확한 방법을 제공

True 일 때는 항상 AdoptionDirectoryFalse 일 때는 항상 DogList가 표시

즉 유사하게 보이더라도 위치에 따라 어느 뷰인지 판별 가능

 

 

 

하지만 이 방법은 view들이 자리를 변경하지 않고(swap) 계속해서 같은 자리에 있음을(stay) SwiftUI가 정적으로 보장 받을 수 있을 때만 작동

SwiftUI는 뷰 계층 구조(view hierarchy)의 type 구조를 살펴봄으로서 이를 수행

SwiftUI가 뷰를 볼때는 위 코드는 참/거짓 content 정보를 가지는 하나의 generic type_ConditionalContent<TrueContent, FalseContent> 로 변환

이 변환은 Swift의 result Builder type인 ViewBuilder 에 의해 수행

view protocol 에서는 body property 를 암시적으로 ViewBuilder 에 래핑하기 때문에 우리가 따로 명시하지 않아도 됨

 

generic type_ConditionalContent<TrueContent, FalseContent> 를 사용하여 SwiftUI는 true일때는 항상 AdoptionDirectoryfalse일 때는 항상 DogList 라는 것을 보장

안정적으로 암시적 identity를 할당

이로 인해서 SwfitUI는 if문의 분기가 고유한 id를 가지는 다른 뷰를 나타내는 것을 이해하기 때문에

if문일 때는 서로 다른 id를 가지는 다른 뷰(Different Views)이기 때문에 fade in/out 과 같은 독립적인 방식으로 전환

동일한 뷰(Same View)일때는 일관된 ID로 단일 뷰를 수정하기 때문에 현위치에서 다른 위치로 화면을 가로질러 이동하는 방식으로 전환

위의 두 방법 모두 작동하지만 SwiftUI는 일반적으로 두번째 방식인 단일ID를 유지하고 유연한 전환을 제공하는 방식을 권장

이는 뷰의 수명과 상태를 보존하는데 도움이 되기 때문

 

Evil nemesis AnyView

AnyView가 뷰의 구조에 미치는 영향

분기 마다 서로 다른 종류의 뷰를 return하기 때문에 함수의 return Type을 통일하기 위해 AnyView로 래핑

AnyView로 래핑되면 함수의 return Type을 AnyView로 간주하기 때문에 SwiftUI는 조건부 구조를 확인할 수 없게 됨

 

 

AnyView가 “type-erasing wrapped type” 라 불리는 이유

AnyView는 generic signature에서 view의 type을 숨김

또한 코드의 가독성이 떨어지게 됨

AnyView 를 언래핑하게되면 컴파일 에러가 발생하는데 SwiftUI가 단일 return type을 요구하기 때문

 

 

이러한 오류를 피할려고 하면 ?

View protocol 이 암시적으로 ViewBuilder 에 래핑하기 때문에 body property 가 특별하다는것을 기억하면 됨

Swift는 기본적으로 help functionViewBuilder 로 추론하지 않지만 명시적으로 ViewBuilder 속성을 적용 가능

이를 통해 경고나 오류 없이 AnyView 언래핑 가능

 

type signature의 결과를 보면 _ConditionalContent 트리를 통해 SwiftUI에게 ID 제공

 

 

switch문을 사용하여 코드 가독성을 더욱 높일 수 있음

 

AnyView 사용을 지양해야 하는 이유

  • 코드 가독성 저하
  • AnyView는 컴파일러에서 static type 정보를 숨기기 때문에 도움이 되는 에러나 경고 파악 불가
  • 퍼포먼스 저하

AnyView 대신 generic 사용을 지향

참고

https://developer.apple.com/videos/play/wwdc2021/10022

[WWDC - 2019] Data Flow Through SwiftUI - @State, @Binding
[WWDC - 2020] Data Essentials in SwiftUI - @StateObject, @ObservableObject, @EnvironmentObject

요약

  • SwiftUI는 View와 View의 State관리가 중요
  • SwiftUI는 @State 를 통해 Single Source of Truth(SOT)를 나타낼수 있음
  • SwiftUI는 @State 로 선언된 상태 프로퍼티의 저장소를 관리
  • @Binding 을 통해 Single Source of Truth(SOT)에 접근 가능
  • @Binding 을 할때는 @State 로 선언된 상태 프로퍼티의 이름에 prefix로 $표시를 추가해 사용
  • SwiftUI는 State프로퍼티가 변하면 State프로퍼티가 포함된 View 계층구조의 일부 View들을 업데이트

SwiftUI가 가장 중요하게 생각하는 것

  • Single Source of Truth(SOT) 에 의한 상태(State) 관리
  • UIKit에서는 데이터가 여기저기서 주고 받아지고, 그렇게 받아진 데이터를 여기저기서 수정. 결국에는 어떤게 진짜인지 헷갈리고 복사된 데이터 값들이 다 다르다 보니 버그에 취약해지기 때문에 개발자가 신경써야할 게 많았음. SwiftUI에선 그런 고민들을 시스템에서 다 처리해주자 !

State, Binding은 Value Type을 위한 프로퍼티 래퍼

  • 지난 글을 통해 배운 @State, @Binding 이 두가지의 프로퍼티 래퍼는 Value Type을 위한 것
  • struct, enum, Int, Bool 등

그렇다면 Reference Type의 프로퍼티 래퍼는 ??

  • @StateObject, @ObservedObject, @EnvironmentObject
  • @EnvironmentObject 는 후반부에서 계속
  • Single Source Of Truth
    • Value : @State
    • Reference : @StateObject
  • Reference of SOT
    • Value : @Binding
    • Reference : @ObservedObject

protocol ObservableObject

  • 프로퍼티 래퍼 @StateObject, @ObservedObject 를 사용하려면 class에선 protocol ObservableObject 을 채택해주어야 함
  • protocol ObservableObject 은 AnyObject 를 채택 중 즉 class Type만 사용가능하다
  • class에 ObservableObject 을 채택하고 class 내에서 관찰할 프로퍼티에 @Published 를 사용

  • @Published 프로퍼티에서 변화가 생기면 값을 방출하여 SwiftUI가 뷰를 다시 그림
    • 값을 어디로 방출?
    • protocol ObservableObject 의 objectWillCahnge로
class ReadingListStore: ObservableObject {
    @Published var listName: String = "Book List"
}

@ObservedObject

  • ObservableObject class 를 관찰하려는 View에서 @observedobject 를 붙혀 사용
  • @observedobject 는 뷰 내부에서 직접 초기화가 가능
  • 하지만 @observedobject 는 인스턴스를 소유하고 있지 않고 있기 때문에 문제가 발생

@StateObject

struct ReadingList: View {
    @StateObject private var store = ReadingListStore()

    var body: some View {
        ReadingItem(store: store)
    }
}

struct ReadingItem: View {
    @ObservedObject var store: ReadingListStore

    var body: some View {
    }
}

그림으로 쉽게 이해하기

Note that the lifetime of a view is separate from the lifetime of a struct that defines it.

@EnvironmentObject 는 왜 생겨났나

  • ObservableObject 가 필요한 곳이 멀리 떨어져 있다면 ?

  • ObservableObject 가 필요하지 않은 뷰임에도 하위 뷰로 전달하기 위해서 추가해주어야 함

class MyObservableObject: ObservableObject { }

struct View1: View {
    @StateObject var myObservableObject = MyObservableObject()

    var body: some View {
        View2(myObservableObject: myObservableObject)
    }
}

struct View2: View {
    @ObservedObject var myObservableObject: MyObservableObject

    var body: some View {
        View3(myObservableObject: myObservableObject)
    }
}

struct View3: View {
    @ObservedObject var myObservableObject: MyObservableObject

    var body: some View {
        View4(myObservableObject: myObservableObject)
    }
}

// myObservableObject 필요한 곳
struct View4: View {
    @ObservedObject var myObservableObject: MyObservableObject

    var body: some View {
        myObservableObject
    }
}
  • 이럴때 사용하는 것이 @EnvironmentObject

  • 뷰에 environmentObject 로 등록하면 계층구조에 상관없이 접근 가능
  • @EnvironmentObject 를 사용하여 ObservableObject 가 필요한 뷰에서 접근
class MyObservableObject: ObservableObject {}

struct MyContentView: View {
    var body: some View {
        View1()
            .environmentObject(MyObservableObject())
    }
}

struct View1: View {

    var body: some View {
        View2()
    }
}

struct View2: View {

    var body: some View {
        View3()
    }
}

struct View3: View {
    var body: some View {
        View4()
    }
}

struct View4: View {
    @EnvironmentObject var myObservableObject: MyObservableObject

    var body: some View {
        myObservableObject
    }
}

Luckily, we have a solution for this problem with EnvironmentObject. EnvironmentObject is both a view modifier and a property wrapper.

참고

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

[Data Essentials in SwiftUI - WWDC20 - Videos - Apple Developer

Data is a complex part of any app, but SwiftUI makes it easy to ensure a smooth, data-driven experience from prototyping to production...

developer.apple.com](https://developer.apple.com/videos/play/wwdc2020/10040/)

https://developer.apple.com/documentation/Combine/ObservableObject

[ObservableObject | Apple Developer Documentation

A type of object with a publisher that emits before the object has changed.

developer.apple.com](https://developer.apple.com/documentation/Combine/ObservableObject)

https://developer.apple.com/documentation/combine/published

[Published | Apple Developer Documentation

A type that publishes a property marked with an attribute.

developer.apple.com](https://developer.apple.com/documentation/combine/published)

https://developer.apple.com/documentation/swiftui/observedobject

[ObservedObject | Apple Developer Documentation

A property wrapper type that subscribes to an observable object and invalidates a view whenever the observable object changes.

developer.apple.com](https://developer.apple.com/documentation/swiftui/observedobject)

https://developer.apple.com/documentation/swiftui/stateobject

[StateObject | Apple Developer Documentation

A property wrapper type that instantiates an observable object.

developer.apple.com](https://developer.apple.com/documentation/swiftui/stateobject)

https://developer.apple.com/documentation/Combine/ObservableObject

[ObservableObject | Apple Developer Documentation

A type of object with a publisher that emits before the object has changed.

developer.apple.com](https://developer.apple.com/documentation/Combine/ObservableObject)

https://developer.apple.com/documentation/swiftui/environmentobject

[EnvironmentObject | Apple Developer Documentation

A property wrapper type for an observable object that a parent or ancestor view supplies.

developer.apple.com](https://developer.apple.com/documentation/swiftui/environmentobject)

[WWDC - 2019] Data Flow Through SwiftUI - @State, @Binding
[WWDC - 2020] Data Essentials in SwiftUI - @StateObject, @ObservableObject, @EnvironmentObject

UIKit에서 Data Flow

Swift UI가 나오기전 UIKit에서의 Data Flow는 보통 아래와 같음

하나의 VC(PlayerViewController)에 하위 View(PlayerView, PlayButton)들이 있고

VC의 Model에서 하위 View들에게 값을 전달해주는게 일반적이었는데 여기서 동기화의 문제가 발생

PlayButton과 PlayerViewController의 isPlaying의 값이 동일해야 함

혹은 다른 다양한 View에서 같은 속성(isPlaying)을 사용한다면 같은 속성을 사용하는 View들의 isPlaying 값이 동일해야 함

즉 중복된 데이터들이 생기게 되고 어떤값이 진짜인지 알 수 없게 됨

그래서 이 문제들을 해결하기위해 다양한 아키텍쳐 및 디자인 패턴들이 등장하였고

결과적으로 View와 Model을 분리하고 Model을 한곳에서만 관리하여 View간의 동기화 문제를 해결하고자 하였음

SwiftUI에서 DataFlow

그래서 SwiftUI에선 새로운 Data Flow Tool들을 제공

SwiftUI는 UIKit과 다르게 VC가 없음 크게 뷰와 뷰의 상태정보만이 존재하는데

SwiftUI에선 뷰의 상태 프로퍼티를 보관하는 장소가 따로 있음

SwiftUI는 @State로 선언된 프로퍼티의 저장소를 관리

struct PlayerView: View {
    var isPlaying: Bool

    var body: some View {
          PlayerButton(isPlaying: isPlaying)
    }
}

struct PlayerButton: View {
    var isPlaying: Bool
}

PlayerView와 PlayerButton 둘 다 isPlaying이라는 프로퍼티가 필요해서 생기는 문제

- 기존까지의 방식으론 isPlaying 프로퍼티는 두 View에 존재하는 문제

- PlayerView의 isPlaying과 PlayerButton의 isPlaying 값을 동기화하려면 개발자가 별도의 로직을 구현

- SwiftUI는 View들이 struct이기 때문에 내부에서 isPlaying의 값을 바꿔주려면 Mutating 키워드를 붙혀야하는 문제

이때 사용하는것이 바로 @State

PlayerView의 isPlaying 프로퍼티 앞에 @State 키워드를 붙이는 순간 isPlaying 프로퍼티는

Single Source of Truth이 되어 SwiftUI에서 관리

Use state as the single source of truth for a given value stored in a view hierarchy.

Single Source of Truth가 된 프로퍼티를 다른곳에서도 사용할 수 있게 해주는것이 @Binding

@Binding을 통해 @State로 Single Source of Truth가 된 프로퍼티 원본에 접근 가능

State 키워드가 붙은 프로퍼티의 prefix에 $를 추가하여 Binding

struct PlayerView: View {
    @State private var isPlaying: Bool

    var body: some View {
         PlayerButton(isPlaying: $isPlaying)
    }
}

struct PlayerButton: View {
    @Binding var isPlaying: Bool
}

SwiftUI는 상태프로퍼티의 값이 변경 되면 상태프로퍼티에 맞게 View가 업데이트가 되어야 한다는것을 알 수 있고

SwiftUI는 상태프로퍼티가 포함된 View 계층구조의 일부 View들을 업데이트

@State

SwiftUI에서 관리하는 값을 읽고 쓸 수 있는 property wrapper type

@State에 private 키워드를 붙이는것을 권장하고 있는데 외부에서 State 프로퍼티에 접근하지 못하게 해야

모든 스레드에서 State 프로퍼티의 값을 안전하게 변경할 수 있음

struct PlayerView: View {
    @State private var isPlaying: Bool
}

@Binding

Single Source of Truth(State Property)가 소유한 값을 읽고 쓸 수 있게 해주는 프로퍼티 랩퍼

State는 View내부에서 선언하는것과 달리 Binding은 상위 View로부터 전달받아서 사용

생성자를 통해 상위 View의 State 값을 주입받아야 하고 주입받을 상태프로퍼티의 이름에 prefix로 $표시를 붙혀 Binding

struct PlayerView: View {
    @State private var isPlaying: Bool

    var body: some View {
         PlayerButton(isPlaying: $isPlaying)
    }
}

struct PlayerButton: View {
    @Binding var isPlaying: Bool
}

요약

  • SwiftUI는 View와 View의 State관리가 중요
  • SwiftUI는 @State 를 통해 Single Source of Truth(SOT)를 나타낼수 있음
  • SwiftUI는 @State 로 선언된 상태 프로퍼티의 저장소를 관리
  • @Binding 을 통해 Single Source of Truth(State Property)에 접근 가능
  • @Binding 을 할때는 @State 로 선언된 상태 프로퍼티의 이름에 prefix로 $표시를 추가해 사용
  • SwiftUI는 State프로퍼티가 변하면 State프로퍼티가 포함된 View 계층구조의 일부 View들을 업데이트

참고

Data Flow Through SwiftUI - WWDC19 - Videos - Apple Developer

[Data Flow Through SwiftUI - WWDC19 - Videos - Apple Developer

SwiftUI was built from the ground up to let you write beautiful and correct user interfaces free of inconsistencies. Learn how to connect...

developer.apple.com](https://developer.apple.com/videos/play/wwdc2019/226/)

Data Essentials in SwiftUI - WWDC20 - Videos - Apple Developer

[Data Essentials in SwiftUI - WWDC20 - Videos - Apple Developer

Data is a complex part of any app, but SwiftUI makes it easy to ensure a smooth, data-driven experience from prototyping to production...

developer.apple.com](https://developer.apple.com/videos/play/wwdc2020/10040/)

Introduction to SwiftUI - WWDC20 - Videos - Apple Developer

[Introduction to SwiftUI - WWDC20 - Videos - Apple Developer

Explore the world of declarative-style programming: Discover how to build a fully-functioning SwiftUI app from scratch as we explain the...

developer.apple.com](https://developer.apple.com/wwdc20/10119)

Demystify SwiftUI - WWDC21 - Videos - Apple Developer

[Demystify SwiftUI - WWDC21 - Videos - Apple Developer

Peek behind the curtain into the core tenets of SwiftUI philosophy: Identity, Lifetime, and Dependencies. Find out about common patterns,...

developer.apple.com](https://developer.apple.com/videos/play/wwdc2021/10022)

State | Apple Developer Documentation

[State | Apple Developer Documentation

A property wrapper type that can read and write a value managed by SwiftUI.

developer.apple.com](https://developer.apple.com/documentation/swiftui/state)

Binding | Apple Developer Documentation

[Binding | Apple Developer Documentation

A property wrapper type that can read and write a value owned by a source of truth.

developer.apple.com](https://developer.apple.com/documentation/swiftui/binding)

Managing user interface state | Apple Developer Documentation

[Managing user interface state | Apple Developer Documentation

Encapsulate view-specific data within your app’s view hierarchy to make your views reusable.

developer.apple.com](https://developer.apple.com/documentation/swiftui/managing-user-interface-state)

[iOS / SwiftUI] 사용자 인터페이스 상태 관리하기

[[iOS / SwiftUI] 사용자 인터페이스 상태 관리하기

안녕하세요 Niro 입니다! 이번엔 SwiftUI 에서 View 끼리 어떻게 데이터를 주고받고 View 를 최신 상태로 업데이트 하는지 전반적인 흐름을 알아보고자 Apple Developer 에 있는 Document 를 살펴 보려고 합니

velog.io](https://velog.io/@niro/iOS-SwiftUI-View-State)

[WWDC19] Data Flow Through SwiftUI

[[WWDC19] Data Flow Through SwiftUI

every time you read a piece of data in your view, you're creating a dependency for that view.View에서 데이터를 읽을 때마다, 해당 View에 대한 의존성을 생성한다. (데이터가 변경될 때마다

velog.io](https://velog.io/@marisol/WWDC19-Data-Flow-Through-SwiftUI)

[Swift] @State, @Binding, @Published 대해 알아보기(Property Wrapper 1편)

[[Swift] @State, @Binding, @Published 대해 알아보기(Property Wrapper 1편)

안녕하세요. 오늘은 SwiftUI를 접하면서 동시에 Property Wrapper 또한 접하게 되면서 주로 사용하는 것들에 대해서 정리할 필요성을 느껴 Property Wrapper에 대해 작성해 보려 합니다. 때문에 이번에 알아

siwon-code.tistory.com](https://siwon-code.tistory.com/73)

+ Recent posts