본문 바로가기
iOS/SwiftUI

SwiftUI) @ViewBuilder를 왜 쓸까 (feat. Property Wrapper)

by Dev.Andy 2023. 11. 23.

머리말

결론

(애플이 아닌) 직접 만든 코드에는 하위 뷰를 담을 수 있게 하는 로직이 들어가 있지 않기 때문에

- 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: {
//...
})
})
}

댓글