[WWDC21 ] Demystify SwiftUI

marisol👩🏻‍💻·2022년 12월 3일
0

SwiftUI는 우리의 코드에서 위 3가지 요소를 확인한다

  1. Identify - SwiftUI가 어떻게 앱의 여러 요소를 같거나 다르게 인식하는지
  2. LifeTime - SwiftUI가 어떻게 뷰와 데이터의 존재를 추적하는지
  3. Dependency - SwiftUI가 어떻게 인터페이스를 업데이트해야하는 시기과 이유를 이해하는지

=> 위 3가지 개념을 통해 SwiftUI는 무엇이, 어떻게, 언제 바뀌어야 하는지 결정하여 UI를 생성한다

✏️ 1. Identity

이 두 개의 뷰는 완전히 다른 뷰일까? 같은 뷰일까?

만약 다른 뷰라고 한다면, icon이 독립적으로 전환되어야 한다.
별개의 UI 요소를 나타내는 뷰는 항상 다른 Identity를 가질 것이다.
(초록색/빨간색이 각각 다른 Identity를 가짐)

만약 동일한 뷰라고 한다면, 뷰가 한 위치에서 다른 위치로 이동하면 된다.
동일한 Identity를 공유하는 뷰는, 동일한 UI 요소의 다른 상태를 나타낸다.
(초록색/빨간색이 같은 UI 요소이고, 상태에 따라 초록색/빨간색이 될 수 있는 것)

⭐️ SwiftUI가 사용하는 2가지 유형의 Identity

1️⃣ Explicit identity (사용자 지정 식별자 또는 데이터 기반 식별자 사용)
2️⃣ Structural identity (뷰를 타입과 뷰 계층의 위치로 구분)

강아지가 똑같이 생겼을 때, 어떤 추가 정보가 강아지를 식별하는데 도움이 될까?
=> "이름" 물어보기

두 강아지가 똑같이 생겼고, 같은 이름을 가지고 있다? -> 같은 강아지
두 강아지가 똑같이 생겼지만, 다른 이름을 가지고 있다? -> 다른 강아지

이렇게 이름이나 식별자를 할당하는 것이 explicit identity (명시적 정체성)의 한 형태이다.
명시적 정체성은 강력하고 유연하지만, 누군가 어딘가에서 그 모든 이름을 추적해야만 한다.
이미 익숙한 명시적 정체성의 한 형태는, UIKit 및 AppKit 전반에 걸쳐 사용되는 pointer identity이다.

UIView와 NSView는 클래스이기 때문에, 각각 메모리 할당에 대한 고유한 포인터가 있다.
포인터를 사용하여 개별 뷰를 참조할 수 있으며, 두 뷰가 동일한 포인터를 공유한다면 실제로 동일한 뷰임을 보장할 수 있다.

하지만 SwiftUI는 포인터를 사용하지 않는다. (SwiftUI의 뷰는 일반적으로 클래스 대신 구조체 타입이기 때문)
value type은 참조를 가지고 있지 않지만, 대신 SwiftUI는 다른 형태의 명시적 정체성에 의존한다.

구조견 목록이 이렇게 있다고 할 때,
id 매개변수가 명시적 identity의 한 형태이다.
각 구조견의 강아지 태그 id는 List에서 해당 View를 명시적으로 식별하는데에 사용된다.
구조견 배열이 변경되면, SwiftUI는 이 id를 사용하여 정확히 무엇이 변경되었는지 파악하고,
서로 다른 섹션 사이를 이동하는 애니메이션을 생성할 수 있다.

ScrollViewReader를 사용하여 맨 하단의 "Jump to Top" 버튼을 누르면 상단의 HeaderView로 이동시킬 수 있다.
id modifier는 커스텀 identifier를 사용하여 뷰를 명시적으로 식별할 수 있다.
이동해야할 뷰 (HeaderView)에 identifier를 scrollViewProxy의 scrollTo(_:) 메서드에 전달하여 특정 뷰로 이동하도록 할 수 있다.

우리가 모든 뷰를 명시적으로 식별할 필요는 없다. 위와 같은 헤더와 같이, 코드의 다른 곳에서 참조해야하는 뷰만 명시적으로 식별하면 된다.
ScrollViewReader, ScrollView, Text, Button 등에는 명시적인 식별자가 필요하지 않다.
-> 이 뷰들의 정체성이 명시적이지 않다고 해서, identity가 없다는 것을 의미하지는 않는다. 왜냐면,

모든 뷰는 정체성을 가지고 있기 때문이다. (명시적이지 않더라도)

=> Structural identity
SwiftUI는 뷰 계층 구조를 사용하여 뷰에 대한 암시적 ID를 생성한다.

비슷한 강아지 두 마리가 있고, 이름은 모르지만 각각의 강아지를 식별해야 한다고 가정하자.
이 강아지들이 각각 "어디에 앉아 있는지"를 기준으로 그들을 식별할 수 있다.
(왼쪽에 있는 개 - 오른쪽에 있는 개)

서로를 구별하기 위해 상대적인 배열(relative arrangement)을 사용하고 있다.
이것이 Structural Identity

SwiftUI는 API 전반에 걸쳐 Structural Identity를 활용하며,
전형적인 예는 뷰 코드 내에서 if문과 기타 조건부 로직을 사용하는 경우이다.
조건문의 구조는 우리에게 각각의 뷰를 식별할 수 있는 명확한 방법을 제공한다.

첫번째 뷰는 조건이 참일 때만 표시되고, 두 번째 보기는 조건이 거짓일 때만 표시된다.

SwiftUI가 뷰를 볼 때, generic types를 본다.
이 경우에는 if문은 _ConditionalContent 로 변환되며, 이 변환은 result builder인 ViewBuilder에 의해 작동된다.
SwiftUI의 View 프로토콜은 body 프로퍼티를 ViewBuilder로 암시적으로 래핑한다.
ViewBuilder는 logic statement에서 단일 일반 뷰를 구성하지만, 코드를 복잡하게 하지 않기 위해 숨긴다.

아까의 Good Dog, Bad Dog 뷰를 다시 보면,
상단의 코드와 같이 if문으로 분기처리해서 각각 다른 뷰로 구성할 수도 있다.

하지만 하단의 코드처럼 상태에 따라 레이아웃과 색상을 변경하는 단일 PawView를 사용할 수도 있다.
다른 상태로 전환되면 뷰가 다음 위치로 자연스럽게 이동한다. (하나의 아이덴티티로 단일 뷰를 수정하기 떄문)

두 가지 경우 모두 가능하지만, 일반적으로 SwiftUI는 두 번째 접근 방식을 권장한다.
뷰의 수명과 상태를 보존하는데 도움이 되며, 자연스러운 전환을 할 수 있기 때문.

✏️ 2. LifeTime

고양이의 이름을 지으면, 그 고양이가 다른 상태에 있거나, 하루 종일 움직이더라도 항상 같은 이름의 "Theseus" 고양이일 것이다.

Identity는 시간이 지남에 따라 다른 값에 대한 안정적인 요소를 정의할 수 있게 해준다.
시간이 지남에 따라 서로 다른 값들을 하나의 객체, 즉 하나의 뷰로 연결한다.

고양이가 그르릉거리는 소리의 강도를 보여주는 코드인데,
Theseus는 처음에는 소리의 강도가 25이다가, 점점 배가 고파지고 관심을 갖고 싶으면 강도가 50으로 변경된다.
이때 뷰에 대한 새로운 값이 생성되고,
SwiftUI는 비교를 위해 값의 복사본을 보관하고 뷰가 변경되었는지 여부를 파악한 다음, 이 전 값을 없앤다.

여기서 이해햐아할 중요한 포인트는, 뷰의 값이 뷰의 ID와 다르다는 점이다.

뷰가 처음 생성되고 나타나면, SwiftUI는 앞서 말했던 것처럼 identity를 할당한다.
시간이 지남에 따라 업데이트로 인해 뷰에 대한 새로운 값이 생성되지만,
SwiftUI 관점에서 이것들은 동일한 뷰를 나타낸다.
뷰의 ID가 변경되거나 뷰가 제거되면 뷰의 lifetime이 종료된다.

여기서 "State"와 "StateObject"는 뷰의 영구 저장소이다.
뷰가 처음 생성되면, SwiftUI는 초기값을 사용하여 State 및 StateObject에 대해 메모리의 스토리지를 할당한다.
뷰의 일생 동안, SwiftUI는 뷰의 body가 변경될 때에도 이 스토리지를 유지한다.

위의 코드는 CatRecirder라는 뷰가 다른 브랜치에 존재하고 있다. (분기 처리)
처음에 SwiftUI는 초기값을 사용하여 영구 스토리지를 할당한다.
이 뷰의 lifetime 동안, SwiftUI는 다양한 작업에 의해 변경될 때에도 상태를 유지한다.
만약 datyTime의 값이 변경되어 false branch에 들어가게 된다면?
SwiftUI는 새 저장소를 생성하고, true branch 뷰의 스토리지를 할당 해제한다.

만약 다시 true 브랜치로 돌아가게 된다면 false branch 뷰의 storage를 할당 해제하고 다시 새 스토리지를 생성한다.
중요한 점은, identity가 바뀔 때마다 state가 교체된다는 것이다.

State의 지속성은 View의 lifetime에 묶여있다.

SwiftUI에는 데이터의 ID를 뷰에 대한 명시적 ID의 형태로 사용하는 데이터 기반 구조가 있다.

위 이니셜라이저는 식별자 역할을 하는 id를 가져야 하지만, RescueCat이 Identifiable을 채택한다면, 생략 가능하다.

✏️ 3. Dependency

2개의 프로퍼티 (강아지, 간식)가 있으며, 이 프로퍼티는 뷰의 종속성이다.
종속성이 변경되면, 뷰에서 body를 새롭게 작성해야 한다.

body - 뷰에 대한 계층을 작성하는 위치

action - 뷰의 종속성에 대한 변경을 트리거 한다

버튼을 누르면 강아지에게 보상이 주어지며, 강아지는 간식을 먹는다.
종속성이 변경되었기 때문에 DobView는 새 body를 생성한다.

동일한 State 또는 데이터에 여러 뷰들이 의존하고 있을 수도 있다.
이 구조를 dependency graph라고 부르는데, SwiftUI가 새 body가 필요한 뷰만을 효율적으로 업데이트한다.

0개의 댓글