๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐ŸŽ iOS/SwiftUI

[iOS/SwiftUI] @ViewBuilder, @resultBuilder ๊ฐ€ ๋ฌด์—‡์ผ๊นŒ?

by Danna 2023. 6. 7.
728x90
728x90

ViewBuilder

A custom parameter attribute that constructs views from closures.

@resultBuilder struct ViewBuilder

SwiftUI ์—์„œ ViewBuilder ๋Š” ํด๋กœ์ ธ๋ฅผ ํ†ตํ•ด ๋ทฐ ๊ณ„์ธต์„ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋Š” ์†์„ฑ์ด๋‹ค.

  • ViewBuilder ๋ฅผ ์ด์šฉํ•˜๋ฉด ํ•จ์ˆ˜๋‚˜ ํด๋กœ์ €๋Š” ์—ฌ๋Ÿฌ๊ฐœ์˜ ๋ทฐ๋ฅผ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์—ฌ๋Ÿฌ๊ฐœ์˜ ๋ทฐ๋ฅผ ์ „๋‹ฌํ•ด์•ผํ•˜๋Š” ์ƒํ™ฉ์—์„œ ์ฝ”๋“œ๋ฅผ ๊น”๋”ํ•˜๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ˜ฏ resultBuilder ?

  • ViewBuilder ๋Š” @resultBuilder ํ”„๋กœํผ๋ฆฌ ๋ž˜ํผ๊ฐ€ ๋ถ™์–ด ์žˆ๋Š”๋ฐ ํ•ด๋‹น ์†์„ฑ์€ Swift5.4 ์— ๋„์ž…๋˜์—ˆ๊ณ  ๊ธฐ์กด ๋ช…์นญ์€ @_functionBuilder ์ด๋‹ค.
    • SE-0289์˜ ResultBuilder
    • resultBuilder ๋Š” ๋ทฐ์— ํ•œ์ •๋˜์ง€ ์•Š๊ณ  ํ•จ์ˆ˜, ํ”„๋กœํผํ‹ฐ, ํƒ€์ž…์— ํ™œ์šฉ๋  ์ˆ˜ ์žˆ๋Š” ํ˜•ํƒœ๋กœ ์ปค์Šคํ…€ํ•œ ๋ฌธ๋ฒ•์„ ๊ฐ€์ง„ ๋นŒ๋”๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ViewBuilder, resultBuilder ๋Š” DLS(Declarative Language Structures, ์„ ์–ธ์  ์–ธ์–ด ๊ตฌ์กฐ) ์˜ ์ผ์ข…์œผ๋กœ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.
  • ์•„๋ž˜ ์˜ˆ์‹œ๋ฅผ ๋ณด๋ฉด @NumberListBuilder ๋ฅผ ์‚ฌ์šฉํ•œ ๋ฉ”์†Œ๋“œ์— ์ค„๋ฐ”๊ฟˆ์œผ๋กœ ์ž‘์„ฑ๋œ 1, 2, 3 ์„ ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ๋‹ค.
@resultBuilder
struct NumberListBuilder {
    static func buildBlock(_ numbers: Int...) -> [Int] {
        numbers
    }
}

@NumberListBuilder
func makeNumberList() -> [Int] {
    1
    2
    3
}

let numbers = makeNumberList()
print(numbers) // ์ถœ๋ ฅ: [1, 2, 3]
  • HTMLBuilder ๋ฅผ ๋งŒ๋“ค์–ด html ๊ตฌ์กฐ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ์˜ˆ์ œ
@resultBuilder
struct HTMLBuilder {
    static func buildBlock(_ components: HTMLComponent...) -> String {
        components.map { $0.htmlString }.joined()
    }
}

protocol HTMLComponent {
    var htmlString: String { get }
}

struct TextComponent: HTMLComponent {
    let text: String
    
    var htmlString: String {
        "<p>\(text)</p>"
    }
}

struct LinkComponent: HTMLComponent {
    let text: String
    let url: String
    
    var htmlString: String {
        "<a href=\"\(url)\">\(text)</a>"
    }
}

func createHTML(@HTMLBuilder content: () -> String) {
    let html = content()
    print(html)
}

createHTML {
    TextComponent(text: "Hello, World!")
    LinkComponent(text: "OpenAI", url: "https://openai.com")
    TextComponent(text: "Welcome to Swift programming!")
}
import RegexBuilder

let emailPattern = Regex {
  let word = OneOrMore(.word)
  Capture {
    ZeroOrMore {
      word
      "."
    }
    word
  }
  "@"
  Capture {
    word
    OneOrMore {
      "."
      word
    }
  }
} // => Regex<(Substring, Substring, Substring)>

let email = "My email is my.name@mail.swift.org."
if let match = try emailPattern.firstMatch(in: email) {
  let (wholeMatch, name, domain) = match.output
  // wholeMatch: "my.name@mail.swift.org"
  //       name: "my.name"
  //     domain: "mail.swift.org"
}

ViewBuilder ํ™œ์šฉ - ์ž์‹๋ทฐ ์ „๋‹ฌ

๋ฉ”์†Œ๋“œ์˜ ํด๋กœ์ ธ ํŒŒ๋ผ๋ฏธํ„ฐ์— @ViewBuilder ๋ฅผ ๋ถ™์—ฌ ์ž์‹๋ทฐ๋ฅผ ์ „๋‹ฌํ•  ๋•Œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

  • contextMenu ๋ผ๋Š” ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด ์—ฌ๋Ÿฌ๊ฐœ์˜ ์ž์‹๋ทฐ๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ํŠน์ •ํ•œ View ๋ฅผ ๋งŒ๋“ค๊ณ  ์—ฌ๋Ÿฌ๊ฐœ์˜ ์ž์‹๋ทฐ๋ฅผ ์†์„ฑ์œผ๋กœ ๊ฐ–๋„๋ก initializer ์—์„œ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋‹ค.
struct Stack<Content: View>: View {
    let content: () -> Content

    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }

    var body: some View {
        VStack {
            content()
        }
    }
}
Stack {
    Text("Cut")
    Text("Copy")
    Text("Paste")
}

ViewBuilder ํ™œ์šฉ - computed property, method

View ๊ตฌ์กฐ์ฒด ์ฝ”๋“œ ๋‚ด์—์„œ๋งŒ ์‚ฌ์šฉ๋˜๋Š” ๊ฐ„๋‹จํ•œ ๋ทฐ์— ๋Œ€ํ•ด computed property ํ˜น์€ method ๋กœ ๋บ„ ๊ฒฝ์šฐ์— ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ๋‹จ์ผ ๋ทฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒฝ์šฐ์—์„œ๋Š” @ViewBuilder ๋ฅผ ๋ถ™์ด์ง€ ์•Š์•„๋„ ๋˜์ง€๋งŒ,
  • ์—ฌ๋Ÿฌ๊ฐœ์˜ ๋ทฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒฝ์šฐ @ViewBuilder ๋ฅผ ๋ถ™์ด์ง€ ์•Š์œผ๋ฉด ์ปดํŒŒ์ผ ์—๋Ÿฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค.

  • computed property ์—์„œ ์—ฌ๋Ÿฌ๊ฐœ์˜ ๋ทฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ์ฝ”๋“œ
    • ํ•ด๋‹น ์ฝ”๋“œ์—์„œ ViewBuilder ๋ฅผ ๋ถ™์ด์ง€ ์•Š์œผ๋ฉด ์ปดํŒŒ์ผ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒ๋œ๋‹ค.
struct ContentView: View {
    @ViewBuilder
    var greeting: some View {
        if shouldShowGreeting {
            Text("Hello, SwiftUI!")
            Text("Welcome to the world of SwiftUI!")
        } else {
            EmptyView()
        }
    }
    
    var shouldShowGreeting: Bool = true
    
    var body: some View {
        VStack {
            greeting
        }
    }
}
  • computed property ์—์„œ ๋‹จ์ผ ๋ทฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ์ฝ”๋“œ
    • ํ•ด๋‹น ์ฝ”๋“œ์—์„œ๋Š” ViewBuilder ๊ฐ€ ์—†์–ด๋„ ๋œ๋‹ค
struct ContentView: View {
    var shouldShowGreeting: Bool = true
    
    var greeting: some View {
        if shouldShowGreeting {
            return Text("Hello, SwiftUI!")
        } else {
            return Text("Goodbye, SwiftUI!")
        }
    }
    
    var body: some View {
        VStack {
            greeting
        }
    }
}
  • method ์—์„œ ์—ฌ๋Ÿฌ๊ฐœ์˜ ๋ทฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ์ฝ”๋“œ
struct ContentView: View {
    var shouldShowGreeting: Bool = true

    var body: some View {
        VStack {
            greeting(shouldShowGreeting)
        }
    }

    @ViewBuilder
    func greeting(_ shouldShowGreeting: Bool) -> some View {
        if shouldShowGreeting {
            Text("Hello, SwiftUI!")
            Text("Welcome to the world of SwiftUI!")
        } else {
            EmptyView()
        }
    }
}

์ฐธ๊ณ  ๋ฌธ์„œ

728x90
728x90