@resultBuilder struct ViewBuilder
View를 선언적으로 생성하는 resultBuilder
DSL(SwiftUI View 생성 전용 언어). 복수의 하위 View를 생성하는 클로저 매개변수에 @ViewBuilder
특성(Attribute)을 사용하는 것이 일반적이다.
swift는 사용자가 내장 기능을 편리하게 사용할 수 있도록 다양한 특성(Attribute) 키워드를 제공한다. ViewBuilder
는 선언적 언어를 쉽게 제작할 수 있는 resultBuilder
특성을 사용하여 구현된 특성이다.
```swift
// 다양한 특성 키워드들...
@objc
@available
@Published
@IBAction
@resultBuilder
...
```
resultBuilder와 DSL에 대해 자세히 알아보고 싶다면 이전 글을 참고하자: Swift Attributes: @resultBuilder
일례로, SwiftUI 라이브러리에 정의된 Button
View의 생성자는 클로저 매개변수 label
에 @ViewBuilder
특성이 붙어 있으므로 여러 View들이 생성되는 클로저를 전달받을 수 있다.
Button.init(action: @escaping () -> Void, @ViewBuilder label: () -> Label)
Button
View 이외에도 HStack
, List
, TabView
등의 컨테이너 View들은 모두 @ViewBuilder
특성을 통해 사용자가 클로저에 복수의 View를 선언할 수 있도록 지원하고 있다.
struct ContentView: View {
var body: some View {
HStack { // HStack 생성자의 클로저 매개변수는 @ViewBuilder 특성을 가지고 있다.
Text("1")
Text("2")
Text("3")
}
}
}
View
프로토콜의 body
프로퍼티 선언에도 @ViewBuilder
특성이 붙어있기 때문에, View
를 상속받는 모든 사용자 정의 View의 body
에는 @ViewBuilder
를 붙이지 않아도 클로저 내에서 복수의 View 선언이 가능하다.
protocol View {
associatedtype Body: View
@ViewBuilder var body: Self.Body { get }
// 프로토콜 명세에 @ViewBuilder 특성이 정의되어 있다.
}
struct ContentView: View {
var body: some View { // body는 @ViewBuilder 특성을 상속받는다.
Text("1")
Text("2")
Text("3")
}
}
ViewBuilder를 활용하여 사용자 정의된 컨테이너 View를 만들기도 한다. 기본값이 변형된 HStack
을 만들어보자. HStack
의 생성자는 다음과 같이 정의되어있다:
// HStack 정의에서...
init(alignment: VerticalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)
위 정의에서 spacing
기본값을 20
으로 설정한 변형버전 HStack
을 구현해볼 수 있다.
struct SpacedHStack<Content: View>: View {
var alignment: VerticalAlignment
var spacing: CGFloat?
var content: Content
// 1. ViewBuilder를 사용해 클로저를 인자로 받는다.
init(alignment: VerticalAlignment = .bottom, spacing: CGFloat? = 20.0, @ViewBuilder content: () -> Content) {
self.alignment = alignment
self.spacing = spacing
// 2. 클로저를 View로 변환한다.
self.content = content()
}
var body: some View {
HStack(alignment: alignment, spacing: spacing) {
// 3. 변환된 View를 사용한다.
content
}
}
}
이렇게 구현한 View는 다른 곳에서 자유롭게 사용할 수 있다.
struct ContentView: View {
var body: some View {
SpacedHStack {
Text("1")
Text("2")
Text("3")
}
}
}
다음은 resultBuilder
로부터 제공받은 명세에 맞추어 ViewBuilder에 구현된 함수들이다. 아래 친절한 설명이 나와있으니, 겁먹지 말고 빠르게 넘어가자.
// buildBlock 오버로딩 타입 메소드 11개
static func buildBlock() -> EmptyView
static func buildBlock<Content>(Content) -> Content
static func buildBlock<C0, C1>(C0, C1) -> TupleView<(C0, C1)>
static func buildBlock<C0, C1, C2>(C0, C1, C2) -> TupleView<(C0, C1, C2)>
static func buildBlock<C0, C1, C2, C3>(C0, C1, C2, C3) -> TupleView<(C0, C1, C2, C3)>
static func buildBlock<C0, C1, C2, C3, C4>(C0, C1, C2, C3, C4) -> TupleView<(C0, C1, C2, C3, C4)>
static func buildBlock<C0, C1, C2, C3, C4, C5>(C0, C1, C2, C3, C4, C5) -> TupleView<(C0, C1, C2, C3, C4, C5)>
static func buildBlock<C0, C1, C2, C3, C4, C5, C6>(C0, C1, C2, C3, C4, C5, C6) -> TupleView<(C0, C1, C2, C3, C4, C5, C6)>
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7>(C0, C1, C2, C3, C4, C5, C6, C7) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7)>
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8>(C0, C1, C2, C3, C4, C5, C6, C7, C8) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8)>
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)>
// if-else문 및 switch문 지원
static func buildEither<TrueContent, FalseContent>(first: TrueContent) -> _ConditionalContent<TrueContent, FalseContent>
static func buildEither<TrueContent, FalseContent>(second: FalseContent) -> _ConditionalContent<TrueContent, FalseContent>
// 단일 if문 지원
// buildOptional과 동일한 함수이다. 왜 이름이 2개인 것인지는 잘 모르겠다.
static func buildIf<Content>(Content?) -> Content?
// if-#available문 지원
static func buildLimitedAvailability<Content>(Content) -> AnyView
위 함수들은 클로저 내의 선언적 구문을 일반적인 swift 구문으로 번역하기 위해 사용된다. 특히 buildBlock
은 resultBuilder
특성에서 필수적으로 요구하는 함수로, 클로저 내에서 나열된 View 표현식을 읽어들인 다음 하나의 View로 묶어 반환한다. 따라서,
EmptyView
를 반환한다.TupleView
를 반환한다.우리가 EmptyView
나 TupleView
를 직접 만들어 사용할 일은 없겠지만, SwiftUI가 자동으로 이러한 View를 만들어냄은 기억하자. 또한 클로저 내에서 11개 이상의 View를 일차원적으로 나열할 수 없는 이유도 알 수 있다. 11개 이상의 인자를 받는 TupleView
및 buildBlock
을 구현하지 않았기 때문이다.
buildEither
함수와 buildIf
함수, buildLimitedAvailability
함수가 정의되어 있으므로 우리는 클로저 내에서 다음을 사용할 수 있다.
if
문if-else
문switch
문if #available
구문if #available
구문을 사용한 예시는 다음과 같다. 다음 예제에서는 기본적으로 LazyVStack
을 사용하지만, LazyVStsck
을 사용할 수 없는 구버전에서는 VStack
으로 대체할 수 있도록 지원하고 있다.
import SwiftUI
@available(macOS 10.15, iOS 13.0, *)
struct ContentView: View {
var body: some View {
ScrollView {
if #available(macOS 11.0, iOS 14.0, *) { // 버전별로 사용할 View를 나눌 수 있다.
LazyVStack {
ForEach(1...1000, id: \.self) { value in
Text("Row \(value)")
}
}
} else {
VStack {
ForEach(1...1000, id: \.self) { value in
Text("Row \(value)")
}
}
}
}
}
}
buildEither
문은 if
문과 else
문의 View 타입 정보를 모두 반환하기 때문에, buildLimitedAvailability
함수는 구 OS 버전에서의 컴파일 에러를 피하기 위해 LazyVStack
타입을 AnyView
타입으로 래핑해서 감춰버린다.