# ๐ฅจ @ViewBuilder๋?
์ ์ : ํด๋ก์ ๋ก ๋ฐ์ ๋ทฐ๋ฅผ ๊ตฌ์ฑํ๋ custom parameter attribute
์ฆ, ํด๋ก์ ๋ก ๋ฐ์ (child)view์ ๋ํ ์ดํธ๋ฆฌ๋ทฐํธ ํค์๋์ด๋ฉฐ, ์ฐ๋ฆฌ๊ฐ ํ์ฉํ ๋ ์ค์ํ๊ฑด closure ๋ด๋ถ์ (child)view ๋ฅผ ์ ๋ฌํ๋ค๋ ๊ฒ์ด๋ค.
* swift์์ ๋งํ๋ @์ดํธ๋ฆฌ๋ทฐํธ ํค์๋๋, ์ปดํ์ผ๋ฌ์๊ฒ ์ถ๊ฐ์ ์ธ ์ ๋ณด๋ฅผ ์๋ ค์ฃผ๋ ์ญํ ์ ํ๋ค.
# ๐ฅจ SwiftUI์์ ์ ์ํ Button์์๋ ๋ณผ ์ ์๋ @ViewBuilder
https://developer.apple.com/documentation/swiftui/button
Button | Apple Developer Documentation
A control that initiates an action.
developer.apple.com
SwiftUI์์ ์ ์ํ Button ์์ฑ์๋ ์๋์๊ฐ๋ค.
init(action: @escaping () -> Void, @ViewBuilder label: () -> Label)
์ฐ๋ฆฌ๊ฐ ์์ ์ ์์์ ํ์ธํ ์ฒ๋ผ label ํด๋ก์ ์ View๋ฅผ ์ ๋ฌํ๊ณ ์๋ค. ์ฐ๋ฆฌ๊ฐ ์ด์ฒ๋ผ ๋ทฐ๋ก ๊ตฌ์ฑ๋ label์ ํด๋ก์ ๋ก ์ ๋ฌ๋ฐ์ ์ ์๋ ๊ฒ๋ SwiftUI์์ ์ ์ํ Button์์ label ๋งค๊ฐ๋ณ์์ @ViewBuilder ํค์๋๊ฐ ๋ถ์ด์๊ธฐ ๋๋ฌธ์ด๋ค.
Button{
print("์ทจ์๋ฒํผ")
} label : {
Text("์ทจ์")
}
# ๐ฅจ @ViewBuilder ์ธ์ ์ฌ์ฉํ๋ฉด ์ข์๊น?
๊ณตํต์ ์ธ ๋ถ๋ชจ Container๋ฅผ ๋ง๋๋๋ฐ ์ ์ฉํ๋ค.
๋๊ฐ์ ๊ฒฝ์ฐ์๋ ์ฃผ๋ก ๋น์ทํ ๋ ์ด์์์ ๊ฐ๋ ๋ทฐ์์ ์ํฉ๋ง๋ค ๋ด๋ถ ๋ด์ฉ์ด ๋ฐ๋๋ ๊ฒฝ์ฐ์ ์ฌ์ฉํด์ฃผ์๋ค.์๋ฅผ๋ค์ด, ์ฑ์์ ์ฌ์ฉ์์๊ฒ ์ด๋ค ์ ๋ณด๋ฅผ ์๋ดํ๊ฑฐ๋ ์์ฌ๋ฅผ ํ์ธํด์ผํ ๋ ๋ชจ๋ฌ์ ๋์ฐ๊ณคํ๋ค. ๋ชจ๋ฌ์ ๋ด๋ถ ๋ทฐ์ ๋ ์ด์์์ด๋ ๊ตฌ์กฐ๋ ํฌ๊ฒ ์ฐจ์ด๋์ง ์๊ณ ์ํฉ๋ณ๋ก ๋ฒํผ์ด๋ ๋ด์ฉ์ ๋ฌธ๋จ ๊ฐ์ ๋ฑ์ด ๋ฐ๋๋ค. ์ด๋ฌํ ๊ฒฝ์ฐ์ ์ธ๋ถ์ ์ผ๋ก ๋ค๋ฅธ ๋ทฐ๋ค์ ํด๋ก์ ๋ก ๋ฐ์ผ๋ฉด ์ฌ์ฌ์ฉ๊ฐ๋ฅํ ๋ชจ๋ฌ๋ก ๋ชจ๋ํํ ์ ์๋ ๊ฒ์ด๋ค.

# ๐ฅจ ๊ณต์๋ฌธ์
ViewBuilder ๊ด๋ จํ ์ ํ ๊ณต์๋ฌธ์
https://developer.apple.com/documentation/swiftui/viewbuilder
ViewBuilder | Apple Developer Documentation
A custom parameter attribute that constructs views from closures.
developer.apple.com
๊ณต์๋ฌธ์์ ๋์จ ์์ ๋ฅผ ์ฌ์ฉํด์ ๋ทฐ๋ฅผ ๊ทธ๋ ค์ฃผ์๋ค.
import SwiftUI
struct MyStruct1 {
func contextMenu<MenuItems: View>(
@ViewBuilder menuItems: () -> MenuItems
) -> some View {
VStack{
Text("@ViewBuilder๋ก ๋ฐ์ ๋ทฐ ๋ณด์ฌ์ฃผ๊ธฐ")
.foregroundColor(.red)
menuItems()
}
}
}
struct ViewBuilderPracticeView: View {
let myStruct1 : MyStruct1 = MyStruct1()
var body: some View {
myStruct1.contextMenu { // ํด๋ก์ ๋ก ๋ทฐ๋ฅผ ์ ๋ฌํ๋ค
Text("Cut")
Text("Copy")
Text("Paste")
}
}
}
# ๐ฅจ ์ค์ ์ฑ์ ํ์ฉ
์ค์ ๋ก ์ฑ์์ ๋ชจ๋ฌ์ ๋ง๋ค ๋ ํ์ฉํ๋ ์ฝ๋ ์์๋ ์๋์ ๊ฐ๋ค.
//ํด๋ก์ ๋ก ๋ฐ๋ ๋ทฐ๋ค์ ์ ๋ค๋ฆญ์ผ๋ก ์ ์ํด์ค๋ค
struct CustomModal<H, C, F>: View where H: View, C : View, F: View {
@Binding var showingModal: Bool
var header : () -> H
var content : () -> C
var footer : () -> F
//์ด๊ธฐํ ์ฝ๋์์ @ViewBuilder ํค์๋๋ฅผ ์ฌ์ฉํ์ฌ ๊ตฌ์กฐ์ฒด ์ด๊ธฐํ ์ ํด๋ก์ ๋ก ๋ทฐ๋ฅผ ๋ฐ์ ์ ์๋๋กํ๋ค
//@escaping ํค์๋๊ฐ ์ฌ์ฉ๋ ์ด์ ๋ ๋ทฐ ์ด๊ธฐํ ์ค์ ๋ฐ๋ก ์ฌ์ฉ๋์ง ์๋๋ค๋ ์๋ฏธ์ด๋ค
init(
showingModal : Binding<Bool>,
@ViewBuilder header : @escaping () -> H,
@ViewBuilder content : @escaping () -> C,
@ViewBuilder footer : @escaping () -> F
) {
self._showingModal = showingModal
self.header = header
self.content = content
self.footer = footer
}
var body: some View {
VStack {
HStack{
Spacer()
Button{
showingModal = false
}label:{
Text("x")
.foregroundColor(.black)
}
}
header()
.padding(.vertical)
content()
.padding(.bottom)
footer()
.padding(.bottom)
}
.padding()
.background(Color.gray)
.frame(width: UIScreen.main.bounds.width*0.8)
.cornerRadius(12)
}
}
์์์ ์ ์ํ ๋ชจ๋ฌ ๋ทฐ๋ฅผ ์๋์ฒ๋ผ ํ์ฉํด์ค ์ ์๋ค.
struct ViewBuilderPracticeView: View {
@State var isShown : Bool = false
var body: some View {
ZStack{
Button{
self.isShown = true
} label : {
Text("ํํดํ์๊ฒ ์ต๋๊น?")
.asGreenToggleButton(isGreen : true)
.frame(width : 200)
.padding()
}
if isShown {
CustomModal(
showingModal : $isShown,
header : {
Text("์ ๋ง ํํดํ์๊ฒ ์ต๋๊น?")
},
content : {
Text("์ง๊ธ ํํดํ์๋ฉด ์ฌ์ฉํ๋ ๊ธฐ๋ก๋ค์ด ์ญ์ ๋ฉ๋๋ค ๊ทธ๋๋ ํํด๋ฅผ ์งํํ์๊ฒ ์ต๋๊น?")
},
footer : {
HStack{
Button{
print("์ทจ์๋ฒํผ")
self.isShown = false
} label : {
Text("์ทจ์")
.asGreenToggleButton(isGreen : true)
}
Button{
print("ํ์ธ๋ฒํผ")
self.isShown = false
} label : {
Text("ํ์ธ")
.asGreenToggleButton(isGreen : false)
}
}
}
)
}
}
}
}
์๋์ ๊ฐ์ ๋ทฐ๋ฅผ ๋ง๋ค ์ ์๋ค.
