SwiftのExtensionをモダンでオシャレに実装する

  • 116
    いいね
  • 0
    コメント

Extensionをモダンでオシャレにしたい

この記事の結論を先に書いておくと、

UIColor.yamabuki

のように書いているExtensionを以下のようにオシャレに書くことができる、ということを解説します。

UIColor.ex.yamabuki

長くなってんじゃねーか。

と、思うかもしれませんが、こうすることによって型に含まれる標準的な機能と拡張機能を明確に区別することができます。

ちょっと便利。ちょっと。

Extensionをどうやって定義するか問題

なぜこのような話をするのかというと、Extensionをどうやって定義するかが問題になったりするからです。小さな問題ではあるんですけど。

Extensionは欲しいところに欲しいものを作ることができるので、結構使う機会が多いんじゃないかと思いますが、どこにどうやって定義するかまで考えたいところです。

局所的に使うものであればprivateなりfileprivateなりのアクセスコントロールを使って公開範囲を制限したりするかと思います。

逆にプロジェクト全体で使うようなものに関しては、publicにしてClass+Extension.swiftみたいなファイルを作成して定義するかと思います。

これらの方法でも問題ないんですが冒頭に述べたとおり、

UIColor.yamabuki

のような書き方だとデフォルトで定義されているものなのか、拡張定義されているものなのか分かりません。

Extensionであることを明確にする

冒頭で書いたような、

UIColor.ex.yamabuki

みたいな定義をRxSwiftが行っています。

RxのようなReactive Programmingを行うための拡張の場合、このように明示的に書くことは有効なのではないかと思います。

これから実装方法の解説のためにRxSwiftのコードが出てきますが、本題とはあんまり関係ないのであまり深く考えないでサラッと読み流してください(RxSwiftやRxCocoaの違いなどの説明は省略します)。

コードの解説

RxSwiftの2系では、ボタンがタップされたときにtapと出力するには以下のようなコードを書いていました。

Swift2
button.rx_tap
  .subscribeNext { _ in
    print("tap")
  }
  .addDisposable(disposeBag)

これがRxSwift3系では以下のように変更されています。

Swift3
button.rx.tap
  .bindNext { _ in
    print("tap")
  }
  .addDisposable(disposeBag)

subscribeNextbindNextに変わっていますがほぼ同じことをしているので、そこは一旦置いておきます。

button.rx_tapbutton.rx.tap と変わっていることに着目してみましょう。

Rxでの内部実装

2系ではこのように実装されていました

extension UIButton {

    /**
    Reactive wrapper for `TouchUpInside` control event.
    */
    public var rx_tap: ControlEvent<Void> {
        return rx_controlEvent(.TouchUpInside)
    }
}

UIButtonに限らず、Rx拡張は単純にrx_*というプリフィックスをつけてExtensionを定義していました(全部に目は通していませんがたぶんそうです)。

なんかオシャレじゃない感じ。

それが、3系では以下のようにジェネリクスを使ってモダンでオシャレな実装に変更されました。

extension Reactive where Base : UIButton {

    /**
    Reactive wrapper for `TouchUpInside` control event.
    */
    public var tap: RxCocoa.ControlEvent<Swift.Void> { get }
}

以上を踏まえてもう一度見てみましょう。

Swift2
button.rx_tap
  .subscribeNext { _ in
    print("tap")
  }
  .addDisposable(disposeBag)
Swift3
button.rx.tap
  .bindNext { _ in
    print("tap")
  }
  .addDisposable(disposeBag)

このあたりの変更内容については https://github.com/ReactiveX/RxSwift/blob/db75d01f3179eb6e3e534ed29815ac7920b5674b/CHANGELOG.md#features で解説されています。

Swiftのcollectionで定義されているlazyにインスパイアされて作られたようです。

実装方法

exというプロパティを経由してExtension定義したものにアクセスしたい場合は以下のようなStructとProtocolを定義します。

struct Extension<Base> {
    let base: Base
    init (_ base: Base) {
        self.base = base
    }
}

protocol ExtensionCompatible {
    associatedtype Compatible
    static var ex: Extension<Compatible>.Type { get }
    var ex: Extension<Compatible> { get }
}

extension ExtensionCompatible {
    static var ex: Extension<Self>.Type {
        return Extension<Self>.self
    }

    var ex: Extension<Self> {
        return Extension(self)
    }
}

Extensionであることを分かりやすくするために、Extension<Base>としてみました。

このExtension<Base>ExtensionCompatibleに対応した型をラップ、そしてExtensionCompatibleのデフォルト実装で.exというプロパティを定義しています。

今回はExtension<Base>でしたが、RxSwiftはReactive<Base>としています。必要であれば用途ごとにラッパーを作ってみるのも良さそうです。

準備ができたので冒頭で見たコードを実装しましょう。

UIColor.ex.yamabuki

これは以下のように実装します。

extension UIColor: ExtensionCompatible {}

extension Extension where Base: UIColor {
    static var yamabuki: UIColor {
        return UIColor(red:0.97, green:0.71, blue:0.00, alpha:1.0)
    }
}

これでOK。Extension<Base>UIColorだった場合のデフォルト実装として定義しています。

UIColorに対する拡張ではなく、Extension<Base>の制約付きの拡張ですね。

まとめ

通常のExtensionでここまでするかというと、多分しないと思うのですが、RxSwiftみたいに通常のプロパティとObservableなプロパティを分けたいような場合には定義がやりやすくなる気がします。

今回取り上げたような単純なExtensionの定義でも明確に分けたい場合は結構役に立つのではないかと思います。

この投稿は Swift Advent Calendar 201612日目の記事です。