Result Builders.
함수나 프로퍼티에 여러 개의 표현식을 결합하여 하나의 값으로 만들 수 있게 해주는 특별한 종류의 함수
예제로 HStack 함수를 보면 @ViewBuilder
를 확인할 수 있고 실제로 Builder 함수입니다.
struct HStack<Content>: View where Content: View {
public init(
alignment: VerticalAlignment = .center,
spacing: CGFloat? = nil,
@ViewBuilder content: () -> Content)
// ...
}
우리는 이 함수를 사용하여 아래와 같이 각기 다른 3가지 표현식을 사용합니다.
HStack {
Text("Finish the Advanced Swift Update")
Spacer()
Button("Complete") { /* ... */ }
}
이 부분은 컴파일러가 View Builders의 buildBlock이라는 정적 메소드를 사용하여 결합시킵니다.
@resultBuilder public struct ViewBuilder {
// ...
public static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2)
-> TupleView<(C0, C1, C2)>
where C0: View, C1: View, C2: View
// ...
}
위 HStack를 다시 작성하면 아래와 같이 쓸 수 있습니다.
HStack {
return ViewBuilder.buildBlock(
Text("Finish the Advanced Swift Update"),
Spacer(),
Button("Complete") { /* ... */ }
)
}
결과적으로 @resultBuilder
를 반환하게 되는데 여러 개의 표현식을 정적 메소드를 활용하여 하나로 만들어주는 함수라고 생각된다.
Blocks and Expressions.
@resultBuilder는 하나 이상의 buildBlock 메서드를 요구하고 빌더 메소드는 기본적으로 buildBlock, buildExpression 두 가지 정적 메소드를 사용할 수 있습니다.
기본적으로는 buildBlock
를 사용하지만,
다양한 유형의 표현식에서는 buildExpression
을 먼저 적용하고 사용하는 방식도 가능합니다.
예를 들어,
아래와 같이 buildBlock
을 선언하고 @StringBuilder를 작성하면
static func buildBlock(_ strings: String...) -> String { strings.joined()
}
@StringBuilder func greeting() -> String {
"Hello, "
"World!"
}
greeting() // Hello, World!
Swift에서는 컴파일러에 의해 아래와 같이 재작성된다.
func greeting_rewritten() -> String {
StringBuilder.buildBlock(
"Hello, ",
"World!"
)
}
이것을 유형에 따라 buildExpression
으로 표현해줄 수 있다.
static func buildExpression(_ s: String) -> String { s
}
static func buildExpression(_ x: Int) -> String { "\\(x)"
}
let planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
@StringBuilder func greetEarth() -> String {
"Hello, Planet "
planets.!rstIndex(of: "Earth")!
"!"
}
greetEarth() // Hello, Planet 2!
Swift에서는 컴파일러에 의해 아래와 같이 재작성된다.
func greetEarth_rewritten() -> String {
StringBuilder.buildBlock(
StringBuilder.buildExpression("Hello, Planet "),
StringBuilder.buildExpression(planets.!rstIndex(of: "Earth")!),
StringBuilder.buildExpression("!")
)
}
하나의 유형으로 사용할 때는 일반적으로 buildBlock
만으로도 충분하지만
buildExpression
를 적절히 이용하여 준다면 다양한 유형을 지원할 수 있을 것 같다.
Overloading Builder Methods.
빌더 함수에서 사용할 수 없는 유형의 경우 buildExpression
를 이용하여 사용하도록 할 수도 있다.
never 유형을 overloading하므로 fatalError를 사용할 수 있게 된다.
static func buildExpression(_x: Never) -> String {
fatalError()
}
void 유형을 overloading하므로 인쇄할 수 있게 된다.
static func buildExpression(_x: Void) -> String {
""
}
지원되지 않는 유형에 대한 정확한 컴파일러 진단을 위해 쓸 수도 있다.
@available(*, unavailable,
message: "String Builders only support string and integer values")
static func buildExpression<A>(_ expression: A) -> String {
""
}
Conditions.
buildIf 및 buildEither를 활용하여 builder에 if, else, switch 기능 지원
buildIf
: 조건 충족하지 않으면 nil.
static func buildIf(_s: String?) -> String {
s??""
}
@StringBuilder func greet(planet: String) -> String {
"Hello, Planet"
if let idx = planets.!rstIndex(of: planet) {
""
idx }
"!"
}
greet(planet: "Earth") // Hello, Planet 2!
greet(planet: "Sun") // Hello, Planet!
Swift에서는 컴파일러에 의해 아래와 같이 재작성된다.
func greet_rewritten(planet: String) -> String {
let v0 = "Hello, Planet"
var v1: String?
if let idx = planets.!rstIndex(of: planet) {
v1 = StringBuilder.buildBlock(
StringBuilder.buildExpression(" "),
StringBuilder.buildExpression(idx)
)
}
let v2 = StringBuilder.buildIf(v1)
return StringBuilder.buildBlock(v0, v2)
}
buildEither
: 조건이 거짓일 때 컨텐츠 생성
static func buildEither(!rst component: String) -> String {
component
}
static func buildEither(second component: String) -> String {
component
}
@StringBuilder func greet2(planet: String) -> String {
"Hello, "
if let idx = planets.!rstIndex(of: planet) {
switch idx {
case 2:
"World"
case 1, 3:
"Neighbor"
default:
"planet "
idx+1
}
}else{
"unknown planet"
}
"!"
}
greet2(planet: "Earth") // Hello, World!
greet2(planet: "Mars") // Hello, Neighbor!
greet2(planet: "Jupiter") // Hello, planet 5!
greet2(planet: "Pluto") // Hello, unknown planet!
Swift에서는 컴파일러에 의해 아래와 같이 재작성된다.
func greet2_rewritten(planet: String) -> String {
let v0 = StringBuilder.buildExpression("Hello, ")
let v1: String
if let idx = planets.!rstIndex(of: planet) {
let v1_0: String
switch idx {
case 2:
v1_0 = StringBuilder.buildEither(!rst:
StringBuilder.buildBlock(StringBuilder.buildExpression("World"))
)
case 1, 3:
v1_0 = StringBuilder.buildEither(second: StringBuilder.buildEither(!rst:
StringBuilder.buildBlock(StringBuilder.buildExpression("Neighbor"))
)
)
default:
let v1_0_0 = StringBuilder.buildExpression("planet") let v1_0_1 = StringBuilder.buildExpression(idx + 1) v1_0 = StringBuilder.buildEither(second:
StringBuilder.buildEither(second: StringBuilder.buildBlock(v1_0_0, v1_0_1)
)
)
}
v1 = StringBuilder.buildEither(!rst: v1_0)
} else{
v1 = StringBuilder.buildEither(second:
StringBuilder.buildBlock(
StringBuilder.buildExpression("unknown planet")
)
)
}
let v2 = StringBuilder.buildExpression("!")
return StringBuilder.buildBlock(v0, v1, v2)
}
Loops.
buildArray
를 활용하여 for..in 반복문을 이용하는 기능 지원
static func buildArray(_ components: [String]) -> String {
components.joined(separator: "")
}
@StringBuilder func greet3(planet: String?) -> String {
"Hello "
if let p=planet{ p
} else{
for p in planets.dropLast() {
"\\(p), "
}
"and \\(planets.last!)!"
}
}
greet3(planet: nil)
// Hello Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune!
Swift에서는 컴파일러에 의해 아래와 같이 재작성된다.
func greet3_rewritten(planet: String?) -> String {
let v0 = StringBuilder.buildExpression("Hello ") let v1: String
if let p=planet{
v1 = StringBuilder.buildBlock(StringBuilder.buildExpression(p))
}else{
var v1_0: [String] = []
for p in planets.dropLast() {
let v1_0_0 = StringBuilder.buildBlock(
StringBuilder.buildExpression("\\(p), ")
)
v1_0.append(v1_0_0)
}
let v1_1 = StringBuilder.buildArray(v1_0)
let v1_2 = "and \\(planets.last!)!"
v1 = StringBuilder.buildBlock(v1_1, v1_2)
}
return StringBuilder.buildBlock(v0, v1)
}
Other Build Methods.
@resultBuilder struct StringBuilder {
static func buildBlock(_ x: [String]...) -> [String] { x.!atMap { $0 } }
static func buildIf(_ x: [String]?) -> [String] { x ?? [] }
static func buildExpression(_ x: String) -> [String] { [x] }
static func buildExpression(_ x: Int) -> [String] { ["\\(x)"] }
static func buildExpression(_ x: Never) -> [String] {}
static func buildExpression(_ x: Void) -> [String] { [] }
static func buildArray(_ x: [[String]]) -> [String] { x.!atMap { $0 } }
static func buildEither(!rst x: [String]) -> [String] { x }
static func buildEither(second x: [String]) -> [String] { x }
static func buildFinalResult(_ x: [String]) -> String { x.joined() }
}
buildLimitedAvailability
static func buildLimitedAvailability(_ component: Component) -> Component
// iOS 15.0 미만에서 사용할 코드
// 제한된 가용성 컨텍스트에서 AnyView로 Type Erasure
buildLimitedAvailability {
Text("Requires iOS 15+")
}
제한된 가용성 컨텍스트를 다룰 수 있습니다.
buildFinalResult
: 최종 결과는 해당 메서드에 대한 호출이 됩니다. 이 변환은 항상 마지막 입니다.
Unsupported Statements.
위에서 지원하는 if/if let, if ... else, switch, for ... in를 제외한 guard, defer, do ... catch, break, continue를 포함한 거의 모든 다른 문은 지원되지 않습니다.(2022년 기준)