ViewBuilder 란 무엇일까? - ①

2023. 9. 16. 14:57Mobile/SwiftUI

클로저에서 뷰를 구성하는 사용자 정의 매개변수 속성!

 

@resultBuilder
struct ViewBuilder

ViewBuilder 를 하위 뷰 생성 클로저 매개 변수에 대한 매개변수 속성으로 사용하여 해당 클로저가 여러 하위 뷰를 제공할 수 있도록 해준다. 예를 들어, contextMenu 함수는 뷰 빌더를 통해 하나 이상의 뷰를 생성하는 클로저를 허용한다.

NOTE
참고로,, contextMenu 는 watchOS 6.0 - 7.0 에서 deprecated(더 이상 사용되지 않음..) 되었따..

func contextMenu<MenuItems>(@ViewBuilder menuItems: () -> MenuItems) -> some View where MenuItems : View
Text("Turtle Rock")
    .padding()
    .contextMenu {
        Button {
            // Add this item to a list of favorites.
        } label: {
            Label("Add to Favorites", systemImage: "heart")
        }
        Button {
            // Open Maps and center it on this item.
        } label: {
            Label("Show in Maps", systemImage: "mappin")
        }
    }

contextMenu 를&nbsp; 구현한 모습


ViewBuilder(뷰빌더) 를 사용하는 이유?!

앱의 다양한 기능에 공통의 View 가 필요한 경우일 때, SwiftUI 자체 필수 기본 구성요소인 @ViewBuilder 가 도움이 되어준다!

다양한 기능에서 공통의 View가 들어갈 때, 각각의 뷰에서 코드를 복사하고, 각 기능을 수정하게 된다면, 또 하나씩 하나씩 일일이 수정해줘야 할수도 있다. 꽤나 번거로운 일이 되겠지..

그래서 이런 번거로움이 발생할 수도 있는 것을 반복가능한 코드, 재사용 가능한 View 로 추출하는 것이다.

코드의 중복은 개발 시 더 깊은 문제가 발생할 수 있다. lists 중 하나를 업데이트 하고 다른 목록에서 동일한 변경을 수행하는 것을 잊어버리면 나중에 불일치가 발생할 수 있기 때문이다.

lists 들 모두 동일하게 나타나야 하며, 표시되는 데이터만 변경된다. 빌드하는 코드를 반복함으로써 무언가를 추가하거나 변경해야 할 때마다 동일한 코드를 반복하는 모든 곳에서 업데이트 해야한다.

다른 View 들에서 List View 에서 무언가를 변경할 때마다 각각의 뷰들에서 업데이트 하는 것을 의미한다. 소스코드는 더 크고, 유지관리가 어려워지고, 빌드하는데 더 오랜시간이 걸리게 된다.

열거형(Enum)을 사용해 View 의 종류를 선택

List 내 추가되는 각 뷰에 대해 열거형을 생성해 문제를 해결할 수도 있다. 새로운 기능이 List 내부에 사용자 정의 view 를 요구할 때마다 열거형에 새 사례를 추가하고 내부에 더 많은 view 를 추가해야 하므로 보기가 더욱 커진다. 그래서 @ViewBuilder 를 사용해야한다.

ViewBuilder 이해하기

@viewBuilder 는 하위 view 생성을 돕기 위해 특별히 설계된 일종의 result builder 다. Result builders 는 시퀀스의 요소로부터 결과를 작성하는 함수를 생성한다. SwiftUI 는 이를 native views, 컨트롤, 컴포넌트들에서 사용한다. body 에서도 또한 이를 사용해 뷰를 구성하게된다. 

result builders 를 사용해 Domain Specific Language- 도메인 특정언어, or DSL 도 만들 수 있다. DSL 은 특정 영역이나 영역 내의 문제를 해결하는 데 사용되는 언어 내 작은 언어(?) 와도 같다.

 

NOTE - Result Builder 
자연스럽고 선언적 방식으로 List 나 Tree 같은 중첩 데이터를 생성하기 위한 구문을 추가하는 사용자 정의 타입이다.
조건부나 반복되는 데이터 조각을 처리하기 위해 if 및 for 와 같은 일반적인 Swift 구문이 포함될 수 있다. 
protocol Drawable {
    func draw() -> String
}
struct Line: Drawable {
    var elements: [Drawable]
    func draw() -> String {
        return elements.map { $0.draw() }.joined(separator: "")
    }
}
struct Text: Drawable {
    var content: String
    init(_ content: String) { self.content = content }
    func draw() -> String { return content }
}
struct Space: Drawable {
    func draw() -> String { return " " }
}
struct Stars: Drawable {
    var length: Int
    func draw() -> String { return String(repeating: "*", count: length) }
}
struct AllCaps: Drawable {
    var content: Drawable
    func draw() -> String { return content.draw().uppercased() }
}​
Drawable 프로토콜은 선이나 모양 같이 그릴 수 있는 항목에 대한 요구사항을 정의하고, 타입은 draw() 메소드를 구현해야 한다.


initializer 를 호출해 이러한 유형으로 그림 만드는 것이 가능해진다.
let name: String? = "Ravi Patel"
let manualDrawing = Line(elements: [
     Stars(length: 3),
     Text("Hello"),
     Space(),
     AllCaps(content: Text((name ?? "World") + "!")),
     Stars(length: 2),
])
print(manualDrawing.draw())
// Prints "***Hello RAVI PATEL!**"​

뒤에 중첩된 괄호는 가독성이 떨어진다. 연산자를 사용해 인라인으로 수행하면 World 를 대체하는 로직인 경우 복잡해질 가능성도 높아진다. 그래서 Result Builder 를 써서 적용해보는 코드를 살펴보면, 우선 정의를 하기위해선 @resultBuilderDrawingBuilder 라는 result Builder 속성의 사용자 정의를 지정해줬다. 
@resultBuilder
struct DrawingBuilder {
    static func buildBlock(_ components: Drawable...) -> Drawable {
        return Line(elements: components)
    }
    static func buildEither(first: Drawable) -> Drawable {
        return first
    }
    static func buildEither(second: Drawable) -> Drawable {
        return second
    }
}​

struct 는 result Builder 구문의 일부를 구현하는 세가지 메소드를 정의하고, 메서드 안 코드 블록에 일련의 줄을 작성한다. 

위처럼 정의하면, 
함수의 매개변수에 속성을 적용시 함수에 전달된 클로저를 result Builder 가 해당 클로저에서 생성하는 값으로 바꿀 수 있다. @DrawingBuilder 에서는 
func draw(@DrawingBuilder content: () -> Drawable) -> Drawable {
    return content()
}
func caps(@DrawingBuilder content: () -> Drawable) -> Drawable {
    return AllCaps(content: content())
}


func makeGreeting(for name: String? = nil) -> Drawable {
    let greeting = draw {
        Stars(length: 3)
        Text("Hello")
        Space()
        caps {
            if let name = name {
                Text(name + "!")
            } else {
                Text("World!")
            }
        }
        Stars(length: 2)
    }
    return greeting
}
let genericGreeting = makeGreeting()
print(genericGreeting.draw())
// Prints "***Hello WORLD!**"


let personalGreeting = makeGreeting(for: "Ravi Patel")
print(personalGreeting.draw())
// Prints "***Hello RAVI PATEL!**"​

 

argument(인자? 매개변수?) 를 사용해 custom 된 인사말을 그릴수 있다. Attribute @ 로 표시된 단일 클로저를 인수로 사용한다.
해당 함수를 사용할 때, 정의하는 특수 구문을 사용한다. SwiftUI 는 그림에 대한 선언적 설명을 함수 인수로 전달된 값을 구축! 빌드! 하기 위한 메서드에 대한 일련의 호출로 변환한다. 

ViewBuilder 의 클로저 사용하기!

뷰빌더를 사용하면 본문 내부에서 이를 사용해 뷰를 빌드하는 이니셜라이저에 클로저를 추가할 수 있다. 이렇게 하면 SwiftUI view 파일의 내부에 뷰를 하드코딩하는 대신 view 생성 responsibility(책임) 을 사용할 기능에 전달할 수 있다.

많은 SwiftUI 뷰는 이미 @ViewBuilder 를 사용하고 있다. 예를 들어,, Button 에는 Label 을 작성하기 위해 @ViewBuilder 클로저를 사용하는 초기화 프로그램 init(action: label:) 이 있다.

Button(action: signIn) {
    Label("Sign In", systemImage: "arrow.up")
}

VStack 이나 HStack 등 들도 이미 @ViewBuilder 를 사용해서 모든 종류의 뷰를 content(콘텐츠) 로 가져온다.

init(alignment: HorizontalAlignment, spacing: CGFloat?, content: () -> Content)
var body: some View {
    VStack(
        alignment: .leading,
        spacing: 10
    ) {
        ForEach(
            1...10,
            id: \.self
        ) {
            Text("Item \($0)")
        }
    }
}

다음에는 뷰빌더의 더 명확한 형태, 적용 코드를 더 찾아서, 블로그에 기록해야겠다..

-

글은 더 추가되거나 수정 및 삭제될 수도 있습니다. 추가 의견이나, 잘못된 정보에 대한  피드백 환영합니다! 

( Deep Dive(몰입해서 깨닫고 왜 사용하는 지 알고 쓴다는것..!?) 한다는 게 아직은 낯설고 쉽지가 않다.. 남을 이해시킬 수 있는 글이 되어야 나 자신도 더 잘 알 수 있을테니 말이다..)