🦾

SwiftでSwiftコードを自動生成する(Sourcery × Swift Templates)

2022/10/18に公開

Sourcery というライブラリはテンプレートを書くとSwiftのコードを自動生成してくれます。そのテンプレートをSwiftで書いてSwift言語だけで自動生成をしようというお話しです。自動生成すればcaseにAssociated Valueを追加した際の漏れを防げたりと、開発効率の向上に大きく貢献できます。
Sourcery自体使ったことない方でもこの記事読んでいただければある程度は使えるようになるかと思います!

要約 (Sourcery使ったことある方向け

テンプレートをStencilで書かれる方が多いと思いますが、Swift templatesを使うことでSwiftの文法でテンプレートを記述できるようになります。
使い方は.stencilファイルを作る代わりに.swifttemplateファイルを作ってテンプレートを記述していくだけです。
テンプレートの書き方はStencilと変わらず、タグで囲ってその中にコードを書いていきます。そのコードをSwiftで書けるのが超便利です。Foundationを使っているようなので、出来ること出来ないことはFoundation依存という感じです。変数名にSwiftの予約語が使えないとかがあります。あと、typeなどで型を参照する場合はキャストしてあげる必要があります(実践編のテンプレート参照)。
Stencilとはタグが異なるので以下のタグさえ覚えてしまえばあとはSwiftの文法で簡単に書けちゃいます。実際にテンプレートで使う値とかはドキュメントを参照しながらでも!

タグ一覧

  • <% %>:普通のタグ。ただ、実際使ってみるとわかるが、<%で空白、%>で改行が自動生成されたコードに挿入されるため、後述のタグを基本的には使う。
  • <%= %>:値を挿入するためのタグ。Swiftで言う\()みたいなイメージ。
  • -%>:前述した%>の改行を挿入しないバージョン。
  • <%_:前述した<%の空白を挿入しないバージョン。<%_ -%> みたいな感じで組み合わせて使うのが基本。
  • _%>:タグより後ろにある空白をトリムしてくれる。
  • <%# %>:コメント。
  • <%- include(“relative_path_to_template.swifttemplate”) %>:他のswifttemplateファイルをincludeできる。
  • <%- includeFile(“relative_path_to_file.swift”) %>:他のswiftファイルをincludeできる。

前提知識

前提知識について話すので、こんなの読んでられない!って方は実践の方に進んでください。

Sourceryとは

Sourcery とは、元となるSwiftのコードを読み込んでテンプレートに則った形式で別のSwiftファイルにコードを出力するライブラリです。ボイラープレートを自動生成することで、列挙した時の漏れを防いだり、複数回同じようなコードを書く手間を省けたりできます。実践で使う元となるコードの場合、元々Assosiated Valueが設定されていないcaseに仕様変更などでAssosiated Valueを追加する必要がでた場合、ビルドエラーが発生しないため修正が必要なことに気づけず余計な不具合が生まれる恐れがあります。そういったことを自動生成で防いでくれます。

Sourcery Pro と言うXcodeの機能を拡張してXcode上で色々できるようにするものもありますが、それについては今回触れません。(よくわかってないだけ)

テンプレート

自動生成する上で肝になってくるのがテンプレートです。Sourceryのテンプレートには以下の3種類があります。

  • Stencil templates
  • Swift templates
  • JavaScript templates
    基本的な書き方は全部共通で、タグで囲んでその中に自動生成を制御するコードを書いていくのですが、そのタグや中に書くコードの言語が異なります。文字で説明してもわかりづらいと思うのでStencil templatesのテンプレートの例を紹介します。
元となるファイル.swift
class Foo {
}

enum Bar {
}
テンプレート.stencil
{% for type in types.all %}
  print("{{ type.name }}")
{% endfor %}

これを実際にビルドして自動生成すると以下のコードが生成されます。

生成されるファイル.swift
print("Foo")
print("Bar")

少し分かりづらいですが、テンプレートのタグ({% %})で囲まれたところで制御して、それ以外の部分(print(“”)の部分)はそのままコードに出力されています。例外として{{ }}で囲まれた部分は値をコードに出力できるようになっています。typesには、元となるコードで定義されているclassやenumなどすべてのタイプが格納されています。

Swift templatesの使い方

Swift templatesは先ほど見せたテンプレートの例の{% %}の中のコードをSwift言語で書ける様にしたものです。なので、Swiftに馴染んでいる方ならタグさえ覚えてしまえばいつもの感覚でテンプレートを書いていくことができます。Swift templatesでのタグは以下になります。(要約のコピペです)

  • <% %>:普通のタグ。ただ、実際使ってみるとわかるが、<%で空白、%>で改行が自動生成されたコードに挿入されるため、後述のタグを基本的には使う。
  • <%= %>:値を挿入するためのタグ。Swiftで言う\()みたいなイメージ。
  • -%>:前述した%>の改行を挿入しないバージョン。
  • <%_:前述した<%の空白を挿入しないバージョン。<%_ -%> みたいな感じで組み合わせて使うのが基本。
  • _%>:タグより後ろにある空白をトリムしてくれる。
  • <%# %>:コメント。
  • <%- include(“relative_path_to_template.swifttemplate”) %>:他のswifttemplateファイルをincludeできる。
  • <%- includeFile(“relative_path_to_file.swift”) %>:他のswiftファイルをincludeできる。

これらを駆使してテンプレートを書いていきます。先ほどのStencilで書いたテンプレートをSwift templatesで書くと以下の様になります。

テンプレート.swifttemplate
<% for type in types.all { -%>
  print("<%= type.name %>")
<% } %>

タグの中のコードがSwiftの文法になっています。この様に、数種類のタグを覚えるだけで基本的にはテンプレートを書くことができます。テンプレートで使用するtypesなどの値に関してはドキュメントを参照しながら使っていくのが分かりやすいと思います。

Swift templatesの制約

テンプレートの変数名などにSwiftの予約語が使えないです。これは、Swift templatesを使用した場合、Swiftのコンパイラを通して色々やっている感じっぽい(憶測ですが)ので変数名にcaseとかは使えません。
また、Foundationをimportしているため、基本的にはFoundationにできることはできて、できないことはできないといった感じになりそうです。

実践

Sourceryはコマンドラインツールなので生成の仕方は手動でコマンド走らせるか、Build Phasesに Run Scriptを記述してビルド時に走らせるかになると思います。今回はCocoaPodsでインストールして、Build Phasesに追加する方針で行きます。

例として、ニュースアプリにおいてクリックされた場所をトラッキングするという要件で、トラッキングに必要なパラメータ(enumで列挙されているもの)からAssoceated Valueかnilを返すコードを自動生成することを目標として説明していきます。元となるコードと今回のゴール(自動生成されるファイル)は以下です。

trackingManager.swift
// 元となるコード

enum ClickArea {
    case article(let id: String)
    case search
    case back
    case myPage
    case ad(let id: String)
}
ClickArea+id.generated.swift
// 自動生成されたファイル

// Generated using Sourcery 1.8.2 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT

extension ClickArea {
    var id: String? {
        switch self {
        case .home: return nil
        case .search: return nil
        case .myPage: return nil
        case .article(let id): return id
        case .tab(let id): return id
        }
    }
}

テンプレートの作成

まずテンプレートのファイルを作成します。適当な場所に.swifttemplateファイルを作成しましょう。今回はルートにClickArea+id.swifttemplateを作成しました。作成できたら早速テンプレートを書いていきましょう。今回作成したテンプレートは以下になります。

ClickArea+id.swifttemplate
extension ClickArea {
    var id: String? {
        switch self {
        <%_ if let clickArea = type["ClickArea"] as? Enum { -%>
          <%_ for enumCase in clickArea.cases { -%>
            <%_ if enumCase.associatedValues.contains(where: { $0.localName == "id" }) { -%>
        case .<%= enumCase.name %>(<% for associatedValue in enumCase.associatedValues { %><% if associatedValue.localName == "id" { %>let id<% } else { %>_<% } %><% if associatedValue != enumCase.associatedValues.last { %>, <% } %><% } %>): return id
            <%_ } else { -%>
        case .<%= enumCase.name %>: return nil
            <%_ } -%>
          <%_ } -%>
        <%_ } -%>
        }
    }
}

自動生成で制御しないベタ書きの部分はタグの外に書きます。あとはドキュメントを参照して取り出す値を調べて、使い方で説明したタグを組み合わせながらswiftでコードを書いていきます。少しだけコード中の値に触れると、指定のclassやenumなどを取り出すときはtype[“取り出したいclass名”]として辞書から取り出した上でキャストします。そうすることでenumならcaseを取り出せる様になります。7行目が大変なことになっていますが、一行で色々操作したい場合はうまくタグを組み合わせないとこの様になります(筆者は解決できなかった、、)。分かりづらいですが、forとifを組み合わせた基本的なことしかやっていないので詳しい説明は省かせていただきます。

また、実際にどの様に生成されるか確認しながらやりたいという方は、一旦タグを使わずに適当に書いてビルド通る状態にした上で次へ進んでください。

Run Scriptの追加

テンプレートの記述が完了したら(完成してなくてもいいですが)、自動生成を行うターゲットのBuild PhasesにRun Scriptを追加していきます。
ターゲットを選んで左上の+ボタンを押したら新たなRun Scriptが追加されるので、画像の様にCompile Sourcesの上に移動します。これはコンパイルされるよりも前に自動生成をすることで、生成されたファイルをコンパイルに含めるためです。

お好みでリネームもしてください。(今回はGenerate ClickArea+idという名前にリネームします)

Run Scriptの記述

続いて先ほど作成したRun Scriptの内容を記述していきます。先ほど追加したRun Scriptのプルダウンを開いて、スクリプトの記述欄に元となるSwiftファイルやテンプレートのパスなどを記述していきます。追加するスクリプトの例はこんな感じです。生成するファイルのパスは、このあと自動で生成されるファイル名にもなります。

"${PODS_ROOT}/Sourcery/bin/sourcery" \
    --sources "${SRCROOT}/${TARGET_NAME}/TrackingManager.swift" \  // 元となるファイルのパス
    --templates "${SRCROOT}/ClickArea+id.swifttemplate" \  // テンプレートのパス
    --output "${SRCROOT}/${TARGET_NAME}/ClickArea+id.generated.swift" \  // 生成するファイルのパス

主に使うのはこれですが、他にも色々オプションがあるので、詳しくはSourceryのREADMEを見てください。

Input Fileなどの追加

次にRun ScriptにInput File、Output Fileを追加していきます。ここではRun Scriptに記述したパス達をそのまま追加し
ていきます。Run Scriptの記述欄の下にあるInput FilesとOutput Filesにこんな感じで対応するパスをコピペしていきます。Input Filesに元となるファイルのパスとテンプレートのパス、Output Filesに生成されるファイルのパスを追加します。

生成されたファイルの参照をXcodeに渡す

最後にXcodeに参照を渡して自動生成されたファイルを使えるようにします。ここまで完了したら一旦ビルドしましょう。テンプレートにエラーがなければ先ほど設定した生成するファイルの場所に勝手にファイルが作られています。それをXcodeのメニューバーからFile>Add Files to “”を選択して追加しましょう(D&Dでも)。ファイルが生成されていない場合はテンプレートやRun Script、ファイルのパスなどに問題があるかもしれないので正しく設定しましょう。無事参照をXcodeに渡せたら導入は完了です!自分で書いたコードと同様に自動生成された値などが使える様になっています。お疲れ様でした!

最後に

SwiftでSwiftのコードを自動生成する方法を紹介しました。使いこなせばスナップショットテストの自動生成などもできるので、開発効率をより向上させてくれそうですね。簡単にですがSourceryの導入〜自動生成までを説明させていただきましたが、不明点や間違い等ございましたらコメントしていただけると幸いです。最後まで読んでいただき、ありがとうございました。

Discussion