iOS, Swift, イベントレポート

SwiftUIにおけるVStack,HStackとFunction Builderの関係

WWDC19,5日目。
前回SwiftUIのコードを読み解くの記事の最後にVStackでViewBuilderが使えるかがわからないという話をしました。
Labで質問をしたのでその結果をまとめます。

そもそもの疑問

SwiftUIのVStackとHStackはクロージャーの中にViewコンポーネントを宣言的に実装することができます。


VStack {
    MapView()
        .edgesIgnoringSafeArea(.top)
        .frame(height: 300)

    CircleImage()
        .offset(x: 0, y: -130)
        .padding(.bottom, -130)

    VStack(alignment: .leading) {
        Text("Turtle Rock")
            .font(.title)
        HStack(alignment: .top) {
            Text("Joshua Tree National Park")
                .font(.subheadline)
            Spacer()
            Text("California")
                .font(.subheadline)
        }
    }
}

見慣れない記法ですがSwift5.1のFunction Builderという機能を使った一つの例です。
Function Builderで作られたViewBuilderにVStackとHStackが適合しているからこのような機能が使えます。

ViewBuilderのドキュメントはこれです。

/// The `ViewBuilder` type is a custom parameter attribute that constructs views from multi-statement
/// closures.
///
/// The typical use of `ViewBuilder` is as a parameter attribute for child view-producing closure
/// parameters, allowing those closures to provide multiple child views. For example, the following
/// `contextMenu` function accepts a closure that produces one or more views via the `ViewBuidler`.
///
/// ```
/// func contextMenu<MenuItems : View>(
///         @ViewBuilder menuItems: () -> MenuItems
///     ) -> some View
/// ```
///
/// Clients of this function can use multiple-statement closures to provide several child views, e.g.,
///
/// ```
/// myView.contextMenu {
///     Text("Cut")
///     Text("Copy")
///     Text("Paste")
///     if isSymbol {
///         Text("Jump to Definition")
///     }
/// }
/// ```
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@_functionBuilder public struct ViewBuilder {

    /// Builds an empty view from an block containing no statements, `{ }`.
    public static func buildBlock() -> EmptyView

    /// Passes a single view written as a child view (e..g, `{ Text("Hello") }`) through
    /// unmodified.
    public static func buildBlock<Content>(_ content: Content) -> Content where Content : View
}

@_functionBuilder public struct ViewBuilder {}@_functionBuilderという修飾子をつけることでViewBuilderのstructをFunction Builderとして利用できるようになります。

しかし、VStackの定義ではViewBuilderの記述がありません。

/// A view that arranges its children in a vertical line.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct VStack<Content> where Content : View {

    /// Creates an instance with the given `spacing` and Y axis `alignment`.
    ///
    /// - Parameters:
    ///     - alignment: the guide that will have the same horizontal screen
    ///       coordinate for all children.
    ///     - spacing: the distance between adjacent children, or nil if the
    ///       stack should choose a default distance for each pair of children.
    @inlinable public init(alignment: HorizontalAlignment = .center, spacing: Length? = nil, content: () -> Content)

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required `body` property.
    public typealias Body = Never
}

どうやってViewBuilderとVStack/HStackが紐付いているのかを質問しました。

回答

Labからの回答は、なんとドキュメントにはそのことは書かれていないとのことでした。
少々気が抜けてしまいましたが、じゃあ、独自実装でViewBuilderに適応させたView作りたいときはどう作ればいいんだと話したらこんなコードを作っていただけました。

struct MyStack<Content> : View where Content: View {
    let content: () -> Content
    
    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }
    
    var body: some View {
        content()
    }
}

struct MyView: View {
    var body: some View {
        MyStack {
            Text("")
            Text("")
        }
    }
}

なるほどinit(@ViewBuilder content: @escaping () -> Content)でイニシャライザに@ViewBuilderをつければいいわけですね。

謎が一つ解けました。
(独自のViewがSwiftUIで使えるのかは不明です)

参考になれば幸いです。

Author image

About Sato Takeshi

  • Tokyo, Japan