Where who wants to meet someone

[WWDC20] Data Essentials in SwiftUI 본문

Apple Developer/iOS(SwiftUI)

[WWDC20] Data Essentials in SwiftUI

Lust3r 2024. 1. 26. 21:27
728x90
 

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

- SwiftUI 앱 내에서 데이터 흐름을 시작하는 방법, State / Binding 키워드 다루는 영상

  1. Getting started
    Data for a new view
    - view가 작업을 수행하려면 어떤 데이터가 필요한가? (예시: 표지, 서명, 저자, 진행률)
    - view가 해당 데이터를 어떻게 조작하는가? (예시: 단순 표시)
    - data는 어디에서 오는가? ("Source of Truth")

    - 가령 읽고 있는 책의 목록을 제공하는 앱이 있다면, 각 항목(표지, 서명, 저자, 진행률을 담고 있는)은 단순히 그 항목에 들어오는 데이터의 정보를 보여주고 있고, Source of Truth는 해당 항목의 View 계층 구조보다 상위에 있음
    - 해당 항목을 인스턴스화할 때 실제 데이터를 전달(예시: let book: Book)
    - SwiftUI에서 가장 간단한 Source of Truth는 '@State'임
    - View는 구조체이기 때문에 렌더링이 끝나면 사라짐. 그러나 저장해야 하는 데이터는 유지되어야 하기 때문에 SwiftUI가 관리
      그러면 다음에 프레임워크가 그 View를 렌더링해야 할 때 구조체를 다시 인스턴스화하고 관리한 storage에 다시 연결
    - SwiftUI에서 모든 Source of Truth에 대한 쓰기 접근을 공유하는 도구는 '@Binding'임"
    - 편집에 관련된 속성을 모은 구조체를 한 항목에서 사용한다고 했을 때, 상태의 추적을 위해 @State로 프로퍼티를 인스턴스화 하고, Binding을 통해 그 값을 변경
    - @Binding은 속성 구조체와 값을 편집하는 곳 사이에 데이터 종속성을 생성

  2. Designing your model
    Using ObservableObject
    - Manage life cycle of your data
    - Handle side-effects
    - Integrate with existing components

    - 위의 경우에 ObservableObject를 사용하는데, 클래스 제한 프로토콜이므로 reference 타입만 채택할 수 있음.
    - ObservableObject에는 objectWillChange 라는 단일 요구사항 속성이 있음.
    - 해당 속성은 Publisher이며, 이름 그대로 해당 프로토콜의 의미론적 요구 사항은 개체에 변화가 적용되기 전에 Publisher가 방출해야 함.
    - 그러면 기본적으로 즉시 사용 가능한 훌륭한 Publisher를 얻게 됨(ObjectWillChangePublisher)
      (필요한 경우 커스텀 Publisher를 제공할 수 있음. 가령 타이머에 Publisher를 사용하거나 기존 모델을 관찰하는 KVO Publisher 등)

    - SwiftUI는 Source of Truth와 View 간의 종속성을 사용하여 자동으로 View의 일관성을 유지하고 데이터의 올바른 표현을 표시함.
    - value 타입을 사용하여 데이터를 모델링하고, reference 타입을 사용하여 데이터의 수명 주기와 부작용을 관리할 수 있음
    - 모든 로직을 위한 단일 위치를 제공하므로 앱에서 가능한 모든 상태와 변형에 대해 쉽게 추론할 수 있음.

    @Published
    - Automatically works with ObservableObject
    - Publishes every time the value changes in willSet
    - projectedValue is a Publisher

    - @Publisher는 Publisher를 노출하여 프로퍼티를 관찰할 수 있게 만드는 property wrapper
    - 대부분의 모델의 경우 ObservableObject를 준수하고 변경될 수 있는 속성에 @Published를 추가하면 작업이 완료됨
    - ObservableObject publisher와 함께 @Published를 사용하면 시스템은 값이 변경되기 직전에 publishing하여 자동으로 무효화를 수행
    - 이전에 다뤘던, 3가지 고려사항에서 2번째(View가 데이터를 어떻게 조작하는가?)는 ObservableObject를 사용하면 답하는 것이 간단. 항상 가변성을 가정하기 때문.
    - 그렇다면, 2번째를 제외한 1, 3번 질문에 대한 답변은?
    - 먼저 1번 질문. SwiftUI는 ObservableObject에 대한 종속성을 생성하기 위해 View에서 사용할 수 있는 세 가지 property wrapper를 제공함.

    1) @ObservedObject
    - ObservableObject를 준수하는 유형을 보유하는 View의 프로퍼티에 사용할 수 있는 property wrapper
    - 이를 사용하면 SwiftUI에 View에 대한 종속성으로 프로퍼티 추적을 시작하도록 알림.
      (Tracks the ObservableObject as a dependency)
      View가 해당 작업을 수행하는 데 필요한 데이터를 정의함.
    - 이는 제공하는 인스턴스의 소유권을 얻지 않고, 수명 주기를 관리하는 것은 개발자의 몫
    - ObservedObject를 사용할 때마다 SwiftUI는 특정 ObservableObject의 objectWillChange를 subscribe함.
      그러면 ObservableObject가 변경될 때마다 이에 의존하는 모든 View가 업데이트됨.
    - 그렇다면, 왜 'Will'Change일까? -> SwiftUI가 모든 변경 사항을 단일 업데이트로 통합할 수 있도록 언제 무언가가 변경되려고 하는지 알아야 하기 때문.

    Binding from ObservableObject
    - SwiftUI components accept Binding
    - Binding can be derived from State and ObservableObject
    - Derive a binding by using the $ prefix

    - 변수 앞에 $를 붙이고 프로퍼티에 접근하면, ObservableObject의 value 유형인 모든 속성에서 바인딩을 가져올 수 있음.
    - @State는 수명 주기가 View에 연결되어 있지만 @ObservedObject는 수명 주기를 소유하지 않으므로 아님.
      그래서 이를 돕기 위한 도구가 @StateObject

    2) @StateObject
    - SwiftUI owns the ObservableObject
    - Creation and destruction is tied to the view's life cycle
    - Instantiated just before body

    - StateObject로 프로퍼티에 주석을 달 때, 초기 값을 제공하고, SwiftUI는 처음으로 body를 실행하기 직전에 해당 값을 인스턴스화함
    - 그러면 SwiftUI는 View의 전체 수명 주기 동안 객체를 활성 상태로 유지함
    - 위와 같은 방식으로 사용을 하면, View가 생성될 때 인스턴스화 하는 것이 아니라 body가 실행되기 직전에 인스턴스화되고 전체 뷰 수명 주기 동안 활성 상태로 유지됨.
    - View가 더 이상 필요하지 않으면 SwiftUI는 해당 인스턴스를 release함(onDisappear를 조작할 필요가 없음)
    - SwiftUI는 View 코스트가 적고, 모듈화를 할 수 있으면 분리하는 것을 권장하기에 계층 구조로 보면 꽤 깊게 이어지기도 함.
      그럴 때, 상위 뷰에서 ObservableObject를 생성하고 동일한 인스턴스를 여러 하위 뷰에서 사용하는 경우나 먼 하위 View에서 ObservableObject를 사용하는 경우, 또는 해당 데이터가 필요하지 않은 View를 통해 이를 전달하는 경우가 생길 수 있음
    - 이것을 해결하기 위한 것이 @EnvironmentObject

    3) @EnvironmentObject
    - view modifier이자 property wrapper
    - ObservableObject를 삽입하려는 상위 View에서 이 modifier를 사용함
    - 특정 ObservableObject의 인스턴스를 읽으려는 모든 View에서 property wrapper를 사용함
    - 상위 View: .environmentObject(ObservableObject)
      하위 View: @EnvironmentObject var model:
    - 프레임워크는 필요한 모든 곳에 해당 값을 전달하고, 읽은 위치에서만 종속성으로 추적함
  3. Techniques for your app
    - View는 SwiftUI에서 단순히 UI 부분에 대한 정의임

    View update life cycle
    - Views define a piece of UI
    - SwiftUI manages identity and lifetime
    - Lightweight and inexpensive

    - View는 UI의 일부를 정의하므로 가볍고 저렴해야 함.
    - View의 수명은 View를 정의하는 구조체의 수명과 별개임.
    - View 프로토콜을 준수하는 구조체는 실제로 수명이 매우 짧음. SwiftUI는 이를 이용하여 렌더링을 생성한 다음 사라짐.

 

- 위는 SwiftUI View LifeCycle.
- 시계 반대방향으로, UI에서 Closure나 action으로 Event가 발생하고, 그로 인해 Source of Truth에 변화가 생긴다면
  렌더링을 생성하는 데 사용할 View의 새 복사본을 얻게 됨. 그 복사본으로 렌더링한 것이 UI임
- 위 그림을 단순화 시키면 이름 그대로 순환하는 원임. 앱이 실행되는 동안 여러 번 반복됨.
- 이상적으로는 지속적으로 순환이 잘 이뤄지지만 cost가 많이 드는 작업이 수행되면 앱 성능이 저하됨.
- 이를 Slow update라고 칭하고, 프레임이 떨어지거나 앱이 중단될 수 있음을 의미함.
- 그렇다면 어떻게 Slow update를 방지할 수 있을까?

 

Avoiding slow updates

- Make view initialization cheap

- Make body a pure function

- Avoid assumptions

 

- View의 초기화 cost를 줄이는 것. View의 body는 부작용이 없는 순수한 함수여야 함.
  dispatching이나 다른 작업 없이.
- body가 언제, 얼마나 자주 호출되는지에 대한 가정을 피해야 함.

- 가령 A View의 body에서 B View를 호출하는데, B View에 @ObservedObject가 있다면 인스턴스화 할 때마다 해당 프로퍼티의
  heap 할당이 반복적으로 발생하고, 이로 인한 slow update가 발생할 수 있음.
- 그렇다면 위의 경우에는 어떻게 해결할 수 있을까?

  답은 @StateObject
  StateObject를 사용하면 SwiftUI가 적시에 ObservableObject를 인스턴스화할 수 있으므로 불필요한 heap 할당이 발생하지 않고
  데이터가 손실되지 않음.

- 기존의 B View에 있던 프로퍼티에 @ObservedObject 대신 @StateObject를 사용.

Event sources

- User interaction

- onReceive

- onChange

- onOpenURL

- onContinueUserActivity

 

- SwiftUI는 slow update를 방지하고 body를 저렴하게 유지하는 데 도움이 되도록 적절한 시간에 closure를 실행함

- 그러나 이 closure는 main thread에서 실행되므로 cost가 큰 작업을 해야 한다면 background queue로 dispatch하는 것을 고려

 

초기에 3가지 질문을 했을 때, 처음 두 질문을 해결하는 방법을 다루었지만 세 번째 질문은 쉽지 않음(데이터는 어디에서 오는가?)

여기에는 정답이 없으므로 도움이 되는 몇 가지 고려사항을 살펴보면 좋음

 

Who owns the data?

- Lift data to a common ancestor

- Leverage @StateObject

- Consider placing global data in app

 

- 가장 중요한 질문은 '누가 데이터를 소유하는가?'임. 데이터를 사용하는 View? 아니면 해당 View의 ancestor? 아니면 subsystem?

- 위에 적혀 있는 3가지처럼 공통 ancestor와 데이터를 공유하고, ObservableObject에 대한 view 소유 source of truth로 @StateObject를 선호하고, 마지막으로 앱에 전역 데이터를 배치하는 것을 고려해야 함.

 

Data lifetime

- App

- Scene

- View

 

- 위에서 다룬 property wrapper는 View와 함께 작동하고, View는 데이터 수명을 연결하는 훌륭한 도구임.

- State, StateObject를 사용하여 데이터 수명을 연결하고 수명을 볼 수 있음.

 

- SwiftUI의 Scene에는 각각 고유한 view tree가 있으므로 중요한 데이터 조각을 tree root에 걸 수 있음.
  (WindowGroup - Window - ContentView / Window - ContentView)

- 가령 WindowGroup 내부의 View에 source of truth를 배치할 수 있고, 그렇게 하면 WindowGroup이 생성하는 scene의 각
  인스턴스는 자체적이고 독립적인 source of truth를 가질 수 있음
  (아이패드에서 같은 앱 두 scene으로 나누어 하는 경우를 예로.)

 

- App은 SwiftUI의 강력한 새 도구로, SwiftUI만으로 전체 app을 작성할 수 있음

- App의 가장 큰 장점은 View에서와 마찬가지로 상태 및 기타 정보 소스를 사용할 수 있다는 것.

- App 구조체 안에서 @StateObject를 사용하여 전체 앱에 대한 전역 모델을 만들 수 있음

- source of truth를 어디에 둘지 고려할 때, 그것이 글로벌 데이터를 나타내는 경우 App에 넣는 것이 좋음

 

Source of Truth lifetime (Process Lifetime)

- State

- StateObject

- Constant

 

- 위 3가지는 프로세스 수명에 묶여 있기 때문에 앱이 종료되거나 기기가 다시 시작되면 상태가 다시 돌아오지 않음.

- 이 문제를 해결하기 위한 것이 Storage

  (SceneStorage, AppStorage)

- 이는 수명이 연장되고 자동으로 저장 및 복원됨. 모델이 아니라 모델과 함께 사용하는 경량 저장소

 

SceneStorage (Extended Lifetime)

- SwiftUI에서 완전히 관리되는 데이터를 읽고 쓰는 Scene별 범위 프로퍼티 wrapper

- View 내에서만 접근할 수 있으므로 앱의 현재 상태에 대한 간단한 정보를 저장하는 데 매우 적합

- 가령 위에서 아이패드로 같은 앱의 두 scene을 띄운 상황을 가정하면, SceneStorage에 저장해야 하는 것은 무엇?
  책의 정보 등은 이미 따로 저장하고 있으므로 어떤 선택(어떤 책의 페이지인지)을 했는지 여기에 저장하여 다음에 실행되었을 때
  이를 참고하여 Scene을 렌더

 

AppStorage (Extended Lifetime)

- UserDefaults를 사용하여 유지되는 앱 범위 전역 저장소

- 어디에서나 사용할 수 있으므로 App이나 View 내에서 접근할 수 있음

- UserDefaults와 마찬가지로 설정과 같은 작은 사이즈의 데이터를 저장하는 데 매우 유용

 

ObservableObject (Custom Lifetime)

- Apple에서 이를 매우 유연하게 설계했기 때문에 이를 사용하여 서버나 다른 서비스에 의한 데이터 백업과 같은 매우 사용자 정의된 동작을 달성할 수 있음

 

모든 상황에 다 쓰일 수 있는 것은 없음. 다양하게 상황에 맞춰 사용해야 함.

가령 위 과정을 통해서 버튼과 진행률 View에는 @State를, currentlyReading model에는 @StateObject를, selection에는 SceneStorage, 설정에는 AppStorage를 사용했음.

데이터의 속성이 무엇인지, 사용할 올바른 source of truth가 무엇인지 생각하는 것이 중요하고, 복잡성을 줄이기 위해 그 수를 제한해야 함.

마지막으로 재사용 가능한 컴포넌트를 구축하는 방법으로 binding을 활용해야 함. 이는 source of truth에 대한 완전한 불가지론이므로 깔끔한 추상화를 구축하기 위한 강력한 도구가 됨.