커스텀 컨테이너 뷰 만들기
하위 뷰는 뷰 선언부를 작고 가벼우며 재사용할 수 있는 블록으로 나누는 유용한 방법을 제공하지만, 컨테이너 뷰의 content가 정적(static)이라는 한계가 있다. 다시 말해, 하위 뷰가 레이아웃에 포함되는 시점에 하위 뷰에 포함될 뷰를 동적으로 지정할 수 없다. 하위 뷰에 포함되는 뷰들은 최초 선언부에 지정된 하위 뷰들뿐이다.
- 3개의 텍스트 뷰가 VStack 안에 포함되고 임의의 간격과 폰트 설정으로 구성된 하위 뷰가 다음과 같이 있다고 하자.
struct MyVStack: View { var body: some View { VStack(spacing: 10) { Text("Text Item 1") Text("Text Item 2") Text("Text Item 3") } .font(.largeTitle) } } - 선언부에 MyVStack 인스턴스를 포함시키려면 다음과 같이 참조할 것이다.
- MyVStack()
하지만, 간격이 10이며 largeTitle 폰트 수정자를 가진 VStack이 프로젝트 내에서 자주 필요하지만, 사용할 곳마다 서로 다른 뷰들이 여기에 담겨야 한다고 가정하자. 하위 뷰를 사용해서는 이러한 유연성을 갖지 못하지만, 커스텀 컨테이너 뷰를 생성할 때 SwiftUI의 ViewBuilder 클로저 속성을 이용하면 가능하다.
ViewBuilder는 스위프트 클로저 형태를 취하며 여러 하위 뷰로 구성된 커스텀 뷰를 만드는 데 사용될 수 있으며, 이 뷰가 레이아웃 선언부 내에 사용될 때까지 내용을 선언할 필요가 없다. ViewBuilder 클로저는 콘텐트 뷰들을 받아서 동적으로 만들어진 단일 뷰로 반환한다. 다음은 MyVStack 뷰를 구현하기 위해 ViewBuilder 속성을 사용하는 예제다.
struct MyVStack<Content: View>: View { let content: () -> Content initI(@ViewBuilder content: @escaping () -> Content) { self.content = content } var body: some View { VStack(spacing: 10) { content() } .font(.largeTitle) } } - 이 선언부는 View 프로토콜을 따르며, body에는 VStack 선언부를 포함한다. 하지만 스택에 정적 뷰를 포함하는 대신에 하위 뷰들은 초기화 메서드에 전달되며, ViewBuilder에 의해 처리되어 VStack에 하위 뷰들로 포함될 것이다.
- 이제 커스텀 MyVStack 뷰는 레이아웃 내에 사용될 서로 다른 하위 뷰들로 초기화될 수 있다.
MyVStack { Text("Text 1") Text("Text 2") HStack { Image(systemName: "star.fill") Image(systemName: "star.fill") Image(systemName: "star") } } [!note] 이게 도대체 무슨소리지??? 전혀 이해를 못했음.
왜 커스텀 컨테이너 뷰가 필요한가?
struct MyVStack: View { var body: some View { VStack(spacing: 10) { Text("Text Item 1") Text("Text Item 2") Text("Text Item 3") } .font(.largeTitle) } } - MyVStack은 안에 들어갈 뷰들이 코드에 고정되어 있다.
- 이렇게 하면 껍데기는 재사용할 수 있지만, 내용은 바꿀 수 없다.
내가 원하는 것: 간격 10, 큰 폰트 등 껍데기는 같으나 그 안에 들어갈 내용만 매번 다르게 넣는 것. -> 그래서 컨테이너 뷰가 필요하고, 그 안에 들어갈 내용을 나중에(사용 시점에) 주입할 수 있어야 한다.
그걸 가능하게 하는 게 @ViewBuilder
일반적인 함수/클로저는 하나의 View만 반환할 수 있지만 SwiftUI의 @ViewBuilder을 붙이면, 클로저 안에
Text("A") Text("B") Image(systemName: "star") - 이렇게 여러 줄을 써도 컴파일러가 "한 덩어리의 View"로 자동 조립해준다. (내부적으로 TupleView, Group 등으로 묶이다. 일단은 여러개를 하나로 합쳐준다는 부분 기억!)
코드 읽는 법(제네릭 + 뷰빌더)
struct MyVStack<Content: View>: View { private let content: Content init(@ViewBuilder content: () -> content) { self.content = content() // 여기서 한 번 빌드해서 저장 } var body: some View { VStack(spacing: 10) { content } .font(.largeTitle) } } - MyVStack<Content: View> -> "안에 어떤 View가 오든(Content), 그것만 View면 돼요"라는 제네릭 선언
[!note] 제네릭 선언이 뭐지? 배운 기억이 안나는데??
제네릭이란, 형태는 하나이지만 안에 담기는 타입은 나중에 정하는 틀이다. 즉, 코드를 여러 타입에 재사용할 수 있게 만드는 문법이다.
struct Box { var item: String } - 이렇게 하면 Box는 문자열만 넣을 수 있다.
- 하지만 박스 안에 숫자도, 이미지도, 뷰도 넣고 싶다면? -> 제네릭을 쓰자.
제네릭 문법 기본형
struct Box<T> { // 아 이렇게 쓰는거구나... var item: T } - 여기서 T가 제네릭 선언
- T는 "타입 매개변수"라고 부르고, 이 자리에 어떤 타입이 들어올지 나중에 정한다는 뜻.
- T 대신에 무슨 이름이든 쓸 수 있다.(Content, Element 등)
제네릭 사용 예
let intBox = Box(item: 123) // T == Int let stringBox = Box(item: "Hello") // T == String let boolBox = Box(item: true) // T == Bool - 하나의 구조체 설계로 다양한 타입을 받을 수 있다.
SwiftUI에서 제네릭 선언이 자주 쓰이는 이유
SwiftUI는 "어떤 View가 들어올 지 모르는 상황"이 많다.
- 예를 들어 MyVStack<Content: View>는 이렇게 읽는다.
- <Content: View>
- 여기 들어올 타입 이름은 Content라고 부를 거고, 반드시 View 프로토콜을 따라야 한다!
- 이렇게 MyVStack 안에는 Text, Image, HStack, 심지어 다른 커스텀 뷰까지도 들어올 수 있게 된다.
SwiftUI 예시 해석
struct MyVStack<Content: View>: View { // Content가 T 역할, View 프로토콜 따르자 let content: Content // content 상수선언했는데, 이건 아까 말한 Content init(@ViewBuilder content: () -> Content) { self.content = content() // 초기화 작업. 이 부분에 대해 애초에 어색해서 어려워보인다 } var body: some View { VStack { content } } } - 이 말은 곧, "나는 View를 만드는 구조체인데, 안에 들어갈 content는 어떤 View 든 받아줄게. 대신 그 타입을 Content라고 부를게." 라고 선언한 것이다.
[!note] 일단 이정도 이해도에서 넘어가는 게 학습효과가 높을 거라 판단된다.
레이블 뷰로 작업하기
Label("Welcome to SwiftUI, systemName: "person.circle.fill") 
Label( title: { Text("Welcome to SwiftUI") .font(.largeTitle) }, icon: { Circle() .fill(Color.blue) .frame(width: 25, height: 25) } ) 
요약
SwiftUI의 사용자 인터페이스는 SwiftUI View 파일에 선언되며, View 프로토콜을 따르는 컴포넌트들로 구성된다. View 프로토콜을 따르도록 하기 위해 구조체는 View 자신이 body라는 이름의 프로퍼티를 포함해야 한다.
SwiftUI는 사용자 인터페이스 레이아웃을 설계하는 데 사용되는 내장 컴포넌트들의 라이브러리를 제공한다. 뷰의 모양과 동작은 수정자를 적용하여 구성할 수 있으며, 커스텀 뷰와 하위 뷰를 생성하기 위해 수정되거나 그루핑될 수 있다. 마찬가지로, 커스텀 컨테이너 뷰는 ViewBuilder 클로저 프로퍼티를 이용해 생성될 수 있다.
수정자를 뷰에 적용하면 새롭게 변경된 뷰가 반환되며, 그 다음에 오는 수정자가 다시 적용된다. 뷰에 수정자를 적용하는 순서가 중요한 영향을 주게 된다.