iOS 스터디 Part4 Result Builders

Advanced Swift (by Chris Eidhof) 책을 번역 후 참고하여 작성하였습니다.

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년 기준)

Last updated