머리말
결론
(애플이 아닌) 직접 만든 코드에는 하위 뷰를 담을 수 있게 하는 로직이 들어가 있지 않기 때문에
- `View` 프로토콜의 `body` 프로퍼티와 SwiftUI에 내장된 구조체에는 이미 `@ViewBuilder`가 구현되어 있어서 아무런 의심 없이 굳이 `@ViewBuilder` 프로퍼티 래퍼를 감싸지 않아도 된다.
- 하지만 직접 프로퍼티를 만들거나 구조체를 만들어서 이를 child view로 넣고 싶다면, `@ViewBuilder`를 활용해야 한다.
- Custom 구조체를 구현할 때 이니셜라이저에 `@ViewBuilder`를 넣어야 한다.
왜 Custom Wrapper의 이니셜라이저에 `@ViewBuilder`가 들어가는가
강의 시간에 커스텀 Wrapper를 이용하여 SwiftUI의 Navigation에 대한 버전 대응을 외부화 하도록 하는 것을 배웠다. 하지만 맨 마지막에 `@ViewBuilder`를 넣는 이유가 이해가 잘 되지 않았다.
struct NavigationWrapper<T: View>: View {
var content: T
// 📌 `@ViewBuilder`가 왜 들어가야 하는가?
init(@ViewBuilder content: () -> T) {
self.content = content() // T에 클로저를 호출하여 그에 대한 반환 값을 할당
}
var body: some View {
if #available(iOS 16.0, *) { // iOS 16 이상일 경우
NavigationStack {
content
}
} else {
NavigationView { // iOS 16 미만일 경우
content
}
}
}
}
실제 SwiftUI View에 적용
잘 동작하는데 왜 굳이 `@ViewBuilder` 넣지?
import SwiftUI
struct StudyNavigationView: View {
var body: some View {
NavigationWrapper(content: { // 📌 init에 `@ViewBuilder`를 빼도 잘만 동작한다
VStack(spacing: 30, content: {
Text("Placeholder")
.font(.largeTitle)
.foregroundStyle(.black)
sampleView
})
})
}
var sampleView: some View {
isTest ? Text("Hi") : Text("Bye")
}
}
본문
에러 발생
NavigationWrapper에 VStack이 아닌 child view로 `Text()`를 넣는다면?
VStack을 빼고 곧바로 자식 뷰로 `Text()`를 넣으면 "`View` 프로토콜을 준수하지 않는다"라는 에러를 발생시킨다.
struct NavigationWrapper<T: View>: View {
var content: T
init(content: () -> T) { // 📌 `@ViewBuilder`생략
self.content = content()
}
struct StudyNavigationView: View {
var isTest = false
var body: some View {
NavigationWrapper(content: {
Text("ABC") // 🔥 ERROR: Type '()' cannot conform to 'View'
}
}
}
SwiftUI에 구현한 `View` 프로토콜과 구조체에는 이미 `@ViewBuilder`가 들어가 있다.
이미 `View` 프로토콜의 `var body` 프로퍼티와, 해당 프로토콜을 채택한 SwiftUI의 구조체인 `VStack`, `HStack` 등에는 이미 `@ViewBuilder`가 구현되어 있다...!
한번 살펴 보자.
`View` 프로토콜과 `var body`프로퍼티
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol View {
associatedtype Body : View
// 📌 여기
@ViewBuilder @MainActor var body: Self.Body { get }
}
`VStack`
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen public struct VStack<Content> : View where Content : View {
@inlinable public init(
alignment: HorizontalAlignment = .center,
spacing: CGFloat? = nil,
@ViewBuilder content: () -> Content // 📌 여기
)
public typealias Body = Never
}
`HStack`
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen public struct HStack<Content> : View where Content : View {
@inlinable public init(
alignment: VerticalAlignment = .center,
spacing: CGFloat? = nil,
@ViewBuilder content: () -> Content // 📌 여기
)
/// The type of view representing the body of this view.
///
/// When you create a custom view, Swift infers this type from your
/// implementation of the required ``View/body-swift.property`` property.
public typealias Body = Never
}
이처럼 이미 SwiftUI에서 구현한 것들은 `@ViewBuilder`가 있다.
이니셜라이저에 프로퍼티 래퍼 활용하기
struct NavigationWrapper<T: View>: View { /* ViewModifier가 아닌 View */
var content: T
/* 이니셜라이저 파라미터로 클로저를 이용 */
/* `@ViewBuilder`를 이용해 child view 생성이 가능해짐 */
init(@ViewBuilder content: () -> T) {
self.content = content() // T에 클로저를 호출한 반환 값을 할당
}
struct StudyNavigationView: View {
var isTest = false
var body: some View {
NavigationWrapper(content: {
Text("ABC") // init에 정의된 `@ViewBuilder`로 child view 생성 가능
VStack(spacing: 30, content: {
//...
})
})
}
'iOS > SwiftUI' 카테고리의 다른 글
SwiftUI) "@State"로 구조체의 프로퍼티를 변경하기 (feat. 프로퍼티 래퍼; Property Wrappers) (2) | 2023.11.14 |
---|---|
[SwiftUI] OT + 자료 모음 (0) | 2023.04.23 |
댓글