dip Engineer Blog

Engineer Blog
ディップ株式会社のエンジニアによる技術ブログです。
弊社はバイトル・はたらこねっとなど様々なサービスを運営しています。

Sourceryでボイラープレートを自動生成したい!

はじめに

こんにちは。iOSエンジニアの@satoshi-babaです。
バイトルアプリではDIコンテナとしてNeedleを導入したのですが、全ての画面に対してComponentを作成するという難関がありました。
これをSourceryで解決したので、その時の話を書いていきたいと思います。

Needleを導入した。

冒頭にも述べたように、バイトルアプリではDIコンテナとしてNeedleを導入しました。
Needleについては細かいことは割愛させていただくのですが、依存性の注入を補助するDIコンテナのライブラリになります。
画面に対してさまざまなものを注入していくわけですが、そのためにはComponentというものを定義していく必要があります。

//
//  HogeComponent.swift
//  SampleApp
//

import UIKit
import NeedleFoundation

protocol HogeBuilder {
    func makeViewController() -> UIViewController
}

public protocol HogeDependency: Dependency {
}

final class HogeComponent: Component<HogeDependency>, HogeBuilder {
    override public init(parent: Scope) {
        super.init(parent: parent)
    }

    func makeViewController() -> UIViewController {
        R.storyboard.hoge.instantiateInitialViewController()!
    }
}

全ての画面に対してComponentを定義していくわけですが、100画面くらいありますので手作業で作るのは現実的じゃないなぁと思いました。
画面遷移パラメーターなどは手作業でやる必要があるのですが、ひとまずSourceryというもので大枠を自動生成することにしました。

Sourceryとは?

SourceryとはSwift言語用のコードジェネレーターで、クラスの定義を解析してコードの生成を補助してくれます。
既存のSwiftコードを利用した定型的なソースコードの生成に向いていて、主にボイラープレートの生成を簡略化することができます。

細かいところはリポジトリを参照してみてください!
https://github.com/krzysztofzablocki/Sourcery

Sourceryでやってみた。

まずは全てのViewControllerに対してComponentを生成するようにしてみます。
Sourceryで解析した結果は、Stencilというテンプレート言語で作成したテンプレートに沿って出力することができます。

ViewControllerに対してComponentを生成するためのテンプレートがこちらです。(view_component.stencil)

{% for type in types.classes %}
{% if type.name | contains:"ViewController" %}
//
//  {{ type.name | replace:"ViewController","Component" }}.swift
//  SampleApp
//

import NeedleFoundation
import UIKit

public protocol {{ type.name | replace:"ViewController","Builder" }} {
    func makeViewController() -> UIViewController
}

public protocol {{ type.name | replace:"ViewController","Dependency" }}: Dependency {
}

public final class {{ type.name | replace:"ViewController","Component" }}: Component<{{ type.name | replace:"ViewController","Dependency" }}>, {{ type.name | replace:"ViewController","Builder" }} {
    public override init(parent: Scope) {
        super.init(parent: parent)
    }

    public func makeViewController() -> UIViewController {
        R.storyboard.{{ type.name | lowerFirstLetter | replace:"ViewController","" }}.instantiateInitialViewController()!
    }
}
{% endif %}
{% endfor %}

これをこんな感じで実行する!

sourcery --sources ./ --templates ./view_component.stencil --output ./Components.swift

そうするとComponentが生成されます。

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

//
//  HogeComponent.swift
//  SampleApp
//

import UIKit
import NeedleFoundation

protocol HogeBuilder {
    func makeViewController() -> UIViewController
}

public protocol HogeDependency: Dependency {
}

final class HogeComponent: Component<HogeDependency>, HogeBuilder {
    override public init(parent: Scope) {
        super.init(parent: parent)
    }

    func makeViewController() -> UIViewController {
        R.storyboard.hoge.instantiateInitialViewController()!
    }
}

//
//  FugaComponent.swift
//  SampleApp
//

import UIKit
import NeedleFoundation

protocol FugaBuilder {
    func makeViewController() -> UIViewController
}

public protocol FugaDependency: Dependency {
}

final class FugaComponent: Component<FugaDependency>, FugaBuilder {
    override public init(parent: Scope) {
        super.init(parent: parent)
    }

    func makeViewController() -> UIViewController {
        R.storyboard.fuga.instantiateInitialViewController()!
    }
}

これでもいいかな〜と思うのですが、メンテナンスの都合上、できればComponentごとにファイルを分けたいですよね。
なのでちょっと手間ですが、findで対象ファイルを検索して、1ファイルごとsourceryにかけていきます。

#!/bin/bash
FILE_LIST=`find ./ -type f -name "*ViewController.swift"`
for FILE in $FILE_LIST; do
    sourcery --sources $FILE --templates ./view_component.stencil --output ${FILE//ViewController/Component}
done

そうすると無事に分割してファイルが生成できました!めちゃくちゃ簡単にできちゃいますね!

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

//
//  HogeComponent.swift
//  SampleApp
//

import UIKit
import NeedleFoundation

protocol HogeBuilder {
    func makeViewController() -> UIViewController
}

public protocol HogeDependency: Dependency {
}

final class HogeComponent: Component<HogeDependency>, HogeBuilder {
    override public init(parent: Scope) {
        super.init(parent: parent)
    }

    func makeViewController() -> UIViewController {
        R.storyboard.hoge.instantiateInitialViewController()!
    }
}
// Generated using Sourcery 1.8.1 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT

//
//  FugaComponent.swift
//  SampleApp
//

import UIKit
import NeedleFoundation

protocol FugaBuilder {
    func makeViewController() -> UIViewController
}

public protocol FugaDependency: Dependency {
}

final class FugaComponent: Component<FugaDependency>, FugaBuilder {
    override public init(parent: Scope) {
        super.init(parent: parent)
    }

    func makeViewController() -> UIViewController {
        R.storyboard.fuga.instantiateInitialViewController()!
    }
}

パラメーターも自動生成していきたいですが、長くなりそうなので今回はここまでとしたいと思います!

まとめ

ということでSourceryでボイラープレートの自動生成をしてみた記事でした。
Sourceryは繰り返し生成するようなときに真価を発揮すると思いますが、今回のように大量ファイルを生成するようなときにも十分使えるかなと思います!
導入も簡単で実装例も公開されてますので、皆さん使ってみてください!

著者

dippeople.dip-net.jp