iOS ๐ŸŽ/SwiftUI

[SwiftUI] ViewBuilder ์•Œ์•„๋ณด๊ธฐ

fram 2023. 9. 19. 13:27

Definition

@resultBuilder
struct ViewBuilder

ํด๋กœ์ €์—์„œ ๋ทฐ๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ์‚ฌ์šฉ์ž ์ง€์ • ํŒŒ๋ผ๋ฏธํ„ฐ ์†์„ฑ

 

func contextMenu<MenuItems: View>(
    @ViewBuilder menuItems: () -> MenuItems
) -> some View

ํด๋กœ์ € ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํ†ตํ•ด child view๋ฅผ ์ƒ์„ฑํ•˜๊ณ ์ž ํ•  ๋•Œ ViewBuilder๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ์œ„์™€ ๊ฐ™์ด ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ViewBuilder๋ฅผ ์‚ฌ์šฉํ•ด ํด๋กœ์ €๋กœ child view๋ฅผ ํฌํ•จํ•˜๋Š” ๋ทฐ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. 

myView.contextMenu {
    Text("Cut")
    Text("Copy")
    Text("Paste")
    if isSymbol {
        Text("Jump to Definition")
    }
}

contextMenu์˜ menuItems ํด๋กœ์ €๋กœ ์—ฌ๋Ÿฌ Text๋ฅผ ํฌํ•จํ•˜๋Š” child view๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. 

์ฃผ๋กœ ๋ณต์žกํ•œ ๋ ˆ์ด์•„์›ƒ์„ ์ชผ๊ฐœ์–ด ์‚ฌ์šฉ์ž ์ •์˜ ๋ทฐ๋กœ ์ƒ์„ฑํ•˜๊ณ  ์กฐํ•ฉํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋œ๋‹ค.

 

so what is ViewBuilder

  • custom parameter attribute 
    • custom : SwiftUI์— ์˜ํ•ด ์ •์˜๋œ ๊ฒƒ์ด ์•„๋‹Œ ์‚ฌ์šฉ์ž ์ •์˜์— ์˜ํ•ด ๋‹จ์ผ ๋ณตํ•ฉ ๋ทฐ ์ƒ์„ฑ ๊ฐ€๋Šฅ
    • parameter : ํ•จ์ˆ˜๋‚˜ ์ด๋‹ˆ์…œ๋ผ์ด์ € ํŒŒ๋ผ๋ฏธํ„ฐ ์•ž์— ์œ„์น˜ํ•˜๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ ์†์„ฑ
    • attribute : ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ, ์ถ”๊ฐ€ ์ •๋ณด๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ์™€ ์ฝ”๋“œ์— ์ œ๊ณตํ•˜๋Š” ์—ญํ™œ
  • ์—ฌ๋Ÿฌ๊ฐœ์˜ ๋ทฐ๋ฅผ ๋‹จ์ผ ๋ณตํ•ฉ ๋ทฐ(Composite View)๋กœ ์กฐํ•ฉ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•จ (๊ฐœ์ธ์ ์œผ๋กœ๋Š” ํด๋กœ์ €๋กœ ์—ฌ๋Ÿฌ๊ฐœ์˜ ๋ทฐ๋ฅผ ์ „๋‹ฌํ•œ๋‹ค๊ธฐ ๋ณด๋‹ค๋Š” ๋ทฐ ๊ณ„์ธต ๊ตฌ์กฐ๋ฅผ ์ „๋‹ฌํ•œ๋‹ค๊ณ  ํ•˜๋Š”๊ฒŒ ๋งž๋Š” ๊ฒƒ ๊ฐ™๋‹ค. view์˜ subview, ์—ฌ๋Ÿฌ view ์ „๋‹ฌ ๊ฐ€๋Šฅํ•˜๊ธฐ ๋•Œ๋ฌธ์—)

advantage

  • ์œ ์—ฐ์„ฑ
    • ๋™์ ์œผ๋กœ ๋ทฐ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Œ. ํด๋กœ์ € ๋‚ด๋ถ€์—์„œ ์กฐ๊ฑด๋ฌธ, ๋ฐ˜๋ณต๋ฌธ์„ ์‚ฌ์šฉํ•˜์—ฌ ์œ ์—ฐํ•˜๊ฒŒ ๋Œ€์‘ ๊ฐ€๋Šฅ
    • ์ƒ์œ„ ๋ทฐ์—์„œ @ViewBuilder๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์–ด๋– ํ•œ ๋ทฐ๋„ ์ „๋‹ฌ ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์œ ์—ฐํ•˜๊ฒŒ ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆ ํ•  ์ˆ˜ ์žˆ์Œ
VStack {
	if isStart {
    	StartView()
    } else {
    	InfoView()
    }
}
  • ์žฌ์‚ฌ์šฉ์„ฑ
    • ์ปค์Šคํ…€ ๋ทฐ๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๋™์ผํ•œ ๊ตฌ์„ฑ์„ ๊ฐ€์ง€๋Š” ๋ทฐ์—์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•จ
  • ์ฝ”๋“œ ๊ฐ„๊ฒฐ์„ฑ
    • ๋ทฐ ๋กœ์ง์„ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ณต์žกํ•œ UI๋ฅผ ๊ตฌ์„ฑํ•  ๋•Œ ์ฝ”๋“œ๋ฅผ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Œ
    • ๋ทฐ๋ฅผ ์ž‘์€ ๋ถ€๋ถ„์œผ๋กœ ์ชผ๊ฐ  ๋’ค ์กฐํ•ฉํ•˜์—ฌ ๋ณต์žกํ•œ UI๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ์œ ์ง€๋ณด์ˆ˜ ์ธก๋ฉด์˜ ์ด์ ์„ ๊ฐ€์ง

disadvantage

  • ๋ณต์žก์„ฑ
    • View๋ฅผ ๋ถ„๋ฆฌํ•˜๋ฏ€๋กœ์„œ ์ƒ๊ธฐ๋Š” ๋ณต์žก์„ฑ์œผ๋กœ ์ธํ•ด ์ฝ”๋“œ์™€ ๋™์ž‘ ์›๋ฆฌ๋ฅผ ์ดํ•ดํ•˜๊ธฐ ํž˜๋“ค๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์œผ๋ฉฐ ๊ฐ€๋…์„ฑ์„ ํ•ด์น  ์ˆ˜ ์žˆ์Œ
  • ๋””๋ฒ„๊น…
    • ์—ฌ๋Ÿฌ ๋ทฐ ๋กœ์ง์ด ์ค‘์ฒฉ๋˜๋ฉด์„œ ์บก์Аํ™” ๋˜๊ธฐ ๋•Œ๋ฌธ์— ํŠน์ • ๋ฐ์ดํ„ฐ๊ฐ€ ์–ด๋””์„œ ์˜ค๊ณ  ์–ด๋””์„œ ์‚ฌ์šฉ๋˜๋Š”์ง€ ์ถ”์ ํ•˜๊ธฐ ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์Œ
  • ๋Ÿฐํƒ€์ž„ ์ด์Šˆ
    • ViewBuilder๊ฐ€ ์ƒ์„ฑํ•˜๋Š” ๋ทฐ์˜ ๊ฐฏ์ˆ˜์™€ ํƒ€์ž…์ด ์ปดํŒŒ์ผ ํƒ€์ด๋ฐ์— ๊ฒฐ์ •๋˜๋ฉฐ ์ด๋Š” ํƒ€์ž… ์•ˆ์ •์„ฑ๊ณผ ๋Ÿฐํƒ€์ž„ ์ด์Šˆ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Œ

 

Example

@inlinable public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)

์ •์˜๋ฅผ ๋ณด๋ฉด ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ @ViewBuilder๊ฐ€ ๋ถ™์€ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ณ  ์ด ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ํด๋กœ์ €๋ฅผ ํ†ตํ•ด ์—ฌ๋Ÿฌ ๋‹จ์ผ ๋ทฐ๋ฅผ ์ „๋‹ฌ ๋ฐ›๋Š”๋‹ค. ๋ทฐ ๋นŒ๋”๋กœ ๋ถ€ํ„ฐ ์ „๋‹ฌ๋œ ๋‹จ์ผ ๋ทฐ๋“ค๋กœ ๋ณตํ•ฉ ๋ทฐ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค. ViewBuilder๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ผ์ข…์˜ custom container๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค. 

ViewBuilder๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ปค์Šคํ…€ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ƒ์„ฑํ•  ๋•Œ ๋‹ค๋ฅธ ํŒŒ๋ผ๋ฏธํ„ฐ ์ธ์ž๋กœ ๋ถ€ํ„ฐ ํ•„์š”ํ•œ ๋””ํŽœ๋˜์‹œ๋ฅผ ํ•จ๊ป˜ ์ „๋‹ฌ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค. 

 

์˜ˆ์ œ ์ฝ”๋“œ

https://github.com/youabledev/viewbuilder_swiftui

 

GitHub - youabledev/viewbuilder_swiftui: viewbuilder ์˜ˆ์ œ

viewbuilder ์˜ˆ์ œ. Contribute to youabledev/viewbuilder_swiftui development by creating an account on GitHub.

github.com

struct NormalView<Content: View>: View { // ์ œ๋„ค๋ฆญ์œผ๋กœ View๋ฅผ Content๋กœ
    let title: String
    let buttonName: String
    let content: () -> Content // ํด๋กœ์ €๋กœ ์ „๋‹ฌ ๋ฐ›์Œ
    
    init(title: String, buttonName: String, @ViewBuilder content: @escaping () -> Content) {
        self.title = title
        self.buttonName = buttonName
        self.content = content
    }
    
    var body: some View {
        VStack(spacing: 0) {
            ZStack {
                Text(title)
                    .font(.system(.headline))
                    .fontWeight(.bold)
                
                HStack {
                    Button {
                        
                    } label: {
                        Image(systemName: "xmark")
                            .frame(width: 44, height: 44)
                            .foregroundColor(.black)
                    }
                    .padding(.leading, 14)
                    
                    Spacer()
                } //: HStack
            } //: ZStack
            .frame(maxWidth: .infinity)
            .frame(height: 48)
            
            Spacer()
            
            content()
            
            Spacer()
            
            Button {
                
            } label: {
                Text(buttonName)
                    .foregroundColor(.white)
                    .padding(.vertical, 20)
                    .frame(maxWidth: .infinity)
                    .background(.black)
            }
        }
    }
}

๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” View๋ฅผ ViewBuilder๋ฅผ ํ†ตํ•ด ์ƒ์„ฑ

struct NormalView_Previews: PreviewProvider {
    static var previews: some View {
        NormalView(title: "์ œ๋ชฉ", buttonName: "๋ฒ„ํŠผ์ด๋ฆ„") {
            Text("ํ…Œ์ŠคํŠธ")
        }
    }
}

preview๋ฅผ ์„ค์ •ํ•ด์„œ ๋ณด๋ฉด ๊ณตํ†ต์ ์œผ๋กœ ์‚ฌ์šฉํ•  ํ™”๋ฉด ์˜์—ญ์„ ViewBuilder ๋ฅผ ์ด์šฉํ•œ View๋กœ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ํด๋กœ์ €๋ฅผ ํ†ตํ•ด ํ‘œ์‹œํ•  ๋ทฐ๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Œ

struct ProductDetailView: View {
    var body: some View {
        NormalView(title: "์ƒํ’ˆ ์ƒ์„ธ", buttonName: "๊ตฌ์ž…ํ•˜๊ธฐ") {
            GeometryReader { geometry in
                VStack(alignment: .leading) {
                    Rectangle()
                        .frame(width: geometry.frame(in: .global).size.width - 48)
                        .frame(height: geometry.frame(in: .global).size.width - 48)
                        .foregroundColor(.yellow)
                    
                    Text("๊ฐ€๊ฒฉ : 20,000")
                }
                .padding(.horizontal, 24)
            }
        } //: NormalView
    }
}

์‹ค์ œ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ์ค‘๋ณต๋˜๋Š” ๋ทฐ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ํ•„์š”๊ฐ€ ์—†์–ด ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋†’์ด๊ณ  ๋ณต์žกํ•œ ๋ทฐ ์ฝ”๋“œ๋ฅผ ๊ฐ„๊ฒฐ

ํ•˜๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค. 

 

์ถœ์ฒ˜ ๋ฐ ์ฐธ๊ณ  ์‚ฌ์ดํŠธ

 

ViewBuilder | Apple Developer Documentation

A custom parameter attribute that constructs views from closures.

developer.apple.com