@ViewBuilder는 평소에서 NavigationLink 또는 Button 같이 이미 사용하고 있습니다

 

 

 

위에서 확인했으면 어떤 식으로 사용해야되는지 느낌이 올 것입니다.

 

@ViewBuilder는 키워드의 이름처럼 뷰를 만들수 있게 해주는데 이 뷰는 클로저를 통해서 구현됩니다. @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()
    }
}
HeaderViewRegular(title: "새로운 제목", description: "설명", iconName: "heart.fill")

 

@ViewBuilder 사용 후

다음은 @ViewBuilder를 활용하여 커스텀 뷰를 만든 코드입니다. 아래에서 확인할 수 있듯이 @ViewBuilder는 클로저를 통해 뷰를 입력받습니다.

struct HeaderViewGeneric<Content: View>: View {

    let title: String
    let content: Content
    
    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()
    }
}
HeaderViewGeneric(title: "Generic 제목") {
    VStack {
        Image(systemName: "heart.fill")
        Text("Generic 설명")
        Image(systemName: "heart.fill")
    }
}

 

 

 

@ViewBuilder를 이용한 커스텀 HStack

@ViewBuilder를 사용하면 기존에 있는 HStack 또는 VStack 같은 것들도 직접 커스텀 구현할 수 있습니다.

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

HStack {
    Text("HStack 1")
    Text("HStack 2")
    Text("HStack 3")
}.padding()

Expression of type 'some View' is unused 에러

다음은 커스텀 @ViewBuilder를 사용하다가 발생할 수 있는 에러입니다.

struct LocalViewBuilder: View {
    
    enum ViewType {
        case one, two, three
    }
    
    let type: ViewType
    
    var body: some View {
        VStack {
            headerSection
        }
    }
    
    private var headerSection: some View {
        if type == .one {
            viewOne
        } else if type == .two {
            viewTwo
        } else if type == .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: "heart.fill")
    }
}

Expression of type 'some View' is unused 에러가 발생합니다. 이것은 viewOne, viewTwo, viewThree 에서 some View를 반환하는데 이 반환되는 뷰가 사용되지 않았다는 것을 의미합니다.

 

첫 번째 해결방법 (추천 X)

해결 방법은 some View를 사용할 수 있는 @ViewBuilder를 사용하면 됩니다. 아래는 HStack 의 @ViewBuilder 클로저를 사용하여 각 반환되는 some View를 받을 수 있게 하였습니다.

private var headerSection: some View {
    HStack {
        if type == .one {
            viewOne
        } else if type == .two {
            viewTwo
        } else if type == .three {
            viewThree
        }
    }
}

 

HStack의 @ViewBuilder를 사용하게 되면 다음과 같이 에러가 발생하지 않는 것을 확인할 수 있습니다.

 

두 번째 해결 방법 (추천O)

첫 번째 해결 방법에서는 HStack 같은 @ViewBuilder를 사용하여 반환받는 some View를 사용하였는데 headerSection 자체를 @ViewBuilder를 만들게 되면 해결할 수 있습니다.

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

 

 

총 코드

import SwiftUI

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()
    }
}

struct HeaderViewGeneric<Content: View>: View {
    
    let title: String
    let content: Content
    
    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()
    }
}

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

struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading) {
            HeaderViewRegular(title: "새로운 제목", description: "설명", iconName: "heart.fill")
            
            HeaderViewRegular(title: "새로운 제목2", description: "설명2", iconName: nil)
            
            HeaderViewGeneric(title: "Generic 제목") {
                VStack {
                    Image(systemName: "heart.fill")
                    Text("Generic 설명")
                    Image(systemName: "heart.fill")
                }
            }
            
            CustomHStack {
                Text("HStack 1")
                Text("HStack 2")
                Text("HStack 3")
            }.padding()
            
            HStack {
                Text("HStack 1")
                Text("HStack 2")
                Text("HStack 3")
            }.padding()
            
            Spacer()
        }
    }
}

#Preview {
    //    ContentView()
    LocalViewBuilder(type: .three)
}

struct LocalViewBuilder: View {
    
    enum ViewType {
        case one, two, three
    }
    
    let type: ViewType
    
    var body: some View {
        VStack {
            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: "heart.fill")
    }
}

 

 

인스타그램 회원가입 같은 비슷한 View에 버튼의 기능만 따로 동작하는 뷰를 여러개의 파일로 만들어서 관리하는 대신 하나의 뷰에서 @ViewBuilder를 사용하여 여러개의 뷰를 하나의 파일에서 관리하면 파일을 관리하기 편할 수 있겠다는 생각이 들었다.

 

 

 

 

ytw_developer