← 메인으로

0813-1 커스텀 컨테이너 뷰

커스텀 컨테이너 뷰 만들기

하위 뷰는 뷰 선언부를 작고 가벼우며 재사용할 수 있는 블록으로 나누는 유용한 방법을 제공하지만, 컨테이너 뷰의 content가 정적(static)이라는 한계가 있다. 다시 말해, 하위 뷰가 레이아웃에 포함되는 시점에 하위 뷰에 포함될 뷰를 동적으로 지정할 수 없다. 하위 뷰에 포함되는 뷰들은 최초 선언부에 지정된 하위 뷰들뿐이다.

struct MyVStack: View { var body: some View { VStack(spacing: 10) { Text("Text Item 1") Text("Text Item 2") Text("Text Item 3") } .font(.largeTitle) } } 

하지만, 간격이 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) } } 
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) } } 

내가 원하는 것: 간격 10, 큰 폰트 등 껍데기는 같으나 그 안에 들어갈 내용만 매번 다르게 넣는 것. -> 그래서 컨테이너 뷰가 필요하고, 그 안에 들어갈 내용을 나중에(사용 시점에) 주입할 수 있어야 한다.

그걸 가능하게 하는 게 @ViewBuilder

일반적인 함수/클로저는 하나의 View만 반환할 수 있지만 SwiftUI의 @ViewBuilder을 붙이면, 클로저 안에

Text("A") Text("B") Image(systemName: "star") 

코드 읽는 법(제네릭 + 뷰빌더)

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

[!note] 제네릭 선언이 뭐지? 배운 기억이 안나는데??

제네릭이란, 형태는 하나이지만 안에 담기는 타입은 나중에 정하는 틀이다. 즉, 코드를 여러 타입에 재사용할 수 있게 만드는 문법이다.

struct Box { var item: String } 

제네릭 문법 기본형

struct Box<T> { // 아 이렇게 쓰는거구나... var item: T } 

제네릭 사용 예

let intBox = Box(item: 123) // T == Int let stringBox = Box(item: "Hello") // T == String let boolBox = Box(item: true) // T == Bool 

SwiftUI에서 제네릭 선언이 자주 쓰이는 이유

SwiftUI는 "어떤 View가 들어올 지 모르는 상황"이 많다.

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 } } } 

[!note] 일단 이정도 이해도에서 넘어가는 게 학습효과가 높을 거라 판단된다.

레이블 뷰로 작업하기

Label("Welcome to SwiftUI, systemName: "person.circle.fill") 
Pasted image 20250813100227.png
Label( title: { Text("Welcome to SwiftUI") .font(.largeTitle) }, icon: { Circle() .fill(Color.blue) .frame(width: 25, height: 25) } ) 
Pasted image 20250813100411.png

요약

SwiftUI의 사용자 인터페이스는 SwiftUI View 파일에 선언되며, View 프로토콜을 따르는 컴포넌트들로 구성된다. View 프로토콜을 따르도록 하기 위해 구조체는 View 자신이 body라는 이름의 프로퍼티를 포함해야 한다.

SwiftUI는 사용자 인터페이스 레이아웃을 설계하는 데 사용되는 내장 컴포넌트들의 라이브러리를 제공한다. 뷰의 모양과 동작은 수정자를 적용하여 구성할 수 있으며, 커스텀 뷰와 하위 뷰를 생성하기 위해 수정되거나 그루핑될 수 있다. 마찬가지로, 커스텀 컨테이너 뷰는 ViewBuilder 클로저 프로퍼티를 이용해 생성될 수 있다.

수정자를 뷰에 적용하면 새롭게 변경된 뷰가 반환되며, 그 다음에 오는 수정자가 다시 적용된다. 뷰에 수정자를 적용하는 순서가 중요한 영향을 주게 된다.