URBAN HACKS iOSプロジェクトで活用しているSwift Macroについて
こんにちは、モバイルエンジニアの藤野です。
以前は東急カードプラスを担当していて、現在は東急線アプリのiOSを担当しています。
今回はURBAN HACKS内でのSwift Macroの運用について紹介したいと思います。
URBAN HACKS共通マクロの運用
現在URBAN HACKS内の東急線アプリや東急カードプラスのiOSプロジェクトでは、独自のSwift Macroを導入しています。
その中でもプロジェクトを横断して使える共通マクロについては、GitHubのprivate repositoryにを用意して、各プロジェクトがSPM経由で取り込んで利用することができるようになっています。
共通化した経緯としては、元々東急カードプラスで作成して運用していたマクロを他のプロジェクトで利用したい場面があったため、一つのSwift Packageにまとめて、プロジェクトから分離することにしました。
マクロを使うような場面では、プロジェクトによらず同じようなボイラープレートが発生するケースが多く、共通化することで各プロジェクトの開発効率を上げることができます。
共通マクロの実装にあたり、OSSを活用することも検討しました。
しかし、Sourceryなど既存のメタプログラミングツールのテンプレートと同じく、社内の利用ケースに応じて微調整が必要な場面が多いため、OSSでは対応が難しいと考えて、独自の実装をすることにしました。
利用しているマクロの紹介
マクロのパッケージ(UHMacro
パッケージ)については、Xcodeで作成できる標準のテンプレートを踏襲して、以下のような構成になっています。
UHMacro
内で各マクロの宣言をして、実装はUHMacroImplements
内で記述しています。
実行用ターゲットのUHMacroClient
は、元のテンプレートに付随していたもので必要ではありませんが、開発時の動作確認用に残してあります。
一部のマクロの利用ケースと、実装内容を簡単に解説していきます。
URLマクロ
こちらは、最もシンプルなExpressionMacro
になっています。
swift-syntaxのExampleにもある、URLのstring literalをコンパイル時にvalidateして、有効な場合はforce unwrapでOptional
無しのURL
として返すマクロです。
事前に変換が成功することがわかっている固定値のURLに対しても、URL.init(string:)
を利用するとURL?
になってしまうのはかなり扱いづらく、どこかでOptional
を外す処理を書かないといけません。
force unwrapを利用する場合には、必ずURL
への変換が成功することを確認する必要がありますが、static assertの考え方で、マクロを介してコンパイル時に検証する事ができれば、事故を防ぐ事ができます。
URBAN HACKS内でもプロジェクトを問わず、WebViewや外部ブラウザで表示する固定値のURLをコード内で扱うことが多いため、利用できるシチュエーションは多いです。
マクロの処理としては、swift-syntaxの例と同様に、expansion
の中で実際に引数のstring literalをURL.init(string:)
に渡して、変換に失敗した際はエラーを投げるようにしています。
let url = #URL("https://10q89s.jp/")
let url = URL(string: "https://10q89s.jp/")!
AutoInitマクロ
こちらは、class
やstruct
のmemberwise initializerを生成するMemberMacro
で、そのクラスのアクセスレベルを引き継ぐことができます。
URBAN HACKSではSwift Packageを利用してパッケージ分割をしているため、パッケージを跨ぐpublic
クラスの定義でmemberwise initializerを書かなければいけないことが多く、利用する機会は多いです。
特に、手書きでDIを行っている部分では、巨大なボイラープレートが発生しやすく、大きくコードを削減する事ができました。
マクロの処理としては、クラスの中からstored propertyのみを抜き出して、上から順番にinit
のパラメータリストを生成しています。
その他の機能については、現状はシンプルにアクセスレベルの変更のみ実装していますが、必要に応じてinitializerに含めないパラメータの指定や、デフォルト引数の指定などをサブのマクロで実現する拡張が考えられます。
@AutoInit
public struct Hoge {
static let const = 42
private var id: Int
private var name: String
var comment: String?
var isChecked = false
var closure: () -> Void
var displayName: String {
name.uppercased()
}
var isFocused: Bool = false {
didSet {
print(isFocused)
}
}
}
public struct Hoge {
static let const = 42
private var id: Int
private var name: String
var comment: String?
var isChecked = false
var closure: () -> Void
var displayName: String {
name.uppercased()
}
var isFocused: Bool = false {
didSet {
print(isFocused)
}
}
public init(
id: Int,
name: String,
comment: String? = nil,
isChecked: Bool = false,
closure: @escaping () -> Void,
isFocused: Bool = false
) {
self.id = id
self.name = name
self.comment = comment
self.isChecked = isChecked
self.closure = closure
self.isFocused = isFocused
}
}
EnumIdentifierマクロ
こちらは、associated valueを持つenum
のcase
名から、String
の識別子を生成するMemberMacro
です。
URBAN HACKSのプロジェクトでは、Firebaseなどに送るログのイベントをenum
で定義することが多く、各パラメータはassociated valueで表現する方式をとっています。
そのため、イベント名を記述する際にrawValue
を使うことができず、毎回computed propertyで自明なコードを書く必要がありました。
enum ActionEvent {
case detailButtonTap(itemId: Int)
case usageButtonTap
...
var name: String {
switch self {
case .detailButtonTap:
"detailButtonTap"
case .usageButtonTap:
"usageButtonTap"
...
}
}
このような巨大なボイラープレートを解消するために、一定のルールに従っているものについては、マクロを利用して、イベント名のcomputed propertyを自動生成するようにしました。
既存コードの適用可否については、シェルスクリプトを利用して、case
名とイベント名が全て対応規則に沿っているかを確認して判断しています。
現状全てのログイベントに対応規則があるわけではないので、利用できない部分もありましたが、今後リファクタリングをしながら適用できる箇所を増やしていきたいと思います。
マクロの処理については、enum
の中のcase
名を抜き出して、それをString
として返すcomputed propertyを生成します。
こちらのcomputed propertyについては、各プロジェクトの利用ケースに合わせて、プロパティ名を変更できるようにしています。
@EnumIdentifier(name: "name")
public enum TestEnum {
case foo, foo2
case bar(id: Int)
case yoo(String)
}
@EnumIdentifier(name: "name")
public enum TestEnum {
case foo, foo2
case bar(id: Int)
case yoo(String)
public var name: String {
switch self {
case .foo:
"foo"
case .foo2:
"foo2"
case .bar:
"bar"
case .yoo:
"yoo"
}
}
}
おわりに
今回はSwift Macroの運用の話でしたが、内製開発のメリットの一つとして、共通モジュールを利用することで、プロジェクトを横断した開発効率の向上が可能なことが挙げられます。
コードベースの課題に限らず、プロジェクト間のシナジーを活かした業務の効率化に、今後も取り組んでいきたいと思います。
Discussion