2023. 9. 16. 14:57ㆍApple/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")
}
}
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 구문이 포함될 수 있다.
Drawable 프로토콜은 선이나 모양 같이 그릴 수 있는 항목에 대한 요구사항을 정의하고, 타입은 draw() 메소드를 구현해야 한다.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() } }
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(몰입해서 깨닫고 왜 사용하는 지 알고 쓴다는것..!?) 한다는 게 아직은 낯설고 쉽지가 않다.. 남을 이해시킬 수 있는 글이 되어야 나 자신도 더 잘 알 수 있을테니 말이다..)
'Apple > SwiftUI' 카테고리의 다른 글
@PropertyWrapper - ObservableObject, EnvironmentObject (수정중..) (0) | 2023.11.12 |
---|---|
Property Wrappers 에 대해 알아보자 - State, Binding ① (0) | 2023.11.04 |
ViewBuilder 는 어떻게 만드는 걸까? - ② (0) | 2023.10.07 |
[ SwiftUI ] SwiftUI 기본 특징 (0) | 2023.09.05 |