Better Swift
Swift Advent Calendar 2018 の 5 日目です。
折角の機会なので普段自分がよりよい Swift を書くためにやっていることを振り返って、まとめてみようと思います。
無意識にやっていることも多いと思うので、言語化できたら追記していきます。
※ iOS に依存した内容も少し含まれてますが、ご了承ください。
※ サンプルコードは Swift 4.2 (Xcode 10.1) です。
SwiftLint を入れる
Swift の linter です。
特にチームで開発する場合はコードレビューのコストを下げることができるので、できれば入れた方が良いです。
CI 環境が整っている場合は fastlane や Danger を組み合わせて CI 上で swiftlint を動かし、プルリク時に lint 系の指摘を自動でやってくれるような環境を整えると素敵です。
自分がプライベートの開発でも利用している .swiftlint.yml を置いておきます。参考にしたい方はどうぞ。
変更したら影響箇所がコンパイルエラーになるような実装を意識する
自分が Swift を好きな理由の一つが変更した部分に影響する箇所がコンパイルエラーになることです。
実行前にエラーになることで変更漏れなどのバグを大幅に減らすことができ、機能変更やリファクタリングなどが捗ります。
ただ、あまりにも自由に Swift を書いているとその恩恵を得られない実装になってしまうことがあり、非常にもったいないです。
思いつく全てのケースをあげているときりがないですが、いくつか代表的なものを紹介します。
Dictionary を避ける
Dictionary は非常に便利ですが、使う前にそれを専用の型にすることができないかを一度考えましょう。
Dictionary を使いたいケースはキーバリューを表現したい時だと思います。そのキーが (数が多すぎないレベルで) 有限であれば、struct
で実装した方が良いことが多いです。
String を避ける
ID を表す時などにそのまま String
を使いがちですが、これも struct
でその ID を表す型を作って利用した方が良いです。
struct UserID {
let rawValue: String
}
このようにすることで、どの場所でこの ID を表す文字列を利用しているかがわかりやすくなります。
また、色々な ID を取り扱っている場合、メソッドの引数で ID をたらい回しに受け渡す時に ID を使い間違えてしまうことも減らせます。(ID の型が異なるのでビルドエラーになります)
Optional を避ける
Swift の便利な機能の一つですが、使い方を誤ってしまうと可読性が落ちてしまいます。
例えば下記のような Optional です。
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyboard
.instantiateViewController(withIdentifier: "TopViewController") as? TopViewController
これは本当に nil
を許容すべきでしょうか?
また、少し Swift になれてくると guard let
などで安全にアンラップしようとしてしまうかもしれませんが、それも本当に必要でしょうか?
Storyboard から TopViewController
を取り出すこの実装は nil
にならないことを保証すべきです。nil になる場合は ID や型が間違っているということなので、アプリのリリース前に必ず直す必要があります。
// force cast を利用する
let viewController = storyboard
.instantiateViewController(withIdentifier: "TopViewController") as! TopViewController
もちろん、例えば自由入力欄のテキスト (String
) を Int
に変換したい場合は nil チェックをすべきなので、この部分は実装者 (or レビュワー) が適切な判断をする必要があります。
Optional は基本的には使う前にアンラップ処理を書かなければならずロジックを読む妨げになったり、実装を読んでいる人がいつ nil
になるんだろうと考えなければならなくなることもあります。
不要な Optional は避けたり、できるだけ早めにアンラップをしてアンラップ後の値を取り回すようにしましょう。
Enum
case の網羅
Swift の enum はかなり優れていますよね。switch 文で case を網羅していると、case が増えたときにその部分がビルドエラーになるのは素敵な機能の一つです。
この機能は if case
で分岐したり、switch 文で default:
を書いてしまうと機能しなくなってしまいます。これは非常にもったいないので case が増えた際に修正が必要になるかもしれない部分はできるだけ switch 文で case を網羅するように書いたほうが良いです。
enum ABTests {
case a
case b
case c
}
let experiment: ABTests = fetchExperiment()
switch experiment {
case .a: doSomething()
case .b, .c: break // 不要でもcaseを記載する
}
このようにすることで case d が増えたときに変更漏れを防ぐことができます。
case があまりにも多い時や case が増えても変更することがない時以外は case を網羅しておくほうが良いと思います。
また、何らかの条件に基づいて case を取得する実装の場合、switch 文で網羅できませんが、あえて何もしない switch 文を作って、強制的にビルドエラーにする方法も場合によっては便利です。
switch value {
case 1...10: return .case1
case 11...20: return .case2
default: return .case3
}
#if DEBUG // 念の為、本番ビルドには含めないようにする
switch value {
case .case1, .case2, .case3: break //MEMO: caseが増えたら↑を編集する
}
#endif
namespace
Swift で namespace を作りたいときは後述する Embedded Framework がおすすめですが、そこまで規模が大きくない場合は enum で namespace を作ると良いです。case のない enum はインスタンス化が出来ないため、予期せぬ使い方を防止できます。
enum Module {
static let value = "value"
struct Model {
...
}
}
struct と class の使い分け
Apple が良いドキュメントを提供しています。
https://developer.apple.com/documentation/swift/choosing_between_structures_and_classes
- Use structures by default.
- 基本的には struct を使う
- Use classes when you need Objective-C interoperability.
- Objective-C との互換性が必要な場合は class を使う
- Use classes when you need to control the identity of the data you're modeling.
- データの同一性を制御する必要があるときは class を使う
- 例) シングルトンや共通の設定を表現するときは class にする
- Use structures along with protocols to adopt behavior by sharing implementations.
- protocol で共通の実装をする場合は struct を使う
- → 共通実装を protocol で実現できる場合は struct にする。出来ない場合または継承が適している場合は class を使う
early return
if 文の入れ子はできるだけ避けて、guard 文などで early return するように意識したほうが良いです。
// if 文の Pyramids of doom
if condition1 {
// do something
if condition2 {
// do something 2
if condition3 {
// do something 3
}
}
}
↓
guard condition1 else { return }
// do something
guard condition2 else { return }
// do something 2
guard condition3 else { return }
// do something 3
early return を使う際に defer
が活かせるケースも多いです。適切に利用しましょう。
defer {
// 最後に実行したい処理
}
guard condition1 else { return }
// do something
guard condition2 else { return }
// do something 2
API のレスポンスは専用の型にする
API から返ってくる値を Dictionary などで取り扱うことは避け、専用の型にすべきです。型にすることで誤ったキーで値を取り出してしまうミスや利用時に値の型を意識する必要性が減ります。
また型にする際に、Codable
を利用すると良いです。JSONのデコードやレスポンスをキャッシュする際の取り回しが非常に楽になります。
struct Article: Codable {
let items: [Item]
struct Item: Codable {
let title: String
let content: String
let author: String
let publishDate: Date
}
}
.lazy
.flatMap
や .compactMap
などを使いこなせるようになったら、次は .lazy
をつけたほうが良いかを意識するようにすると良いです。
let selectedButton = array
.compactMap { $0 as? UIButton } // array.count の回数、flatMapの中身が実行されてしまう
.first { $0.isSelected }
↓
let selectedButton = array.lazy
.compactMap { $0 as? UIButton } // isSelected == true が見つかった段階で処理を切り上げてくれる
.first { $0.isSelected }
メソッド自体を .map に渡す
クロージャを引数に取る .map
などのメソッドにはメソッド自体を渡すことが出来ます。直接メソッドを渡すほうがスッキリとした味わいのコードになります。
func configure(_ view: ProductView) { ... }
let views: [ProductView] = ...
views.forEach(configure) // .forEach { configure($0) }
let intArray = ["1", "2", "🐱"].compactMap(Int.init) // Int のイニシャライザを compactMap に渡す
型の入れ子に extension を使う
深い Nested Type を作ると読みにくくなりがちです。その場合は extension を利用して入れ子にすることでできるだけフラットな記述をすることが出来ます。
struct Article {
let items: [Item]
}
extension Article {
struct Item {
let title: String
let category: Category
...
}
}
extension Article.Item {
enum Category {
case technology
...
}
}
Storyboard や xib から生成した View の初期化
Storyboard や xib から生成した View の場合はイニシャライザで必要なオブジェクトを渡すことが出来ません。そのため、生成後に外側から直接プロパティに値を入れてしまいたくなりますが、初期化方法がわかりにくくなるため、初期化用のメソッドを用意して生成時に外側からプロパティをあまり操作しないようにしましょう。
class Cell: UITableViewCell {
...
func configure(with value: Dependency) {
titleLabel.text = value.title
nameLabel.text = value.name
}
}
class ProfileView: UIView {
// static メソッドを作るとわかりやすい (できれば、protocol 化して I/F を揃えるようにしたい)
static func instantiate(with value: Dependency) -> ProfileView {
let view = Bundle.main.loadNibNamed("ProfileView", owner: nil, options: nil)[0] as! ProfileView
view.nameLabel = value.name
view.iconView.image = value.icon
...
return view
}
}
ドットで補完できるようにする
switch 文の case のように、型が推論できる場合は型名を省略してドットから書き始めることが出来ます。これを利用すると非常にコードが美しくなります。
extension Notification.Name {
static let didChangedTabBar = Notification.Name("didChangedTabBar")
}
NotificationCenter.default
.addObserver(self,
selector: #selector(didChangedTabBar),
name: .didChangedTabBar, // ドットで書き始められる (引数が要求している型が Notification.Name のため)
object: nil)
public extension UserDefaults {
public struct Key {
public let rawValue: String
}
}
public extension UserDefaults {
public func integer(for key: Key) -> Int {
return integer(forKey: key.rawValue)
}
public func set(_ value: Int, for key: Key) {
set(value, forKey: key.rawValue)
}
}
extension UserDefaults.Key {
static let viewCounter = UserDefaults.Key(rawValue: "viewCounter")
}
// より Swifty な UserDefaults に
let count = UserDefaults.standard.integer(for: .viewCounter)
UserDefaults.standard.set(count + 1, for: .viewCounter)
クロージャの即時実行
JavaScript ではおなじみですが、Swift でもクロージャの即時実行ができます。条件によって代入する値を変えたいときなどに利用すると便利です。
let value = {
switch condition {
case "dog": return 🐶
case "cat": return 🐱
default: return 😀
}
}()
Generics の型を型推論できるようにする
Generics の型パラメータの型を引数に取るメソッドを作る際に、デフォルト引数を設定すると戻り値の型で型推論させることもできるようになり、可読性が良くなります。
func value<T>(_ type: T.Type = T.self, forKey: String) -> T { ... }
// 型を記述して利用する
let intValue = value(Int.self, forKey: "age")
// 型推論を利用する
let user = User(name: "Taro", age: value(forKey: "age"))
Embedded Framework
コンポーネントごとに機能をまとめたり、レイヤードアーキテクチャを採用したい場合は Embedded Framework を導入すると実装が強制 (矯正) されて、良いです。
詳しくはこちらのリンクがとても参考になります。
Embedded Framework使いこなし術
Swift Extension
こちらに投稿した Extension 集がおすすめです。
使うと手放せなくなるSwift Extension集
ただ、Swift のExtension は便利ですが、闇雲に増やさないほうが良いです。グローバルなメソッドよりは型に制限がある分まだましですが、利用箇所が限定的であれば、よりスコープを狭めることをおすすめします。
スコープを狭める方法としては private extension と protocol がおすすめです。
// そのファイル内でしか利用しない extension には private をつける
private extension String {
func parseAsTime() -> Time? {
guard self.count == 4,
let hour = Int(self[0...1]),
let minute = Int(self[2...3]) else { return nil }
return .init(hour: hour, minute: minute)
}
}
// protocol を利用すると extension の影響範囲が明確になり、可読性が上がり、間違った利用を減らせる。
// 空の protocol を作る
protocol NibDesignable: AnyObject {} // @IBDesignable の View のみに適用したい機能
// その protocol を拡張して実装する
extension NibDesignable where Self: UIView {
private func loadNib() -> UIView {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: className, bundle: bundle)
return nib.instantiate(withOwner: self).first as! UIView
}
func setUpNib() {
let view = self.loadNib()
addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
leftAnchor.constraint(equalTo: view.leftAnchor),
rightAnchor.constraint(equalTo: view.rightAnchor),
topAnchor.constraint(equalTo: view.topAnchor),
bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}
protocol に stored property を持たせる
本当に必要なときしか利用しないほうが良いです。可読性が著しく落ちる場合があります。
protocol を利用して機能を実装しているときに値を専用のプロパティに保存したいケースがあります。その場合は、Objective-C の機能を利用して実装することが可能です。
protocol ViewCountable: AnyObject {}
private struct AssociatedKeys {
static var viewCounterKey: Void?
}
extension ViewCountable {
func viewCountUp() {
viewCounter += 1
}
private(set) var viewCounter: Int {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.viewCounterKey) as? Int ?? 0
}
set {
objc_setAssociatedObject(self,
&AssociatedKeys.viewCounterKey,
newValue,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
class ViewController: UIViewController, ViewCountable {
func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.viewCountUp() // プロパティを実装していないが状態を更新できる
}
}
Kingfisher などの OSS のコードを参考にしてみると良いと思います。 objc_setAssociatedObject
で検索してみましょう。
ログの計測など、本質的でないロジックを隠したいときに限定的に利用するのがおすすめです。
コンパイラに優しいコードを書く事も必要
Swift のコンパイラは複雑な型推論が必要になるとビルドに時間がかかったり、そもそもビルドできなくなったりする時があります。慣れると「これはコンパイラがかわいそうだな」と思うようになり、適切に型を明示できるようになります(?)。慣れるまではどの実装のビルドに時間がかかっているのかをたまに確認するようにすると良いです。
ビルド時間の計測にはこちらのツールがオススメです。
小さなアプリであればビルド時間の影響は少ないですが、規模が大きくなってくるとかなり辛い問題になります。
こちらのツールで見つかったビルド時間の長い実装に型を明示したり、式を分割するなどしてコンパイラに優しい Swift のコードにしていきましょう。
コード生成
Swift はコード生成関係の言語機能が弱いです。そのため、どうしても冗長なコードを書く必要が出てきてしまいますが、サードパーティのツールを利用するとある程度コード生成を実現できます。
リソース関係の定数の自動生成は下記がオススメです。
自分で自動生成する内容をカスタマイズしたい場合は Sourcery がオススメです。
アナリティクス用のログ送信に必要な定数を生成したり、ユニットテスト用のモッククラスを自動生成したりなど、人類がやるべきでないことをツールに任せることが出来ます。
まとめ
変更箇所に影響がある部分をビルドエラーにできる Swift の良さを活かすためには実装者も Swift に歩み寄る必要があります。
今回紹介したような Tips を利用して、運用中にバグが生まれにくく、新規メンバーもコミットしやすくなるような状態を作っていきたいですね。
以上、Swift Advent Calendar 2018 の 5 日目でした。