#Swift fastlaneの次に来る? Sourcery🔮でメタプログラミングする


Sourcery

krzysztofzablocki/Sourcery

Sourcery scans your source code, applies your personal templates and generates Swift code for you, allowing you to use meta-programming techniques to save time and decrease potential mistakes.

テンプレートからSwiftのコードを生成するツール

Benefit

  • ボイラープレートコードが減り、DRY(Don't repeat yourself)原則を守りやすい
  • リファクタリング時のヒューマンエラーを減らす
  • テンプレートを書くと、リアルタイムにFeedbackが行われる

など、良さそうです

Installing

方法は以下の4通り

  • Binary form
  • Via CocoaPods
  • Via Swift Package Manager
  • From Source

今回はCocoaPodsでインストールします!

と思ったらpod installでエラーが出た

LoadError - cannot load such file -- nanaimo
/Library/Ruby/Gems/2.0.0/gems/xcodeproj-1.4.1/lib/xcodeproj/plist.rb:23:in `read_from_path'
/Library/Ruby/Gems/2.0.0/gems/xcodeproj-1.4.1/lib/xcodeproj/project.rb:200:in `initialize_from_file'
/Library/Ruby/Gems/2.0.0/gems/xcodeproj-1.4.1/lib/xcodeproj/project.rb:102:in `open'
/Library/Ruby/Gems/2.0.0/gems/cocoapods-1.2.0/lib/cocoapods/installer/analyzer.rb:851:in `block (2 levels) in inspect_targets_to_integrate'
/Library/Ruby/Gems/2.0.0/gems/cocoapods-1.2.0/lib/cocoapods/installer/analyzer.rb:850:in `each'
/Library/Ruby/Gems/2.0.0/gems/cocoapods-1.2.0/lib/cocoapods/installer/analyzer.rb:850:in `block in inspect_targets_to_integrate'
/Library/Ruby/Gems/2.0.0/gems/cocoapods-1.2.0/lib/cocoapods/user_interface.rb:64:in `section'
/Library/Ruby/Gems/2.0.0/gems/cocoapods-1.2.0/lib/cocoapods/installer/analyzer.rb:845:in `inspect_targets_to_integrate'
/Library/Ruby/Gems/2.0.0/gems/cocoapods-1.2.0/lib/cocoapods/installer/analyzer.rb:66:in `analyze'
/Library/Ruby/Gems/2.0.0/gems/cocoapods-1.2.0/lib/cocoapods/installer.rb:236:in `analyze'
/Library/Ruby/Gems/2.0.0/gems/cocoapods-1.2.0/lib/cocoapods/installer.rb:150:in `block in resolve_dependencies'
/Library/Ruby/Gems/2.0.0/gems/cocoapods-1.2.0/lib/cocoapods/user_interface.rb:64:in `section'
/Library/Ruby/Gems/2.0.0/gems/cocoapods-1.2.0/lib/cocoapods/installer.rb:149:in `resolve_dependencies'
/Library/Ruby/Gems/2.0.0/gems/cocoapods-1.2.0/lib/cocoapods/installer.rb:110:in `install!'
/Library/Ruby/Gems/2.0.0/gems/cocoapods-1.2.0/lib/cocoapods/command/install.rb:37:in `run'
/Library/Ruby/Gems/2.0.0/gems/claide-1.0.1/lib/claide/command.rb:334:in `run'
/Library/Ruby/Gems/2.0.0/gems/cocoapods-1.2.0/lib/cocoapods/command.rb:52:in `run'
/Library/Ruby/Gems/2.0.0/gems/cocoapods-1.2.0/bin/pod:55:in `<top (required)>'
/usr/local/bin/pod:23:in `load'
/usr/local/bin/pod:23:in `<main>'

解決

gem list | grep nanaimo  // print nanaimo (0.2.3, 0.2.2)
gem uninstall nanaimo -v 0.2.2

テンプレート用・生成コード用のフォルダを作成

なんでも良さそうですが、公式サンプルと同じ名前にします

  • Templates - テンプレート用フォルダ
  • CodeGenerated - 生成コード用フォルダ

プロジェクトにScriptを追加

Build Phases > Run Scriptに以下を追加

$PODS_ROOT/Sourcery/bin/sourcery {source} {templates} {output}

今回、プロジェクト名は SourcerySample にしたので、以下のScriptを追加しました

"${PODS_ROOT}/Sourcery/bin/sourcery" "${SRCROOT}/SourcerySample" "${SRCROOT}/Templates" "${SRCROOT}/CodeGenerated"

Usage

.stencilファイルにテンプレートを定義

Stencilとは

kylef/Stencil

Stencil is a simple and powerful template language for Swift. It provides a syntax similar to Django and Mustache. If you're familiar with these, you will feel right at home with Stencil.

syntaxがDjango、Mustacheに似た、Swiftのためのテンプレート言語

Stencilのテンプレート

今回はプロジェクト内の列挙型にcountを返すextensionを定義します
Stencilファイル名は、Enum+Countにします

Enum+Count.stencil
{% for enum in types.enums %}
extension {{ enum.name| }} {
static var count: Int { return {{ enum.cases.count }} }
}
{% endfor %}

検証用に、以下のSwiftの列挙型を用意しました

Planets.swift
enum Planets {

    case mecury
    case venus
    case earth
    case mars
    case jupiter
    case saturn
    case uranus
    case neptune
}

コマンドを実行

$ ./sourcery . templates output --watch

  • source - Swiftファイルのpath
  • templates - テンプレートのディレクトリのpath
  • output - 生成したコードのディレクトリのpath
  • --watch - このflagにより、Templateファイルの変更を受けてリアルタイムにコード反映を行う

生成されました

Finder

このSwiftファイルをXcodeのプロジェクトに追加します

コード

検証用の列挙型のcountを定義したExtensionが生成されています

Enum+Count.generated.swift
extension Planets {
static var count: Int { return 8 }
}

リアルタイムにコードを更新

一度コマンドを実行すると、停止するまでwatchが継続されます
その間、Stencilファイルやテンプレート定義に該当するファイルを変更すると、
⌘ + Sで生成コードがリアルタイムに更新されます

今までのテンプレートから少し変更して、extensionにコメントを付与してみます

Enum+Count.stencil
{% for enum in types.enums %}
// {{ enum.name }}のcount定義をするExtension ← 新たに追加したコメント
extension {{ enum.name }} {
static var count: Int { return {{ enum.cases.count }} }
}
{% endfor %}

⌘ + Sで保存すると、ジェネレートされたファイルに反映されています

Enum+Count.generated.swift

// Planetsのcount定義をするExtension
extension Planets {
static var count: Int { return 8 }
}

列挙型の宣言を増やしてもバッチリです

Enum+Count.generated.swift

// Planetsのcount定義をするExtension
extension Planets {
static var count: Int { return 8 }
}

// Zodiac.Signのcount定義をするExtension
extension Zodiac.Sign {
static var count: Int { return 12 }
}

Sample Code

mafmoff/SourcerySample

  1. cloneする
  2. pod install
  3. $ ./sourcery . templates output --watchのpathを書き換えて実行

リアルタイムで、コードジェネレートを試すことができます

テンプレートの書き方

その他のテンプレートの書き方は公式Readmeにまとまっています
krzysztofzablocki/Sourcery#writing-templates

参考

ありがとうございました!

まとめ

  • 4通りのインストール方法がある
  • テンプレートとジェネレート用のディレクトリが必要
  • --watchの間は、リアルタイム反映ができる
  • ⌘ + Sでジェネレートするコードを更新

投稿のタイトルについて

参考にさせていただいた#292: Metaprogramming with Sourcery 🔮

In modern times, things like Fastlane, CocoaPods, and Carthage come to mind. Slightly more seasoned folks may remember the emergence of Pull to Refresh, or BWToolkit.
Sourcery from Krzysztof Zabłocki is the latest addition to this set.

と書いてあったので、タイトルにしてみました

以上です
今回は参考リンクで勉強させていただきました
もっといろいろ触って学びたいです

読んでいただき、ありがとうございました