카테고리 없음

[iOS/SwiftUI] @ViewBuilder

게게겍 2025. 2. 17. 15:30

ViewBuilder

A custom parameter attribute that constructs views from closures. closure 에서 view를 구성하는 사용자 정의 매개변수 속성입니다.

 

코드로 바로 예시를 들어보자면!

struct ContentView: View {
    var body: some View {
        VStack {  // 여기서 ViewBuilder가 동작
            Text("안녕하세요")
            Text("반갑습니다")
            Button("클릭") {
                print("버튼 클릭됨")
            }
        }
    }
}

위 코드에서 VStack 내부의 여러 뷰들이 쉼표없이 자연스럽게 나열이 되어있는데 이것이 가능한 이유가 @ViewBuilder입니다.

public init(@ViewBuilder content: () -> Content)

@ViewBuilder는 여기서 한 개 이상의 뷰를 받아서 하나의 뷰로 조합을 해줍니다.

내부 동작 원리

스위프트가 Function Builder를 인식하면, 함수(혹은 이니셜라이저) 내부에 있는 구문들을 자동으로 buildBlock 메서드에 넘겨 하나의 결과물로 바인딩해서 넘겨줍니다.

VStack {
    Text("첫 번째")
    Text("두 번째")
}

이 코드는 내부적으로

VStack(content: {
    return ViewBuilder.buildBlock(
        Text("첫 번째"),
        Text("두 번째")
    )
})

이렇게 변환됩니다.

@ViewBuilder의 암시적/명시적 사용

SwiftUI에서 @ViewBuilder는 두 가지 방식으로 사용됩니다

1. 암시적(Implicit) 사용

body 프로퍼티는 @ViewBuilder가 암시적으로 적용이 되어있습니다.

struct ContentView: View {
// 암시적으로 @ViewBuilder가 적용되어 있음
    var body: some View {
        Text("Hello")
        Text("World")
    }
}

하지만 body외의 다른 프로퍼티나 메소드는기본적으로 ViewBuilder로 유추(infer)하지 않기 때문에 @ViewBuilder를 명시적으로 넣어야 합니다.

2. 명시적(Explicit) 사용

struct ContentView: View {
// ❌ 컴파일 에러: 여러 뷰를 직접 반환할 수 없음
    var headerViews: some View {
        Text("제목")
        Text("부제목")
    }

// ✅ @ViewBuilder를 명시적으로 추가
    @ViewBuilder
    var headerViews: some View {
        Text("제목")
        Text("부제목")
    }

// ✅ 메서드에도 동일하게 적용
    @ViewBuilder
    func createHeaderViews() -> some View {
        Text("제목")
        Text("부제목")
    }
}

실 활용 예제

1. 커스텀 컨테이너 뷰

struct CardView<Content: View>: View {
    let content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 10)
                .fill(.white)
                .shadow(radius: 5)
            content
                .padding()
        }
    }
}

// 사용 예시
CardView {
    VStack {
        Text("제목")
        Image(systemName: "star")
        Text("내용")
    }
}

2. 네비게이션 매니저

final class NavigationManager: ObservableObject {
    @Published var selectedTab: TabType = .map
    @Published var mapPath: [ViewType] = []

    @ViewBuilder
    func build(_ view: ViewType) -> some View {
        switch view {
        case .searchView:
            SearchView()
        case .detailView(let postId):
            DetailView(postId: postId)
        case .searchLocationView(locationId: let locationId,
                               locationTitle: let locationTitle):
            SearchLocation(locationId: locationId,
                         locationTitle: locationTitle)
        }
    }
}

3. 조건부 렌더링

struct ConditionalView: View {
    let isEnabled: Bool

    var body: some View {
        VStack {
            Text("항상 표시")
            if isEnabled {
                Text("조건부 표시")
            }
        }
    }
}