on
[얼죽아] UIView 계층 구조를 더 직관적으로 표현하기
UIView 계층 구조를 더 직관적으로 표현하기
문제 상황
iOS 개발을 하다보면 View의 계층 구조가 복잡해집니다. 그리고 UIKit에서 addSubview를 호출해서 하위 View를 추가하는데 이 방식이 View의 계층 구조를 한눈에 파악하기 어렵다는 문제가 있었습니다. 그래서 이런 문제를 Swift의 Result Builder를 활용해 해결하고자 했습니다.
원했던 View 계층 구조 표현
SwiftUI처럼 View의 계층 구조를 선언적으로 정의하면, 명시적인 addSubview 호출 없이도 자동으로 하위 View들이 적절한 계층으로 구성되도록 하고 싶었습니다.
class SomeView: BaseView {
@ViewHierarchyBuilder
override var viewHierarchy: ViewHierarchy {
headerView {
titleLabel
descriptionLabel
}
contentView {
contentHeaderView {
badgeView
nameLabel
}
button
}
}
}
Result Builder
컴파일 타임에 Swift 컴파일러가 일반 코드를 변환하는 DSL(Domain-Specific-Language) 구축 기능입니다. 컴파일 타임에 변환이 이루어져서 런타임 오버헤드가 없습니다. 또한 SwiftUI에서 View를 구축하는데 사용되는 기능이기도 합니다.
사용법
@resultBuilder
struct StringBuilder {
// 1. 기본 컴포넌트 변환
static func buildExpression(_ expression: String) -> String {
expression
}
// 2. 컴포넌트들을 하나의 결과로 결합
static func buildBlock(_ components: String...) -> String {
components.joined(separator: "\n")
}
// 3. 조건부 컴포넌트 처리
static func buildOptional(_ component: String?) -> String {
component ?? ""
}
// 4. if-else 처리
static func buildEither(first component: String) -> String {
component
}
static func buildEither(second component: String) -> String {
component
}
// 5. 배열 처리
static func buildArray(_ components: [String]) -> String {
components.joined(separator: "\n")
}
// 6. 실패 가능한 상황 처리
static func buildLimitedAvailability(_ component: String) -> String {
component
}
}
이 중 buildExpression
와 buildBlock
이 가장 중요한데
@StringBuilder
var greeting: String {
"Hello" // 컴파일러가 buildExpression("Hello")로 변환
"World" // 컴파일러가 buildExpression("World")로 변환
}
라는 코드가 컴파일러에서 다음과 같이 변환합니다.
var greeting: String {
return buildBlock(
buildExpression("Hello"),
buildExpression("World")
)
}
Result Builder를 활용한 기본 구현
public struct ViewGraph {
public let views: [UIView]
public init(views: [UIView]) {
self.views = views
}
}
@resultBuilder
public struct ViewGraphBuilder {
// 단일 View를 ViewGraph로 변환
public static func buildExpression(_ expression: UIView) -> ViewGraph {
return ViewGraph(views: [expression])
}
// [UIView] 배열을 ViewGraph로 변환
public static func buildExpression(_ expression: [UIView]) -> ViewGraph {
return ViewGraph(views: expression)
}
// 여러 ViewGraph를 하나로 결합
public static func buildBlock(_ components: ViewGraph...) -> ViewGraph {
return ViewGraph(views: components.flatMap { $0.views })
}
}
이 구현만으로는 최상위 View들을 하나의 ViewHierarchy로 통합할 수 있지만, 각 View의 하위 계층 구조는 아직 처리되지 않습니다. 따라서 각 View의 하위 View들을 상위 View의 subview로 자동 추가하는 로직이 추가로 필요합니다.
callAsFunction
Swift 5.2에서 도입된 기능으로, 타입의 인스턴스를 함수처럼 호출할 수 있게 해주는 메서드입니다. 그런데 타입의 인스턴스를 함수처럼 호출할 수 있게 해준다는 말이 잘 와닿지 않아서 예시를 보면 바로 이해가 갈 듯 합니다.
struct NumberMultiplier {
let multiplier: Int
func callAsFunction(_ number: Int) -> Int {
return number * multiplier
}
}
// 사용 예시
let doubler = NumberMultiplier(multiplier: 2)
let result = doubler(4) // 8
callAsFunction을 활용한 마무리
extension UIView {
@discardableResult
public func callAsFunction(@ViewHierarchyBuilder _ builder: () -> ViewHierarchy) -> UIView {
builder().views.forEach { addSubview($0) }
return self
}
}
그리고 BaseView의 initializer에서 viewHierarchy.views.forEach { addSubview($0) }
만 추가하면 완성입니다.
이렇게하면 앞에서 보여준 이 코드만 선언하면 SomeView와 그 하위 View들의 계층을 선언적으로 표현할 수 있고, addSubview
를 일일히 호출할 필요도 없어집니다.
class SomeView: BaseView {
@ViewHierarchyBuilder
override var viewHierarchy: ViewHierarchy {
headerView {
titleLabel
descriptionLabel
}
contentView {
contentHeaderView {
badgeView
nameLabel
}
button
}
}
}
컴파일러의 Result Builder 변환 과정
이해하기 쉽도록 컴파일러에서 변환된 코드로 보여드리겠습니다.
class SomeView: BaseView {
override var viewHierarchy: ViewHierarchy {
ViewHierarchyBuilder.buildBlock(
// headerView 블록
headerView.callAsFunction {
ViewHierarchyBuilder.buildBlock(
ViewHierarchyBuilder.buildExpression(titleLabel),
ViewHierarchyBuilder.buildExpression(descriptionLabel)
)
},
// contentView 블록
contentView.callAsFunction {
ViewHierarchyBuilder.buildBlock(
// contentHeaderView 블록
contentHeaderView.callAsFunction {
ViewHierarchyBuilder.buildBlock(
ViewHierarchyBuilder.buildExpression(badgeView),
ViewHierarchyBuilder.buildExpression(nameLabel)
)
},
ViewHierarchyBuilder.buildExpression(button)
)
}
)
}
}
가장 안쪽의 buildExpression부터 실행되어 각 UIView가 ViewHierarchy로 변환되고, buildBlock을 통해 상위 레벨에서 이들을 결합합니다. 각 뷰의 callAsFunction은 하위 뷰들을 자동으로 addSubview 해주므로, 결과적으로 선언적 구문이 실제 뷰 계층 구조로 자연스럽게 변환됩니다.