iOS/SwiftUI

VStack ์•ˆ์—์„œ ์—ฌ๋Ÿฌ ๊ฐœ์˜ View๋ฅผ ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„ํ•˜์ง€ ์•Š๋Š” ์ด์œ 

Earth Wave 2024. 7. 6. 16:15

๊ถ๊ธˆ์ฆ์ด ์ƒ๊ฒผ๋‹ค

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
        .padding()
    }
}

#Preview {
    ContentView()
}
import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.spacing = 10

        let imageView = UIImageView(image: UIImage(systemName: "globe"))
        imageView.contentMode = .scaleAspectFit
        stackView.addArrangedSubview(imageView)

        let label = UILabel()
        label.text = "Hello, world!"
        stackView.addArrangedSubview(label)

        stackView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stackView)

        NSLayoutConstraint.activate([
            stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
}

 

UIKit์„ ์œ„์ฃผ๋กœ ๊ฐœ๋ฐœ์„ ํ•˜๋‹ค๊ฐ€, ์ฒ˜์Œ SwiftUI๋ฅผ ์ ‘ํ–ˆ์„ ๋•Œ ๊ต‰์žฅํžˆ ์‹ ๊ธฐํ•œ ์  ๋‘ ๊ฐ€์ง€๊ฐ€ ์žˆ์—ˆ๋‹ค.

์ฒซ ๋ฒˆ์งธ๋Š” VStack ์•ˆ์— ์—ฌ๋Ÿฌ ๊ฐœ์˜ View๋ฅผ ๋„ฃ๋Š”๋ฐ, ๊ทธ View๋“ค์„ ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„ํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ์ ์ด์—ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๋‘๋ฒˆ์งธ๋Š” VStack์— UIKit ์ฒ˜๋Ÿผ ParentView์— ChildView๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ์‹๋„ ์•„๋‹ˆ๊ณ , ๊ทธ๋ƒฅ Vstack ํด๋กœ์ € ์•ˆ์— View๋ฅผ ๋„ฃ๊ธฐ๋งŒ ํ•˜๋ฉด View๊ฐ€ ๋š๋”ฑํ•˜๊ณ  ๋งŒ๋“ค์–ด์ง„๋‹ค๋Š” ์ ์ด์—ˆ๋‹ค.

VStack ์•ˆ์—์„œ ์–ด๋–ค ์ผ์ด ์ผ์–ด๋‚˜๊ณ  ์žˆ๊ธธ๋ž˜, ์ดํ† ๋ก ์‰ฝ๊ฒŒ View๊ฐ€ ๋งŒ๋“ค์–ด์ง€๋Š” ๊ฒƒ์ผ๊นŒ?

VStack

@frozen public struct VStack<Content> : View where Content : View {

    /// ์ฃผ์–ด์ง„ ๊ฐ„๊ฒฉ๊ณผ ์ˆ˜ํ‰ ์ •๋ ฌ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
    ///
    /// - ๋งค๊ฐœ๋ณ€์ˆ˜:
    ///   - alignment: ์ด ์Šคํƒ์˜ ํ•˜์œ„ ๋ทฐ๋ฅผ ์ •๋ ฌํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ€์ด๋“œ์ž…๋‹ˆ๋‹ค.
    ///     ์ด ๊ฐ€์ด๋“œ๋Š” ๋ชจ๋“  ํ•˜์œ„ ๋ทฐ์— ๋Œ€ํ•ด ๋™์ผํ•œ ์ˆ˜์ง ํ™”๋ฉด ์ขŒํ‘œ๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค.
    ///   - spacing: ์ธ์ ‘ํ•œ ํ•˜์œ„ ๋ทฐ ๊ฐ„์˜ ๊ฑฐ๋ฆฌ์ž…๋‹ˆ๋‹ค. ๊ฐ ์Œ์˜ ํ•˜์œ„ ๋ทฐ์— ๋Œ€ํ•ด
    ///     ์Šคํƒ์ด ๊ธฐ๋ณธ ๊ฑฐ๋ฆฌ๋ฅผ ์„ ํƒํ•˜๋„๋ก ํ•˜๋ ค๋ฉด `nil`์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.
    ///   - content: ์ด ์Šคํƒ์˜ ๋‚ด์šฉ์„ ์ƒ์„ฑํ•˜๋Š” ๋ทฐ ๋นŒ๋”์ž…๋‹ˆ๋‹ค.
    @inlinable public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)

    /// ์ด ๋ทฐ์˜ ๋ณธ๋ฌธ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ทฐ์˜ ํƒ€์ž…์ž…๋‹ˆ๋‹ค.
    ///
    /// ์‚ฌ์šฉ์ž ์ •์˜ ๋ทฐ๋ฅผ ์ƒ์„ฑํ•  ๋•Œ, Swift๋Š” ํ•„์ˆ˜ ์†์„ฑ์ธ ``View/body-swift.property``์˜
    /// ๊ตฌํ˜„์—์„œ ์ด ํƒ€์ž…์„ ์ถ”๋ก ํ•ฉ๋‹ˆ๋‹ค.
    public typealias Body = Never
}

 

๊ถ๊ธˆ์ฆ์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ, VStack ์ด ์–ด๋–ป๊ฒŒ ์ด๋ค„์ ธ ์žˆ๋Š” ๊ฑด์ง€ ์•Œ์•„๋ณด๊ธฐ๋กœ ํ–ˆ๋‹ค.

 

VStack์€ ์ œ๋„ค๋ฆญ ๊ตฌ์กฐ์ฒด๋กœ, Content ๋ผ๋Š” ์ œ๋„ค๋ฆญ ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜๋ฉฐ, ์ด ํƒ€์ž…์€ View Protocol๋ฅผ ์ค€์ˆ˜ํ•ด์•ผ ํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  VStack ์ž์ฒด๋„ View Protocol ์„ ์ค€์ˆ˜ํ•œ๋‹ค.

 

VStack์˜ ์ดˆ๊ธฐํ™” ๋ฉ”์„œ๋“œ๋Š” ์„ธ ๊ฐ€์ง€ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๊ฐ€์ง„๋‹ค. ์ฒซ ๋ฒˆ์งธ, alignment๋Š” ์Šคํƒ ๋‚ด ํ•˜์œ„ ๋ทฐ๋ฅผ ์ˆ˜ํ‰์œผ๋กœ ์ •๋ ฌํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ, ๊ธฐ๋ณธ๊ฐ’์€ .center ์ด๋‹ค. ๋‘ ๋ฒˆ์งธ, spacing ์€ ์ธ์ ‘ํ•œ ํ•˜์œ„ ๋ทฐ ๊ฐ„์˜ ๊ฑฐ๋ฆฌ๋กœ, ๊ธฐ๋ณธ๊ฐ’์€ nil์ด๋ฉฐ, ์ด๋Š” ์Šคํƒ์ด ๊ธฐ๋ณธ ๊ฑฐ๋ฆฌ๋ฅผ ์„ ํƒํ•˜๋„๋ก ํ•œ๋‹ค. ์„ธ ๋ฒˆ์งธ๋Š” content ๋กœ @ViewBuilder ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ด ์Šคํƒ์˜ ๋‚ด์šฉ์„ ์ƒ์„ฑํ•˜๋Š” ํด๋กœ์ €์ด๋‹ค.

 

์—ฌ๊ธฐ์„œ ๋‚ด๊ฐ€ ๊ฐ€์ง„ ๊ถ๊ธˆ์ฆ์— ๋Œ€ํ•œ ํ•ด๋‹ต์ด ๋  ๋ถ€๋ถ„์€ @ViewBuilder ์†์„ฑ์ด๋ผ๋Š” ํŒ๋‹จ์ด ๋‚ด๋ ค์กŒ๋‹ค. @ViewBuilder ์†์„ฑ์— ์˜ํ•ด์„œ, VStack ๋‚ด์˜ View๋“ค์ด ์กฐํ•ฉ์ด ๋˜๋Š” ๊ฒƒ ๊ฐ™์€๋ฐ, ๊ทธ๋Ÿฌ๋ฉด @ViewBuilder ์†์„ฑ์€ ๋ฌด์—‡์ผ๊นŒ?

@ViewBuilder

@ViewBuilder ์†์„ฑ์€ ์–ด๋–ป๊ฒŒ ์ด๋ฃจ์–ด์ ธ ์žˆ๋Š” ๊ฒƒ์ผ๊นŒ?

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@resultBuilder public struct ViewBuilder {

    public static func buildExpression<Content>(_ content: Content) -> Content where Content : View

    public static func buildBlock() -> EmptyView
    
    public static func buildBlock<Content>(_ content: Content) -> Content where Content : View

    public static func buildBlock<each Content>(_ content: repeat each Content) -> TupleView<(repeat each Content)> where repeat each Content : View
}
  • iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0 ์ด์ƒ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Œ
  • @resultBuilder ์–ดํŠธ๋ฆฌ๋ทฐํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Œ

@resultBuilder

Swift 5.4์— ์ƒˆ๋กœ ์ƒ๊ธด ๊ธฐ๋Šฅ์œผ๋กœ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์ฒด๋ฅผ ๋‹จ๊ณ„๋ณ„๋กœ ๋นŒ๋“œํ•˜๋Š” ํƒ€์ž…. ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ์„ ์–ธ์ ์ธ ๋ฐฉ๋ฒ•์œผ๋กœ ์ค‘์ฒฉ๋œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด ๋„๋ฉ”์ธ - ํŠน์ • ์–ธ์–ด(DSL)๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด Result Builder๋ฅผ ์‚ฌ์šฉํ•จ.

๐Ÿ’ก DSL์ด๋ž€?

“๋„๋ฉ”์ธ” ์ด๋ผ๋Š” ํŠน์ • ์˜์—ญ์—์„œ ์ž‘๋™ํ•˜๋Š” ํ”„๋กœ๊ทธ๋žจ์„ ์œ„ํ•ด ์„ค๊ณ„๋œ ์ผ์ข…์˜ ์†Œํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด. DSL์€ ํŠน์ • ์ข…๋ฅ˜์˜ ์ž‘์—…์„ ์—ผ๋‘ํ•ด ๋‘๊ณ  ์„ค๊ณ„๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ทธ๋Ÿฌํ•œ ์ข…๋ฅ˜์˜ ์ž‘์—…์„ ๋” ์‰ฝ๊ฒŒ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ํŠน๋ณ„ํ•œ ๊ธฐ๋Šฅ์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์Œ.

 

resultBuilder์˜ static method

static func buildBlock(_ components: Component...) -> Component

๋ถ€๋ถ„ ๊ฒฐ๊ณผ์˜ ๋ฐฐ์—ด์„ ๋‹จ์ผ ๋ถ€๋ถ„ ๊ฒฐ๊ณผ๋กœ ๊ฒฐํ•ฉํ•จ

@resultBuilder
struct ArrayBuilder {
    
    static func buildBlock(_ components: [String]...) -> [String] {
        return Array(components.joined())
    }
    
}

@ArrayBuilder var array: [String] {
    ["Apple", "Developer", "Academy"]
    ["TeaPot"]
}

print(array) // ["Apple", "Developer", "Academy", "TeaPot"]

 

static func buildExpression(_ expression: Expression) -> Component

ํ‘œํ˜„์‹์„ ๋‚ด๋ถ€ ํƒ€์ž…์œผ๋กœ ๋ณ€ํ™˜์„ ์ˆ˜ํ–‰ํ•˜๊ฑฐ๋‚˜ ์‚ฌ์šฉํ•˜๋Š” ๊ณณ์—์„œ ํƒ€์ž… ์ถ”๋ก ์„ ์œ„ํ•œ ์ถ”๊ฐ€ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@resultBuilder
struct ArrayBuilder {
    
    static func buildBlock(_ components: [String]...) -> [String] {
        return Array(components.joined())
    }
    
    static func buildExpression(_ expression: String) -> [String] {
        return [expression]
    }
    
    static func buildExpression(_ expression: [String]) -> [String] {
        return expression
    }
    
}

@ArrayBuilder var array: [String] {
    ["Apple", "Developer", "Academy"]
    ["TeaPot"]
    "Tea"
}

print(array) // ["Apple", "Developer", "Academy", "TeaPot", "Tea"]


static func buildPartialBlock(first: Component) -> Component

buildPartialBlock(accumulated:next:)

 

Result Builder์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋ฉ”์„œ๋“œ ์ค‘ ํ•˜๋‚˜๋กœ, ๋ณต์žกํ•œ ๋นŒ๋” ๋ธ”๋ก์„ ๋‹จ๊ณ„์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ๋„์ž…๋œ ๊ฒƒ

Swift ์ปดํŒŒ์ผ๋Ÿฌ๋Š” ์ด ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋นŒ๋” ๋ธ”๋ก์„ ๋ถ€๋ถ„์ ์œผ๋กœ ํ‰๊ฐ€ํ•˜๊ณ  ๊ฒฐํ•ฉํ•  ์ˆ˜ ์žˆ์Œ

@resultBuilder
struct SimpleBuilder {
		// ๋นŒ๋” ๋ธ”๋ก์˜ ์ฒซ ๋ฒˆ์งธ ํ‘œํ˜„์‹์„ ์ฒ˜๋ฆฌํ•จ 
    static func buildPartialBlock(first: String) -> String {
        return first
    }
		
		// ์ด์ „ ๋‹จ๊ณ„์—์„œ ๋ˆ„์ ๋œ ๊ฒฐ๊ณผ์™€ ํ˜„์žฌ ํ‘œํ˜„์‹์„ ๊ฒฐํ•ฉํ•จ 
    static func buildPartialBlock(accumulated: String, next: String) -> String {
        return accumulated + " " + next
    }
}

@SimpleBuilder var example: String {
    "Hello"
    "World"
}

 

๊ทธ๋Ÿฌ๋ฉด ๋‹ค์‹œ ViewBuilder

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@resultBuilder public struct ViewBuilder {

    /// ๋นŒ๋” ๋‚ด์—์„œ ํ‘œํ˜„์‹์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
    public static func buildExpression<Content>(_ content: Content) -> Content where Content : View

    /// ์•„๋ฌด๋Ÿฐ ๋ทฐ๊ฐ€ ์—†๋Š” ๋ธ”๋ก์—์„œ ๋นˆ ๋ทฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
    public static func buildBlock() -> EmptyView

    /// ๋‹จ์ผ ์ž์‹ ๋ทฐ๋ฅผ ์ˆ˜์ •ํ•˜์ง€ ์•Š๊ณ  ๊ทธ๋Œ€๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
    /// ์ž์‹ ๋ทฐ๋กœ ์ž‘์„ฑ๋œ ๋‹จ์ผ ๋ทฐ์˜ ์˜ˆ๋Š” `{ Text("Hello") }`์ž…๋‹ˆ๋‹ค.
    public static func buildBlock<Content>(_ content: Content) -> Content where Content : View

    /// ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋ทฐ๋ฅผ ํŠœํ”Œ ๋ทฐ๋กœ ๊ฒฐํ•ฉํ•˜์—ฌ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
    public static func buildBlock<each Content>(_ content: repeat each Content) -> TupleView<(repeat each Content)> where repeat each Content : View
}

 

VStack ์—์„œ์˜ ๋™์ž‘ ์›๋ฆฌ

// ์ฐธ๊ณ  
@inlinable public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)

// VStack ์ด ์–ด๋–ป๊ฒŒ ์ด๋ฃจ์–ด์ ธ ์žˆ๋Š”์ง€ 
VStack {
	Text("Title").font(.title)
	Text("Contents")
}

// #1
Vstack.init(content: {
	Text("Title").font(.title)
	Text("Contents")
	return // TODO: build results using 'ViewBuilder'
})

// #2 
Vstack.init(content: {
	// 1. ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ชจ๋“  ๋ช…๋ น๋ฌธ์— ๋Œ€ํ•œ ๋ณ€์ˆ˜ ์ƒ์„ฑ 
	let v0 = Text("Title").font(.title) 

	// result builder๊ฐ€ ๋ณด๊ธฐ ์ „์— ๊ฐ’์ด ๋ณ€๊ฒฝ๋จ 
	// ์ˆ˜์ •์ž๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๋Šฅ๋ ฅ + result builder ๊ฐ€ ๋ณด๊ธฐ ์ „์— ๊ฐ’์„ ์ˆ˜์ •ํ•œ๋‹ค = Swift DSL ์ด ์ˆ˜์ •์ž๋ฅผ ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ 
	let v1 = Text("Contents")
	// 2. buildBlock์˜ ์ž‘์—…์€ ๋ชจ๋“  ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๋‹จ์ผ๊ฐ’์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ฑฐ๋‚˜ ๊ฒฐํ•ฉํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ 
	return ViewBuilder.buildBlock(v0, v1)
	// 3. Vstack ์ด ์ฝ˜ํ…์ธ ๋กœ ์‚ฌ์šฉํ•  ๋‹จ์ผ ๊ฐ’์œผ๋กœ ์–ด์…ˆ๋ธ” ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•จ
})

// #2 
Vstack.init(content: {
	let v0 = Text("Title").font(.title) 
	let v1 = Text("Contents")
	return ViewBuilder.buildBlock(v0, v1)
})

 

๊ฒฐ๋ก 

resultBuilder๋กœ ๊ตฌ์„ฑ๋œ ViewBuilder ์†์„ฑ ๋•๋ถ„๋ฐ, VStack ์•ˆ์—์„œ ์‰ผํ‘œ ์—†์ด ๋ทฐ๋“ค์„ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.