๐ @ViewBuilder ๋ ?
: SwiftUI์์ ์ฌ๋ฌ View๋ฅผ ๊ทธ๋ฃนํํด์ ๋ฐํํ ์ ์๋๋ก ๋์์ฃผ๋ ํน๋ณํ ๋น๋ ๋ฌธ๋ฒ.
๋ณดํต Swift ํจ์๋ return์ ํ๋์ ๊ฐ์ ๋ฐํํ์ง๋ง, ViewBuilder๋ฅผ ์ฌ์ฉํ๋ฉด ๋ค์์ View๋ฅผ ์ ์ธ์์ผ๋ก ๋์ดํ ์ ์๊ฒ ํด์ค.
- ๊ฐ๋จํ ์์
struct MyView: View {
var body: some View {
VStack {
Text("Hello")
Text("World")
}
}
}
-> VStack ์ trailing closure๋ ViewBuilder๋ก ์ ์๋ ํด๋ก์ ์ด๊ธฐ ๋๋ฌธ์ Text๋ฅผ ์ฌ๋ฌ ๊ฐ ๋์ด ํ ์ ์์
โ ViewBuilder๊ฐ ์์ผ๋ฉด?
func makeView() -> some View {
// ์ด๋ ๊ฒ๋ ์๋จ (์ปดํ์ผ ์๋ฌ)
Text("Hello")
Text("World")
}
Swift๋ ๊ธฐ๋ณธ์ ์ผ๋ก ํ๋์ View๋ง ๋ฐํํ ์ ์๋๋ฐ,
ViewBuilder๋ฅผ ์ฐ๋ฉด ์ฌ๋ฌ View๋ฅผ ๋ง์น ํ๋์ธ ๊ฒ์ฒ๋ผ ๋ฌถ์ด์ค ์ ์์
โ ViewBuilder๋ฅผ ์ง์ ์ฌ์ฉํ ์์
@ViewBuilder
func greetingView() -> some View {
Text("Hello")
Text("World")
}
- @ViewBuilder๋ฅผ ๋ถ์ด๋ฉด ์ฌ๋ฌ View๋ฅผ ๋ฐํํ๋ ํจ์๊ฐ ๊ฐ๋ฅํด์ ธ.
- Swift๊ฐ ์๋์ผ๋ก ๋ฌถ์ด์ ์ฒ๋ฆฌํด์ค.
โ ViewBuilder์ ์ฅ์
- SwiftUI์ ์ ์ธ์ UI ๋ฌธ๋ฒ์ ๊น๋ํ๊ฒ ์ฌ์ฉํ ์ ์์
- ๋ทฐ ์กฐํฉ ์ ๊ฐ๋ ์ฑ์ด ๋์์ง
- ์กฐ๊ฑด๋ฌธ๊ณผ ๋ฐ๋ณต๋ฌธ์ ์ ์ฐํ๊ฒ ์ ์ฉ ๊ฐ๋ฅ
VStack {
if isLoggedIn {
Text("Welcome!")
} else {
Text("Please log in.")
}
}
-> ์ด๋ฐ ์กฐ๊ฑด ๋ถ๊ธฐ๋ ์์ฐ์ค๋ฝ๊ฒ ์ธ ์ ์์.
์ฌ์ค @ViewBuilder๋ Swift์ @resultBuilder ๋ฌธ๋ฒ์ ํ์ฉํด์ ๋ง๋ '์ปค์คํ ๋น๋' ์ค ํ๋
SwiftUI๋ฅผ ์ ๋๋ก ์ดํดํ๋ ค๋ฉด @resultBuilder์ ๊ตฌ์กฐ๋ฅผ ์๋๊ฒ ์ข์.
โ @resultBuilder๋?
- ๋ค์ํ ํํ์๋ค์ ํ๋์ ์ต์ข ๊ฒฐ๊ณผ๋ก ํฉ์ณ์ฃผ๋ ๊ธฐ๋ฅ์ ๊ฐ์ง Swift์ ํน๋ณํ ์ดํธ๋ฆฌ๋ทฐํธ
- "์ฌ๋ฌ ์ค์ ์ฝ๋๋ฅผ ํ ๋ฉ์ด๋ฆฌ๋ก ํฉ์ณ์ฃผ๋ ๊ท์น"์ ์ ์ํ ์ ์๊ฒ ํด์ค.
- SwiftUI์ ViewBuilder, SwiftRegexBuilder, StringBuilder ๋ฑ์ ์ฌ์ฉ๋จ
โ ๊ธฐ๋ณธ ๊ตฌ์กฐ
@resultBuilder๋ ์ปดํ์ผ๋ฌ๊ฐ ์ด๋ค ํจ์๋ฅผ ์ด๋ป๊ฒ ํด์ํ ์ง ์๋ ค์ฃผ๋ "์ปดํ์ผ๋ฌ ๊ฐ์ด๋" ๊ฐ์ ์ญํ ์ ํด.
@resultBuilder
struct ArrayBuilder {
static func buildBlock(_ components: Int...) -> [Int] {
return components
}
}
@ArrayBuilder
func makeArray() -> [Int] {
1
2
3
}
// ๊ฒฐ๊ณผ: [1, 2, 3]
- buildBlock ๋ฉ์๋: ์ฌ๋ฌ ํํ์์ ๋ฌถ์ด์ฃผ๋ ์ญํ
โ ํ์ ๋ฉ์๋
| ๋ฉ์๋ | ์ญํ |
| buildBlock | ์ฌ๋ฌ ํํ์์ ํ๋๋ก ๋ฌถ์ |
| buildOptional | if ๋ฌธ์์ ์ฌ์ฉ, nil์ผ ์๋ ์์ |
| buildEither(first:) / buildEither(second:) | if-else์์ ๋ถ๊ธฐ ์ ํ |
| buildArray | ForEach ๊ฐ์ ๋ฐ๋ณต๋ฌธ ์ง์ |
| buildLimitedAvailability | @available ๊ฐ์ ์กฐ๊ฑด ์ฒ๋ฆฌ |
๐ ์์ธํ ๋ณด๊ธฐ
1. buildBlock
static func buildBlock(_ components: View...) -> View { }
- VStack { Text("a") ; Text("b") } ์ฒ๋ผ ์ฌ๋ฌ View๋ฅผ ํ๋๋ก ๋ฌถ์ ๋ ์ฌ์ฉ
2. buildEither
static func buildEither(first: View) -> View { }
static func buildEither(second: View) -> View { }
- if, else ๋ฌธ์ ์ฌ์ฉํ ๋ ์ปดํ์ผ๋ฌ๊ฐ ์๋์ผ๋ก ์ด ๋ฉ์๋๋ฅผ ํธ์ถ
3. buildOptional
static func buildOptional(_ component: View?) -> View { }
- if ๋จ๋ ๋ฌธ์์ optional ๊ฐ์ ์ฒ๋ฆฌ ํ ๋ ํธ์ถ
4. buildArray
static func buildArray(_ components: [View]) -> View { }
- ForEach ๊ฐ์ ๋ฐ๋ณต๋ฌธ ์ฒ๋ฆฌํ ๋ ํธ์ถ
โ SwiftUI์ ViewBuilder ๋ด๋ถ
@resultBuilder
public struct ViewBuilder {
public static func buildBlock(_ components: some View...) -> some View { ... }
public static func buildOptional(_ component: some View?) -> some View { ... }
public static func buildEither(first component: some View) -> some View { ... }
public static func buildEither(second component: some View) -> some View { ... }
public static func buildArray(_ components: [some View]) -> some View { ... }
}
๐ ์ฆ, SwiftUI์์
VStack {
if isOn { Text("ON") }
else { Text("OFF") }
}
์ด๋ ๊ฒ ์์ฑํ๋ฉด ์ค์ ๋ก๋ ViewBuilder.buildEither๊ฐ ํธ์ถ ๋๊ณ , ์ปดํ์ผ๋ฌ๊ฐ ๋ค ์กฐ๋ฆฝํด์ฃผ๋ ๊ฒ !
๋ค์ ViewBuilder๋ก ๋์์์,
๊ทธ๋ผ ViewBuilder๋ฅผ ์ง์ ์ฌ์ฉํ๋ฉด ์ข์ ๊ฒฝ์ฐ๋ ์ธ์ ์ผ๊น ?
โ ViewBuilder๋ฅผ ์ง์ ์ฌ์ฉํ๋ฉด ์ข์ ๊ฒฝ์ฐ
1. ์ฌ๋ฌ View๋ฅผ ๋ฐํํ๋ ์ปค์คํ ํจ์๋ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค ๋
@ViewBuilder
func CustomSection(title: String) -> some View {
Text(title)
.font(.headline)
Divider()
}
๐ @ViewBuilder ๋๋ถ์ Text์ Divider๋ฅผ ํจ์์์ ์ฌ๋ฌ ์ค๋ก ๋ฐ๋ก ๋ฆฌํด ๊ฐ๋ฅ.
2. ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ปดํฌํดํธ๋ฅผ ๋ง๋ค ๋
- ํนํ SwiftUI์์๋ Slot API ๊ฐ์ ๋์์ธ์ ๊ตฌํํ ๋ ViewBuilder๊ฐ ์ ๋ง ์ ์ฉํด.
* Slot API๋ "๋ด๊ฐ ๋ง๋ ์ปดํฌ๋ํธ ์์, ๋ค๋ฅธ ์ฌ๋์ด '์ํ๋ View'๋ฅผ ๊ฝ์ ๋ฃ์ ์ ์๊ฒ ๋ง๋ ๊ตฌ์กฐ"
struct CustomCard<Content: View>: View {
let content: () -> Content
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
var body: some View {
VStack {
content()
}
.padding()
.background(Color.gray.opacity(0.2))
.cornerRadius(12)
}
}
๐ ์ด๋ ๊ฒ ๋ง๋ค๋ฉด ๋ด๋ถ์ ์ํ๋ View๋ฅผ ๋ง์๋๋ก ๋๊ฒจ์ค ์ ์์.
CustomCard {
Text("Hello")
Image(systemName: "star")
}
์ด๊ฒ VStack { }์ฒ๋ผ ๋์ํ๋ ์๋ฆฌ.
(์ฌ์ค VStack๋ ์ด๋ฐ ์์ผ๋ก ViewBuilder๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ์)
โ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ปดํฌ๋ํธ ๋ง๋ค๊ธฐ ์ถ๊ฐ ์์
1) ๊ทธ๋ฅ ์ผ๋ฐ ์ปดํฌ๋ํธ
struct BasicCard: View {
var body: some View {
VStack {
Text("Title")
Text("Content")
}
}
}
๐ ์ด๊ฑด ๊ณ ์ ๋ ๋ทฐ์ผ. ํญ์ "Title" + "Content"๋ง ๋ณด์ฌ์ค.
๋ค๋ฅธ ํ๋ฉด์์ ์ฌํ์ฉํ๊ธฐ์ ๋ฑ ์ด๊ฑฐ๋ฐ์ ๋ชป ๋ณด์ฌ์ค.
2) Slot API๋ฅผ ์ ์ฉํ ์ปดํฌ๋ํธ - ์์ ์ฒซ๋ฒ์งธ ์ฝ๋๋ธ๋ญ
- content: () -> Content → ๐ก ๋ด๊ฐ View๋ฅผ "์ฌ๋กฏ์ฒ๋ผ" ๋๊ฒจ์ค ์ ์์
- @ViewBuilder → ์ฌ๋ฌ View๋ฅผ ๋์ดํ ์ ์์
CustomCard {
Text("์ด๊ฑด ๋ด ์นด๋ ์ ๋ชฉ")
.font(.headline)
Text("์ด๊ฑด ๋ด ์นด๋ ๋ด์ฉ")
.font(.subheadline)
Image(systemName: "star.fill")
.foregroundColor(.yellow)
}
๐ ๊ฒฐ๊ณผ:
- CustomCard๋ผ๋ "ํ"์ ์ฌ์ฌ์ฉํ๊ณ
- ์์ ๋ค์ด๊ฐ๋ ๋ด์ฉ์ ๋ด๊ฐ ์ํ๋ ๋๋ก ๋ฐ๊ฟ ์ ์์
- ์ฆ, ๋ด๊ฐ ๋ง๋ ์ปดํฌ๋ํธ์ ๋ผ๋๋ ์ ์งํ๊ณ , ๋ด๋ถ๋ ์ ์ฐํ๊ฒ ์ปค์คํฐ๋ง์ด์ง ๊ฐ๋ฅํด์ง !
3. ์กฐ๊ฑด๋ฌธ๊ณผ ๋ฐ๋ณต๋ฌธ์ด ๋ค์ด๊ฐ๋ ๋ณต์กํ View ์กฐ๋ฆฝ ํจ์ ๋ง๋ค ๋
@ViewBuilder
func StatusView(isActive: Bool) -> some View {
if isActive {
Text("Active")
.foregroundColor(.green)
} else {
Text("Inactive")
.foregroundColor(.red)
}
}
๐ if-else๊ฐ ์์ฐ์ค๋ฝ๊ฒ ์๋ํ๋ ์ด์ ๋ ViewBuilder๊ฐ ๋ด๋ถ์ ์ผ๋ก buildEither๋ฅผ ํธ์ถํ๊ธฐ ๋๋ฌธ.
4. SwifUI Container๋ฅผ ์ปค์คํ ์ผ๋ก ๋ง๋ค ๋
- SwiftUI์ VStack, HStack ๊ฐ์ ์ปจํ ์ด๋๋ฅผ ์ง์ ๋ง๋ค๊ณ ์ถ์ ๋.
struct CustomStack<Content: View>: View {
let content: () -> Content
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
var body: some View {
VStack(alignment: .leading) {
content()
}
}
}
๐ ์ด๋ฌ๋ฉด CustomStack { ... } ํํ๋ก ๋ด๊ฐ ๋ง๋ Stack์ ์ฌ์ฉํ ์ ์์ด.
โ ์ธ์ ์ ์จ๋ ๋๋๊ฐ?
- ์ผ๋ฐ์ ์ธ View ํ์ผ์์ body๋ฅผ ์์ฑํ ๋ (์๋ ์ ์ฉ๋จ)
- ๋จ์ผ View๋ง ๋ฐํํ๋ ๊ฐ๋จํ ํจ์
๊ทธ๋ผ ViewBuilder๋ฅผ ์ฌ์ฉํ ๋ ์ฃผ์ํด์ผํ ์ ์ด ์์๊น ?
โ ViewBuilder ์ฌ์ฉํ ๋ ์ฃผ์ํ ์
1. ๋ฐํ View๋ ํญ์ ๋์ผํ ํ์ ์ผ๋ก ๋ฌถ์ฌ์ผ ํ๋ค
- ViewBuilder๋ ์ฌ๋ฌ View๋ฅผ ๋ฐํํ๋ ๊ฒ ๊ฐ์ง๋ง, Swift ๋ ๊ฒฐ๊ตญ body์์ ํ๋์ View ํ์ ๋ง ํ์ํจ.
โ๏ธ ์ด๋ป๊ฒ ํด๊ฒฐํ๋๋ฉด → SwiftUI๊ฐ ๋ด๋ถ์์ TupleView๋ Group์ผ๋ก ๋ฌถ์ด์ค.
VStack {
if condition {
Text("True")
} else {
Image(systemName: "star") // ๊ฐ๋ฅ, but ์ฃผ์ ํ์
}
}
โ๏ธ ์ฃผ์:
SwiftUI๊ฐ ์์์ ๋ฌถ์ด์ฃผ๊ธด ํ์ง๋ง, ๋ณต์กํ ๊ฒฝ์ฐ Swift๊ฐ ํ์
์ถ๋ก ์ ๋ชป ํ ์๋ ์์ด.
๐ ์ด๋ด ๋ Group {}๋ก ๋ช ์์ ์ผ๋ก ๋ฌถ์ด์ฃผ๋ฉด ์ค๋ฅ๋ฅผ ํผํ ์ ์์ด.
๐ฅ Group์ ๋ญ๋๋ฉด:
Group์ SwiftUI๊ฐ ์ ๊ณตํ๋ ํ์ ์ค๋ฆฝ ์ปจํ ์ด๋์ผ.
๋ด๋ถ์ ์ด๋ค View๊ฐ ๋ค์ด์ค๋ ํญ์ ๊ฐ์ ํ์ : Group<_Content>
VStack {
if condition {
Group { Text("True") }
} else {
Group { Image(systemName: "star") }
}
}
๐ ํ์ ๊ด์ ์์ ๋ณด๋ฉด:
- Group { Text("True") } → ํ์ : Group<Text>
- Group { Image(systemName: "star") } → ํ์ : Group<Image>
2. ๋ณต์กํ ์กฐ๊ฑด๋ฌธ ์ค์ฒฉ ์ ํ์ ์ถ๋ก ์ด ๊นจ์ง ์ ์์
- if, else if, else ๊ฐ์ ๋ณต์กํ ๋ถ๊ธฐ๋ฌธ์ด ๋ง์์ง๋ฉด, ์ปดํ์ผ๋ฌ๊ฐ ํ์ ์ ์ ๋๋ก ์ถ๋ก ๋ชปํ๋ ๊ฒฝ์ฐ๊ฐ ์์
โ๏ธ ์ด๋ด ๋๋ Group์ ์ ์จ์ฃผ๊ฑฐ๋, View๋ฅผ ๋ฐ๋ก ๋นผ์ฃผ๋ ๊ฒ ์ข์.
3. ViewBuilder๋ View์์๋ง ์ธ ์ ์์
- ViewBuilder๋ SwiftUI View ์ปจํ ์คํธ์์๋ง ์ ํจ.
๋ก์ง ์ฝ๋, ์ผ๋ฐํจ์์์๋ ์ฌ์ฉ ๋ถ๊ฐ, ํญ์ View๋ฅผ ๋ฐํํด์ผ ํจ
4. ํ๋ผ๋ฏธํฐ๋ก ๋๊ธธ ๋ escaping ์ฃผ์
let content: () -> Content
- ๋ณดํต ์ด๋ ๊ฒ ์ ์ธํ์ง๋ง, ViewBuilder๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ -> @escaping์ ๋ถ์ด๋ ๊ฑธ ์์ผ๋ฉด ์๋ฌ ๋ฐ์ํ ์ ์์
โ๏ธ SwiftUI์ ๋๋ถ๋ถ์ API๋ ์ด ๋ถ๋ถ์ ์ด๋ฏธ ์ฒ๋ฆฌํด์ฃผ์ง๋ง, ๋ด๊ฐ ์ง์ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค ๋ ๊ผญ ํ์ธํด์ผ ํด.
5. ViewBuilder ๋ด๋ถ๋ ์ต๋ 10๊ฐ๊น์ง๋ง View๋ฅผ ์ง์ ๋์ด ๊ฐ๋ฅ (Swift ์ ํ)
์ด๊ฑด Swift ์ปดํ์ผ๋ฌ์ ์ ์ฝ์ธ๋ฐ:
VStack {
Text("1")
Text("2")
...
Text("11") // ์ฌ๊ธฐ์๋ถํฐ ์๋ฌ๋ ์๋ ์์
}
โ๏ธ Swift์ Tuple์ด 10๊ฐ๊น์ง๋ง ์ง์๋ผ์, 11๊ฐ ์ด์ View๋ฅผ ๋์ดํ๋ฉด Swift๊ฐ ํ์ ์ ์ถ๋ก ํ์ง ๋ชปํด.
๐ ์ด๋ด ๋ ForEach๋ก ๋ฐ๊ฟ์ ํด๊ฒฐ ๊ฐ๋ฅ:
VStack {
ForEach(0..<11) { index in
Text("\(index)")
}
}
6. ViewBuilder๋ ์ผ๋ฐ์ ์ธ ๋ฆฌํด๊ฐ ํจ์์ ๋ค๋ฅด๋ค
- ํจ์์์ ์ฌ๋ฌ ๊ฐ์ ๋์ดํด๋ return์ ์ฐ์ง ์๊ณ ๋ง์ง๋ง ์ค์ ๋ฆฌํดํ๊ฒ ์์ฑํด์ผ๋จ !!
- SwiftUI์ ์ ์ธ์ ๋ฌธ๋ฒ ์คํ์ผ
โ๏ธ ๋ง์ฝ return์ ์ด๋ค๋ฉด → ์ปดํ์ผ ์ค๋ฅ ๋ ์๋ ์์ด. !!
'iOS > SwiftUI' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| @StateObject vs @ObservedObject ์ฐจ์ด (2) | 2025.06.26 |
|---|---|
| SwiftUI) Drawing and Animation 1 - Drawing Paths and Shapes (0) | 2023.04.02 |
| SwiftUI) SwiftUI Essentials 3 - Handling User Input (4) | 2023.03.31 |
| SwiftUI) SwiftUI Essentials - 2 Building Lists and Navigation (0) | 2023.03.29 |
| SwiftUI) SwiftUI Essentials (13) | 2023.03.29 |