Interface Builderの問題点
iOSアプリを開発する限り付き合わなければいけないInterface Builder。
視覚的にViewの構造を確認できる便利な代物ですが、人類が扱うには少々厳しいポイントがいくつかあります。その1つが「指定できるオプションの多さ」です。
現在、Interface Builderのツールタブは6つに別れておりオプションを一望することはできません。そのため、指定するオプションを間違えていたとしても気づきにくい仕様になっています。
例えば、プロジェクト全体でRelative to margin
の使用を禁止で統一しているとします。1 しかし、プログラマーはInterface Builder上でなかなか気がつくことができません。コードレビューで見つかる可能性もありますが、レビュアーの負担はできるだけ減らしたいです。
IBLinter
そこで、Interface Builder用のLinter を作りました。
SwiftLintと同様にBuild Phase
にコマンドを仕込んで警告を出したり、ビルドエラーで落とすことができます。
インストール
Homebrewで入れられます。
$ brew install kateinoigakukun/homebrew-tap/iblinter
使い方
Build Phase
で + New Run Script Phase して以下のスクリプトを仕込みます。
if which iblinter >/dev/null; then
iblinter lint
fi
これによりビルドする度にLintが走り、問題があれば警告を出すようになります。
設定
.iblinter.yml
という設定ファイルをプロジェクトのルートディレクトリに配置すると色々と設定できます。
enabled_rules: #有効にするルールid
- relative_to_margin
- misplaced
disabled_rules: #無効化するルールid
- custom_class_name
- enable_autolayout
excluded: #Lintから除外するパス
- Carthage
- Pods
以下は現在定義してあるルールです。
Rule id | 説明 |
---|---|
custom_class_name |
ViewControllerのCustom Classをファイル名と一致させる |
relative_to_margin |
Relative to margin を禁止する 1
|
misplaced |
ViewがAutolayoutの制約通りに配置されていない時にビルドエラーにする |
enable_autolayout |
useAutolayout をオンでないときにビルドエラーにする |
IBLinterの実装
Interface Builderで作成したStoryboardやxibファイルの実態は単なるXMLなので、コマンド内部で行っているのは
- XMLをパース
- パースしたオブジェクトをバリデート
- Xcodeの警告として表示
のみです。XPathや正規表現でバリデートする方がパフォーマンスは良さそうですがメンテナンス性に欠けるため、Swiftオブジェクトにマッピングしています。
以下のコードはrelative to margin
を禁止するルールのコードです。一度マッピングして型を付けているのでコード補完によって気持ちよくルールが書けます。
public struct RelativeToMarginRule: Rule {
public static let identifier: String = "relative_to_margin"
public init() {}
public func validate(storyboard: StoryboardFile) -> [Violation] {
let scenes = storyboard.document.scenes
let viewControllers = scenes?.flatMap { $0.viewController }
return viewControllers?.flatMap { $0.rootView }
.flatMap { validate(for: $0, file: storyboard) } ?? []
}
public func validate(xib: XibFile) -> [Violation] {
return xib.document.views?.flatMap { validate(for: $0, file: xib) } ?? []
}
private func validate(for view: ViewProtocol, file: InterfaceBuilderFile) -> [Violation] {
guard let constraints = view.constraints else {
return view.subviews?.flatMap { validate(for: $0, file: file) } ?? []
}
let relativeToMarginKeys: [InterfaceBuilderNode.View.Constraint.LayoutAttribute] = [
.leadingMargin, .trailingMargin, .topMargin, .bottomMargin
]
let attributes = constraints.flatMap { [$0.firstAttribute, $0.secondAttribute] }.flatMap { $0 }
let violations: [Violation] = attributes.filter { relativeToMarginKeys.contains($0) }
.map { at in
let message = " \(at) is deprecated in \(view.customClass ?? view.elementClass)"
return Violation.init(interfaceBuilderFile: file, message: message, level: .warning)
}
if let subviews = view.subviews {
return subviews.flatMap { validate(for: $0, file: file) } + violations
} else {
return violations
}
}
}
まとめ
Interface Builderと上手く付き合っていく方法の1つを紹介してみました。今後も色々と模索して、幸せな世界を手に入れたいですね。
まだまだバリデートのルールが少ないのでPR下さい。
https://github.com/kateinoigakukun/IBLinter