@ViewBuilder는 평소에서 NavigationLink 또는 Button 같이 이미 사용하고 있습니다
위에서 확인했으면 어떤 식으로 사용해야되는지 느낌이 올 것입니다.
@ViewBuilder는 키워드의 이름처럼 뷰를 만들수 있게 해주는데 이 뷰는 클로저를 통해서 구현됩니다. @ViewBuilder는 사용자가 커스텀 뷰를 만들 때 사용될 수 있습니다.
@ViewBuilder 사용 전
struct HeaderViewRegular: View {
let title: String
let description: String?
let iconName: String?
var body: some View {
VStack(alignment: .leading) {
Text(title)
.font(.largeTitle)
.fontWeight(.semibold)
if let description = description {
Text(description)
.font(.callout)
}
if let iconName = iconName {
Image(systemName: iconName)
}
RoundedRectangle(cornerRadius: 5)
.frame(height: 2)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
}
HeaderViewRegular(title: "새로운 제목", description: "설명", iconName: "heart.fill")
@ViewBuilder 사용 후
다음은 @ViewBuilder를 활용하여 커스텀 뷰를 만든 코드입니다. 아래에서 확인할 수 있듯이 @ViewBuilder는 클로저를 통해 뷰를 입력받습니다.
struct HeaderViewGeneric<Content: View>: View {
let title: String
let content: Content
init(title: String, @ViewBuilder content: () -> Content) {
self.title = title
self.content = content()
}
var body: some View {
VStack(alignment: .leading) {
Text(title)
.font(.largeTitle)
.fontWeight(.semibold)
content
RoundedRectangle(cornerRadius: 5)
.frame(height: 2)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
}
HeaderViewGeneric(title: "Generic 제목") {
VStack {
Image(systemName: "heart.fill")
Text("Generic 설명")
Image(systemName: "heart.fill")
}
}
@ViewBuilder를 이용한 커스텀 HStack
@ViewBuilder를 사용하면 기존에 있는 HStack 또는 VStack 같은 것들도 직접 커스텀 구현할 수 있습니다.
struct CustomHStack<Content:View>:View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
HStack {
content
}
}
}
CustomHStack {
Text("HStack 1")
Text("HStack 2")
Text("HStack 3")
}.padding()
HStack {
Text("HStack 1")
Text("HStack 2")
Text("HStack 3")
}.padding()
Expression of type 'some View' is unused 에러
다음은 커스텀 @ViewBuilder를 사용하다가 발생할 수 있는 에러입니다.
struct LocalViewBuilder: View {
enum ViewType {
case one, two, three
}
let type: ViewType
var body: some View {
VStack {
headerSection
}
}
private var headerSection: some View {
if type == .one {
viewOne
} else if type == .two {
viewTwo
} else if type == .three {
viewThree
}
}
private var viewOne: some View {
Text("One!")
}
private var viewTwo: some View {
VStack {
Text("Two")
Image(systemName: "heart.fill")
}
}
private var viewThree: some View {
Image(systemName: "heart.fill")
}
}
Expression of type 'some View' is unused 에러가 발생합니다. 이것은 viewOne, viewTwo, viewThree 에서 some View를 반환하는데 이 반환되는 뷰가 사용되지 않았다는 것을 의미합니다.
첫 번째 해결방법 (추천 X)
해결 방법은 some View를 사용할 수 있는 @ViewBuilder를 사용하면 됩니다. 아래는 HStack 의 @ViewBuilder 클로저를 사용하여 각 반환되는 some View를 받을 수 있게 하였습니다.
private var headerSection: some View {
HStack {
if type == .one {
viewOne
} else if type == .two {
viewTwo
} else if type == .three {
viewThree
}
}
}
HStack의 @ViewBuilder를 사용하게 되면 다음과 같이 에러가 발생하지 않는 것을 확인할 수 있습니다.
두 번째 해결 방법 (추천O)
첫 번째 해결 방법에서는 HStack 같은 @ViewBuilder를 사용하여 반환받는 some View를 사용하였는데 headerSection 자체를 @ViewBuilder를 만들게 되면 해결할 수 있습니다.
@ViewBuilder private var headerSection: some View {
switch type {
case .one:
viewOne
case .two:
viewTwo
case .three:
viewThree
}
}
총 코드
import SwiftUI
struct HeaderViewRegular: View {
let title: String
let description: String?
let iconName: String?
var body: some View {
VStack(alignment: .leading) {
Text(title)
.font(.largeTitle)
.fontWeight(.semibold)
if let description = description {
Text(description)
.font(.callout)
}
if let iconName = iconName {
Image(systemName: iconName)
}
RoundedRectangle(cornerRadius: 5)
.frame(height: 2)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
}
struct HeaderViewGeneric<Content: View>: View {
let title: String
let content: Content
init(title: String, @ViewBuilder content: () -> Content) {
self.title = title
self.content = content()
}
var body: some View {
VStack(alignment: .leading) {
Text(title)
.font(.largeTitle)
.fontWeight(.semibold)
content
RoundedRectangle(cornerRadius: 5)
.frame(height: 2)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
}
struct CustomHStack<Content:View>:View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
HStack {
content
}
}
}
struct ContentView: View {
var body: some View {
VStack(alignment: .leading) {
HeaderViewRegular(title: "새로운 제목", description: "설명", iconName: "heart.fill")
HeaderViewRegular(title: "새로운 제목2", description: "설명2", iconName: nil)
HeaderViewGeneric(title: "Generic 제목") {
VStack {
Image(systemName: "heart.fill")
Text("Generic 설명")
Image(systemName: "heart.fill")
}
}
CustomHStack {
Text("HStack 1")
Text("HStack 2")
Text("HStack 3")
}.padding()
HStack {
Text("HStack 1")
Text("HStack 2")
Text("HStack 3")
}.padding()
Spacer()
}
}
}
#Preview {
// ContentView()
LocalViewBuilder(type: .three)
}
struct LocalViewBuilder: View {
enum ViewType {
case one, two, three
}
let type: ViewType
var body: some View {
VStack {
headerSection
}
}
@ViewBuilder private var headerSection: some View {
switch type {
case .one:
viewOne
case .two:
viewTwo
case .three:
viewThree
}
}
private var viewOne: some View {
Text("One!")
}
private var viewTwo: some View {
VStack {
Text("Two")
Image(systemName: "heart.fill")
}
}
private var viewThree: some View {
Image(systemName: "heart.fill")
}
}
인스타그램 회원가입 같은 비슷한 View에 버튼의 기능만 따로 동작하는 뷰를 여러개의 파일로 만들어서 관리하는 대신 하나의 뷰에서 @ViewBuilder를 사용하여 여러개의 뷰를 하나의 파일에서 관리하면 파일을 관리하기 편할 수 있겠다는 생각이 들었다.
'SwiftUI' 카테고리의 다른 글
RotationGesture 제스처로 돌리기 (0) | 2024.04.18 |
---|---|
MagnificationGesture 로 확대 축소하기 (0) | 2024.04.14 |
Generics (T)모든 타입에 대응하기 (0) | 2024.04.13 |
MatchedGeometryEffect (0) | 2024.04.12 |
@discardableResult 반환값을 무시하게 만들기 (0) | 2024.04.12 |