Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
SwiftUI/SwiftUI(Advanced)

6. @ViewBuilder / 뷰 빌더

by Toughie 2023. 6. 22.

🦁@ViewBuilder / 뷰 빌더🦁

SwiftUI에서 @ViewBuilder는 클로저로부터 여러 뷰를 만들 수 있는 커스텀 파라미터 속성래퍼이다.

뷰를 생성하는 함수나 프로퍼티에 적용되고, @ViewBuilder가 적용되어 있으면 여러 개의 뷰를 반환할 수 있고, 이 뷰들은 

자동으로 단일 뷰로 결합된다.이는 복잡한 뷰 계층 구조를 생성하는데 유용하다.


만약 화면 상단의 헤더, 즉 제목이나 간단한 설명, 이미지가 들어가는 뷰를 반복해서 써야 하는 경우라면?

struct HeaderViewRegular: View {
    
    let title: String
    let description: String?
    let iconName: String?
    var body: some View {
        VStack(alignment: .leading) {
            Text(title)
                .font(.largeTitle)
                .fontWeight(.semibold)
            if let description = description {
                Text(description)
                    .font(.callout)
            }
            if let iconName = iconName {
                Image(systemName: iconName)
            }
            RoundedRectangle(cornerRadius: 5)
                .frame(height: 2)
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding()
    }
}

title은 초기화 시 값을 꼭 받아와야하고, 나머지는 옵셔널이기 때문에 바인딩 처리를 해주고 있다.

구조를 보면 결국 VStack 안에 차례대로 title, description, icon이 쌓이고 있다.

여기서 제네릭을 활용하면 코드를 개선할 수 있다.(더 유연하게)


struct HeaderViewGeneric<Content: View>: View {
    
    let title: String
    let content: Content
    
    //to make closure
    init(title: String, @ViewBuilder content: () -> Content) {
        self.title = title
        self.content = content()
    }
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(title)
                .font(.largeTitle)
                .fontWeight(.semibold)
            
            content
            
            RoundedRectangle(cornerRadius: 5)
                .frame(height: 2)
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding()
    }
}

<Content: View> 즉 View프로토콜을 채택하는 타입만 허용하는 제네릭 문법을 사용했고

생성자에서 content 파라미터에 @ViewBuilder를 붙여줌으로써 Content 제네릭 뷰 타입을 반환하는 클로저를 사용할 수 있는 것이다.

 

아래는 동일한 문법으로 커스텀 HStack을 구현하는 예시이다.

struct CustomHStack<Content: View>: View {
    
    let content:  Content
    
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    var body: some View {
        HStack {
            content
        }
    }
}

...

            CustomHStack {
                Text("hi")
                Text("hi")
            }
            
            HStack {
                Text("hi")
                Text("hi")
            }

완전  동일하다.


만약 열거형을 활용해서 상황에 따라 다른 뷰를 그리고 싶다면? 아래와 같은 방식으로도 구현 가능하다.

struct LocalViewBuilder: View {
    
    enum ViewType {
        case one, two, three
    }
    
    let type: ViewType
    
    var body: some View {
        
        headerSection
    }
    
    @ViewBuilder private var headerSection: some View {
        
        switch type {
        case .one:
            viewOne
        case .two:
            viewTwo
        case .three:
            viewThree
        }
    }
    
    private var viewOne: some View {
        Text("one")
    }
    
    private var viewTwo: some View {
        VStack {
            Text("Two")
            Image(systemName: "heart.fill")
        }
    }
    
    private var viewThree: some View {
        Image(systemName: "paperplane")
    }
}

먼저 열거형으로 케이스를 나열하고, 초기화 시에 해당 열거형 케이스에 따라 다른 뷰를 그리는 구조이다.

여기서 body를 경량화, 정리하기 위해서 사용한 기법들에 대해 알아보자.

 

먼저 private var viewOne, viewTow, viewThree를 보자.

 

이들은 계산속성이다.(computed property)

계산 속성은 저장 속성과는 다르게 값을 저장하지 않고, 필요할 때마다 동적으로 계산해서 값을 반환하는 속성이다.

계산 속성을 사용하면 뷰가 실제로 사용되기 전까지는 계산이 이루어지지 않고 필요한 경우에만 계산되기 때문에

메모리를 절약하고 성능을 향상시킬 수 있다.

 

계산 속성은 원래 마지막에 ()를 붙여줘야 하지 않나요? 🤔

맞다. 여기서는 ()가 생략된 것이다. 생략이 가능한 이유는 SwiftUI에서 some View와 같은

*Opaque Return Type을 사용할 수 있기 때문이다.

-> 불투명 반환 타입은 Swift에서 제공하는 타입 시스템 중 하나로,

 함수나 계산 속성이 어떤 구체적인 타입을 반환하는지 외부에 노출시키지 않고, 추상화된 타입을 반환할 수 있게 해준다.

프로토콜 지향 프로그래밍에서 활용되는 개념이라고 한다..

구체적인 타입을 숨기고, 해당 타입이 채택하는 프로토콜의 관점에서 반환 값이나 프로퍼티를 사용하게 해준다고 이해하자!


그리고 body 내부에서 switch문을 아래와 같이 빼두었는데 여기서 @ViewBuilder를 적용하지 않으면

    @ViewBuilder private var headerSection: some View {
        
        switch type {
        case .one:
            viewOne
        case .two:
            viewTwo
        case .three:
            viewThree
        }
    }

아래와 같은 에러가 발생한다.

이는 계산 속성이 불투명한 반환 타입을 선언했지만, 반환할 구체적인 타입을 명시적으로 나타내는 반환 구문이 없기 때문이다.

클로저 구현부에서 반환 구문이 없어서 컴파일러가 컴파일러가 반환 타입 추론을 하지 못한다는 의미.

 

여기서 @ViewBuilder를 사용하면 컴파일러가 반환타입에 대한 추론을 자동으로 처리할 수 있게 된다.

즉 반환 구문을 명시적으로 작성하지 않아도 된다는 말. 컴파일러가 View 계층 구조를 분석해서 자동으로 반환 타입을 추론한다.


@ViewBuilder의 개념은 생각보다 어려운 것 같다.

다만 제네릭과 함께 더 유연한 코드 작성에서 필요한 개념이라는 것은 알겠다.

그리고 Opaque return type, 컴파일러의 타입 추론 등 재밌지만 아직은 확실히 와닿지 않는 개념들도 있는 것 같다 .🤔

앞으로 ViewBuilder를 더 써보면서 이해해 보도록 하지.