Demystify SwiftUI containers (feat. WWDC 2024)
안녕하세요. 그린입니다 🍏
이번 포스팅에서는 WWDC 2024에서 말아주는 Demystify SwiftUI containers 세션에 대해 같이 볼까 합니다 🙋🏻
세션 타이틀부터 느껴지는게 SwiftUI 컨테이너 뷰에 대해 알아보면서 어떻게 뷰들이 컨테이너에 의해 관리되는지 우리에게 설명해줄것 같아요 😃
그럼 바로 들어가볼까요? 🏃🏻
Demystify SwiftUI containers
SwiftUI는 API에서 리스트 컨테이너와 같이 모든 기능을 갖춘 다양한 종류의 컨테이너를 제공합니다.
이렇게 컨테이너 뷰는 컨텐츠를 래핑하기 위해서 후행 뷰 빌더 클로즈를 사용합니다.
뷰 빌더를 사용하면 하드 코딩된 텍스트 뷰들의 리스트처럼 콘텐츠를 정적으로 정의할 수 있죠!
또한, ForEach를 이용하여 동적으로도 생성할 수 있어요.
즉, 뷰 빌더는 동일한 컨테이너 내에서 모든 종류의 컨텐츠를 함께 구성하는것을 지원하고 있습니다.
또한, 일부 컨테이너들은 헤더/푸터 섹션을 제공하거나 별도 섹션으로 그룹화도 가능하죠.
해당 세션에서는 몇가지 새로운 API를 이용해 다양한 기능을 지원하는 커스텀한 컨테이너 뷰를 구축하는 방법을 소개하려 합니다 🙋🏻
Composition
우선 커스텀 컨테이너가 모든 컨텐츠 구성을 지원하고 유연성을 극대화하는 방법에 대해 알아봅니다.
SwiftUI는 위 처럼 다양한 컨텐츠를 구성하기 위해 API들을 제공해 격차를 해소할 수 있습니다.
즉, 위 코드가 동일한것이죠.
물론, 기존처럼 ForEach를 사용해도 되죠!
또한 유연하게 이렇게 하드코딩된 정적인 텍스트 뷰들과 동시에 동적으로 나머지 행에 대한 텍스트 뷰도 같이 공존하게 구성할 수 있죠.
이제 이렇게 되있던 뷰를 조금 더 유연하게 구성을 지원하도록 변경해봅니다.
첫번째로, 컨테이너를 리팩토링하여 뷰 빌더만 사용해 초기화하도록 하는것입니다.
그럼, 기존 데이터 기반 속성을 단일 뷰 속성으로 바꿔야 합니다.
이제 이렇게 사용할 수 있어요.
즉, content인 View 자체를 ForEach에서 이번 iOS 18에서 새롭게 나온 API의 도움을 받을 수 있습니다.
바로, subviewOf 파라미터죠.
이렇게 초기화를 시키면 단일 뷰 값을 입력으로 받을 수 있습니다.
그 후 각 하위 뷰를 후행 뷰 빌더로 다시 전달하여 해당 카드 뷰와 같은 다른 종류의 뷰로 변환해볼 수 있는것이죠!
해당 코드에서 하위 뷰는 몇개일까요?
코드의 최상위 뷰만 고려한다면 4개라고 할 수 있죠.
근데 여기서 ForEach는 단순한 뷰가 아니라 데이터에서 생성된 뷰 컬렉션을 나타냅니다.
즉, 이렇게 구성될 경우 하위 뷰는 총 12개의 개별 하위 뷰로 구성되는 것이죠.
9개의 하위 뷰와의 차이는, 위를 보시는것처럼 강조된 DisplayBoard 코드의 4개 하위 뷰는 선언된 하위 뷰라고 합니다.
반면, 해당 뷰들은 Resolved subviews라고 하여 해결된 하위 뷰라고 합니다.
여기엔 수동으로 정의한 3개 정적 텍스트 뷰와 ForEach로 생성된 9개의 텍스트 뷰가 포함되죠.
핵심은 SwiftUI의 선언적 시스템에서 선언된 하위 뷰는 SwiftUI 앱이 실행되는 동안 리졸브 하위 뷰를 생성하기 위한 방법을 정의합니다.
예를들어, ForEach 뷰는 자체적으로 특정한 시각적인 모양이나 동작이 없이 선언된 하위 뷰죠.
대신 ForEach 뷰의 의도 자체는 리졸브된 하위 뷰들을 생성하는것에 있습니다.
Group 컨테이너도 마찬가지죠.
혹은 이렇게 EmptyView 처럼 비어있는 뷰로 선언되었다면 리졸브된 하위 뷰가 제로로 생성됩니다.
if나 switch 등 다양한 분기와 같이 다양한 수의 하위 뷰를 조건부로 리졸브될 수도 있죠.
다시 돌아와서 새로운 ForEach subviewsOf의 API는 컨텐츠의 리졸브된 하위 뷰만 반복합니다.
해당 컨테이너가 더 작은 코드로 가능한 모든 컨텐츠 구성을 지원할 수 있게 됩니다.
왜냐하면, 하위 뷰가 코드에서 어떻게 선언되었는지에 관계없이 SwiftUI가 하위 뷰를 리졸브하는 작업을 수행하기 때문이죠.
즉, 컨테이너 구현을 추가로 변경하지 않고도 가능하죠 😃
Group에서도 새롭게 subviewsOf 인자를 사용할 수 있어요.
이걸 언제 적절히 사용하냐면, 예를들어 컨텐츠의 갯수에 따라 카드의 사이즈를 조정하고 싶을 수 있습니다.
그럴때 Group으로 묶어 ForEach에 전체 subViews를 넘겨 판단하게 할 수 있죠.
이런식으로 말이죠!
핵심은 Declare와 Resolve라고 보여집니다 ☺️
Section
섹션에 대해 알아봅니다.
섹션 뷰는 그룹 뷰와 매우 유사하게 작동하지만 선택적으로 헤더 / 푸터와 같은 추가 기능을 제공해줍니다.
그러나, 커스텀한 컨테이너는 기본적으로 섹션을 지원하지 않기에 이를 활성화하려면 몇가지 추가 작업이 필요합니다.
이렇게 별도 커스텀한 뷰를 만들어 활용할 수 있죠.
이제 해당 뷰들을 열로 나누기 위해 섹션 콘텐츠를 HStack으로 래핑합니다.
이제 헤더를 표시하기 위해 VStack으로 구성합니다.
커스텀한 DisplayBoardSectionHeaderCard를 이용해서 담아줄 수 있죠.
이렇게 구성한다면 섹션별로 열로 나눠지는 커스텀한 뷰를 직접 만들어볼 수 있습니다.
이런 형태로 말이죠 😃
즉, 섹션을 제공해주는 컨테이너 뷰들도 있지만, 커스텀하게 만드는 컨테이너 뷰라면 직접 입맛대로도 구성할 수 있는 유연함이 있죠 😃
Customization
컨테이너 내 컨텐츠를 넣기 위해 커스텀한 뷰 모디파이어를 정의하는 방법을 알아봅니다.
예시로 이렇게 List에서 구분선을 숨기긴 위한 뷰 모디파이어가 존재합니다.
또한, 위의 예시에서 계속 다뤘던것처럼 보드에서 특정 노래를 선택하지 않기로 결정했을때 카드를 수정하거나 지울 수 있도록 지원해볼 수 도 있어요.
Container values라고 하는 컨테이너별 수정자를 구축하기 위한 새로운 API가 있습니다.
Container values는 환경 및 기본 설정과 같은 개념과 유사한 새로운 종류의 키 저장소 값입니다.
환경 변수는 전체 뷰 계층 구조에서 하위로 전달되고,
Preference 변수는 상위로 전달되죠.
반면 Container values는 직접 컨테이너를 통해서만 접근할 수 있으므로 컨테이너 별 사용자 정의 옵션을 구현하기 위해 최적화된 도구라 볼 수 있습니다.
즉, 컨테이너 내에서 노는것이죠!
이제 구현해보기 위해서 Container values를 확장해봐야 합니다.
ContainerValues를 확장하여 새롭게 나온 Entry 매크로를 사용해 속성을 선언하고 카드 거부 여부를 추적하기 위한 Bool 값을 저장합니다.
여기서 Entry 매크로는 환경 값, 포커스 값 등을 포함하여 SwiftUI 키 저장 유형에 새로운 값을 추가하기 위한 새로운 API입니다.
그 후, 속성 설정을 위해 View를 확장해 메서드를 만들어봅니다.
이제 해당 메서드를 이용해 새 Container values API를 호출해 키 경로와 설정할 새 값을 View에서 전달할 수 있죠.
이제 해당 설정을 적용해볼까요?
이제 이렇게 values를 이용해 카드뷰에 해당 값을 넘길 수 있는것이죠!
실제로 이렇게 해당 메서드가 이용될 수 있습니다.
마무리
엄청나게 변모되거나 한건 없지만, 조금씩 사용성과 유연성이 증가하고 있습니다.
또한, 어떻게 보면 SwiftUI를 사용하고 계셨던 분께는 시시할 수 있지만, Declare와 Resolve를 확실히 다잡을 수 있는 세션이였어요.