UIKit에서도 Subview를 선언적으로 구성하기

UIKit에서도 Subview를 선언적으로 구성하기

SwiftUI는 View 계층 구조를 한눈에 파악하기 좋습니다. 그래서 UIKit에서도 해보고 싶었습니다.

이런 생각에서 시작해서 SubviewHierarchy라는 작은 라이브러리를 만들었습니다. UIKit 프로젝트에서 SwiftUI스러운 선언형 문법으로 View를 구성할 수 있게 해주는 도구입니다.

1. 왜 만들었는가

SwiftUI를 사용하면 아래와 같이 직관적으로 View 계층을 표현할 수 있습니다.

VStack {
    Text("Hello")
    Image(systemName: "star")
}

이렇게 UI 계층 구조가 코드로 자연스럽게 드러나는 것이 SwiftUI의 큰 장점 중 하나입니다. 하지만 UIKit에서는 addSubview(_:)를 계속 써야 하고, 중첩될수록 가독성이 급격히 떨어집니다.

그래서 “UIKit에서도 SwiftUI처럼 선언형으로 subview를 구성할 수 없을까?” 하는 생각이 들었습니다.

2. 기존 UIKit의 문제점

일반적으로 UIKit 코드에서는 다음과 같이 View 계층을 구성합니다.

let container = UIView()
let label = UILabel()
let button = UIButton()

container.addSubview(label)
container.addSubview(button)

문제는 뷰가 많아지고 중첩될수록 코드만 봐서는 구조를 파악하기 어렵다는 점입니다.

stackView.addSubview(container)
container.addSubview(box)
box.addSubview(button)

이걸 보고 어떤 구조인지 바로 이해하긴 어렵습니다. 그래서 SwiftUI의 장점을 UIKit에서도 써보고 싶었습니다.

3. @resultBuilder + callAsFunction으로 DSL 구현

Swift 5.4부터 지원하는 @resultBuildercallAsFunction()을 활용하면 SwiftUI 스타일 DSL을 UIKit에서도 만들 수 있습니다.

view {
    box1 {
        box2
        box3
    }
}

이런 코드를 가능하게 만든 핵심 구성은 아래와 같습니다:

@resultBuilder
public struct SubviewBuilder {
    public static func buildExpression(_ expression: UIView) -> SubviewHierarchy { ... }
    public static func buildBlock(_ components: SubviewHierarchy...) -> SubviewHierarchy { ... }
    ...
}

extension UIView {
    public func callAsFunction(@SubviewBuilder _ builder: () -> SubviewHierarchy) -> UIView {
        builder().views.forEach { addSubview($0) }
        return self
    }
}

여기서 SubviewHierarchy는 여러 UIView를 감싸는 중간 구조입니다. 그리고 callAsFunction 덕분에 UIView()를 함수처럼 사용할 수 있게 만들었습니다.

4. 테스트 코드 작성 및 커버리지 100%

이 DSL은 직관적이지만, 내부적으로 subview가 잘 추가되는지 확인이 필요했습니다. 그래서 다양한 테스트 코드를 작성했습니다:

@Test
func testSimpleHierarchy() {
    let container = UIView()
    let label = UILabel()
    let button = UIButton()

    container {
        label
        button
    }

    #expect(container.subviews == [label, button])
}

Apple에서 만든 swift-testing 프레임워크를 사용했고, 테스트 커버리지 100%를 달성했습니다.

5. 해결한 문제와 앞으로의 방향

🎯 해결한 것들

🛣️ 앞으로 해보고 싶은 것들

  1. Reactive 상태 관리 도입
    • 조건 분기에서 상태값 변경 시 자동 업데이트 지원
  2. 동적 뷰 업데이트
    • 빈번한 뷰 추가/제거가 발생하는 상황에서의 성능 개선
    • UICollectionView의 DiffableDataSource처럼 이전 선언과 새로운 선언의 차이만 적용하는 최적화

이 프로젝트는 UIKit 기반 코드를 유지하면서도 SwiftUI의 선언형 장점을 가져오고 싶은 팀이나 개발자에게 꽤 도움이 될 수 있다고 생각합니다.

🔗 링크