カテゴリー: iOS

【Swift】SwiftUIの理解のためDSL記法とFunctionBuilderを調べてみた

はじめに

Xcode11でSwiftUIという機能が追加されました。iOSの開発者になってから、StoryboardとSwiftと同じような大きな影響が今後出てくるのではないかと思っております。今回はSwiftUIの記述方法にも関係しているDSLとDSLを実現するための関数ビルダという機能について触れていこうと思います。macOS10.15じゃないとCanvasという機能が使えず、せっかくなので少し背景を調べたものとなります。

DSL(ドメイン固有言語)について

SwiftUIを調べていると出てくるDSLという言葉、何らかの問題領域や処理対象向けに特化した記述言語を指します。
しかしこのような機能があればDSLであるとか、このような水準に達していれば汎用言語であるといったような厳密な基準や定義はないらしいです。
例としてWebページ記述のためのHTML、スタイル記述言語のCSS、データベースへの問い合わせ言語SQLのようなものDSLと思ってください。

またrailsにおけるテストフレームワークのRSpecのように開発用言語の中にその一部として記述できるDSLを埋め込みDSLといい
SwiftUIはユーザーインターフェースに特化したSwiftの埋め込みDSLとなります。
SwiftUIは文字列を表示する場合などは下記のようにシンプルに記述できます。

        Text("Hello, World!")

また文字の色を変更したい時や、その他のプロパティを設定したい場合は、下記のように続けて表記することで、シンプルかつわかりやすく記述することができます。

        Text("Hello, World!")
            .font(.title)
            .foregroundColor(.red)

関数ビルダ

関数ビルダとはプログラム中に記述されたDSLの構文要素を組み合わせ、Swiftのインスタンスを生成する関数を作る仕組みです。SwiftUIのような埋め込みDSLを実装するための中心的な機能です。

定義方法

ひとまとまりのDSL記述は、全体として一つのSwiftのインスタンスとして解釈されます。
@functionBuilder(Xcode11.3では@_functionBuilder)という属性を指定することで定義が可能です。
buildBlock関数はこの関数ビルダを利用する際に暗黙的に呼び出されインスタンスを返却します。

@_functionBuilder
struct ArrayMaker {
    //buildBlock関数が関数構築メソッドとして呼ばれる。
    static func buildBlock<T>(_ components: T...) -> [T]{
        return components
    }
}

使用方法

関数ビルダを使用するためには「@」+関数ビルダ型の名前をつけることで利用できます。今回であれば@ArrayMakerです。
実際に記述してみましょう。シンプルな書き方であると下記のような形です。

@ArrayMaker
func initIntArray(_ n: Int) -> [Int] {
    1
    2;2
    3;3;3
    n;n;n;n
}

@ArrayMaker
func initStringArray(_ s:String) ->[String]{
    "test:";s
}

ArrayMakerではジェネリクスを利用しているので、型はなんでもいいので、文字列でもIntでも作成可能です。
また書かれている内容を見ると普段のSwiftのコードとは異なることがわかるかと思います。関数ビルダの関数内部には式を文として列挙できます。
式文と呼ばれ、文なので改行や[;]が区切りとなります。

先ほどのような使い方では関数ビルダを利用するメリッとが不明かもしれません。実際に使われる方法としては下記のような使われ方がされます。
クロージャを引数とする関数を定義してクロージャの引数で関数ビルダを使用します。先ほどと行なっていることは変わらないですが、使いやすそうです。

func strList(@ArrayMaker _ body:() ->[String] )
    -> [String]{
        return body()
}

let str = strList {
    "テスト"
    "test"
}

SwiftUIの関数ビルダ

せっかくなのでSwiftUIのToggleのコードで実際にどのように使われているかみていきます。

initの中の@ViewBuilder label:labelとしてクロージャを引数とする関数を定義しています。

public struct Toggle<Label> : View where Label : View {

    /// Creates an instance that displays state based on `isOn`.
    ///
    /// - Parameters:
    ///     - isOn: Whether `self` is "on" or "off".
    ///     - label: A view that describes the effect of toggling `isOn`.
    public init(isOn: Binding<Bool>, @ViewBuilder label: () -> Label)

    /// Declares the content and behavior of this view.
    public var body: some View { get }

    /// 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 = some View
}

ViewBuilder側では下記のbuildBlockが呼ばれていることでしょう。

@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はカスタム属性の一種です。Swift5.1から他にも下記のようなカスタム属性が追加されています。
どれもこれも理解が及んでいませんが、SwiftUIの後に改めて調べていこうと思います。
@propertyWrapper
@dynamicCallable
@dynamicMemberLookup

さいごに

最後までありがとうございました。アプリの開発では特に意識をしていなかったので、理解を進めるのにとても時間がかかりました。もうすぐiOS13が出るのに、開発アプリの下限対象がiOS9です。自分の頭もそれぐらいで止まっている気がしてきたので、少しずつバージョンアップしていきます。

おすすめ書籍

suzuki

シェア
執筆者:
suzuki
タグ: Swift

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

3週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

1か月 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前