FURYU Tech Blog - フリュー株式会社

フリュー株式会社の開発者が技術情報を発信するブログです。

Reactive Swift に Completable 的なものを導入した話

こんにちは。

ピクトリンク事業部の足立です。

ピクトリンクはプリントシール機で撮影した画像データを使ったコミュニケーションツールで、私はそのiOS版のアプリの開発を担当をしてます。

最近は、12/10に遅ればせながら、スマブラSPを買いました👋

一応ほぼ全作やっているので、使命感に駆られるように、大乱闘を繰り広げる毎日でちょっと寝不足気味です😪

アドベンチャーモード、ふつうレベルでも意外に難しく苦戦しています。楽しいです✨

では、本題に入りたいと思います。

この記事は フリュー Advent Calendar 2018 の 12/17(月) の記事になります。

今回はピクトリンクアプリの開発で導入している Reactive Swift をプロジェクト内でカスタマイズしたお話です。

きっかけ

ある日のPRレビューを実施していた時のことです。

足立

  「ここ SignalProducer<Void, HogeError> なんですけど、呼び出し元が observer.sendCompleted() しかしていないので then(_:) で繋げないと動かないと思いますよ」

メンバーM

  「え、そうなんですか? I/Fの定義だけだとわかりにくいし、なんとかならないんですかね」

的な会話が繰り広げられていました。

たしかに、SignalProducer<Void, HogeError> だけを見ると completed イベントしか通知されるとは思いませんよね。

実装をするたびに、関数の中を調べるなんて時間の無駄ですし、そんなわかりにくい設計は無くすべきです。

ということで Completable 的なものを導入する

「じゃあ、どんな感じにしようか?」

と、設計しているときに「完了のみの通知だし Completed? Completable? 的な感じですかねー」と進んでいき RxSwift 的には似たような機能はないのか?

と調べ始めました。

ありました。RxSwift / Completable

Completable についての詳しい説明はここではしませんが、 Completable そのものを返却できるような設計のようです。

Reactive Swift でそこまでのカスタマイズをするとなると、とてもじゃあないですが大変です💦

そこで、弊社iOSアプリ開発チームでは 専用のオブジェクトを通知することで Completable のような扱いにする としました。

やったこと

  • 通知に使う専用のオブジェクトを作成
  • Completable の時に使わないメソッドの非推奨化

通知に使う専用のオブジェクトを作成

こんな感じで Completable を定義しました。

/// `SignalProducer`で`sendCompleted`のみで通知されることを明示的に宣言させるための定義。
struct Completable {
    private init() {}
}

`Completable` の時に使わないメソッドの非推奨化

通知されるイベントが completed のみになるので、間違った実装にならないために、 SignalProducerSignal などで定義されている幾つかのメソッドを利用させないようにします。

ReactiveSwift / UninhabitedTypeGuards.swift を参考にしました。

主に startWith*flatMap など value** イベントの通知で使用するメソッド周りです。

以下は SignalProducer 周りの定義を抜粋しています。

// MARK: - SignalProducer Observation

extension SignalProducer where Value == Completable {
    @discardableResult
    @available(*, deprecated, message: "Can not used when `Value` is `Completable`.")
    func startWithResult(_: @escaping (Result&lt;Value, Error>) -> Void) -> Disposable { fatalError() }

    @discardableResult
    @available(*, deprecated, message: "Can not used when `Value` is `Completable`.")
    func startWithValues(_: @escaping (Value) -> Void) -> Disposable { fatalError() }
}

extension SignalProducer where Value == Completable, Error == NoError {
    @discardableResult
    @available(*, deprecated, message: "Can not used when `Value` is `Completable`.")
    func startWithResult(_: @escaping (Result&lt;Value, Error>) -> Void) -> Disposable { fatalError() }
}

// MARK: - SignalProducer flatMap

extension SignalProducer where Value == Completable {
    @discardableResult
    @available(*, deprecated, message: "Can not used when `Value` is `Completable`.")
    func flatMap&lt;Inner: SignalProducerConvertible>(_: FlattenStrategy, _: @escaping (Value) -> Inner) -> SignalProducer&lt;Inner.Value, Error> where Inner.Error == Error { fatalError() }

    @discardableResult
    @available(*, deprecated, message: "Can not used when `Value` is `Completable`.")
    func flatMap&lt;Inner: SignalProducerConvertible>(_: FlattenStrategy, _: @escaping (Value) -> Inner) -> SignalProducer&lt;Inner.Value, Error> where Inner.Error == NoError { fatalError() }
}

extension SignalProducer where Value == Completable, Error == NoError {
    @discardableResult
    @available(*, deprecated, message: "Can not used when `Value` is `Completable`.")
    func flatMap&lt;Inner: SignalProducerConvertible>(_: FlattenStrategy, _: @escaping (Value) -> Inner) -> SignalProducer&lt;Inner.Value, Inner.Error> { fatalError() }

    @discardableResult
    @available(*, deprecated, message: "Can not used when `Value` is `Completable`.")
    func flatMap&lt;Inner: SignalProducerConvertible>(_: FlattenStrategy, _: @escaping (Value) -> Inner) -> SignalProducer&lt;Inner.Value, Inner.Error> where Inner.Error == Error { fatalError() }
}


// MARK: - Signal.Observer send(value:)

extension Signal.Observer where Value == Completable {
    @available(*, deprecated, message: "Can not used when `Value` is `Completable`.")
    func send(value _: Value) { fatalError() }
}

使い方

基本的には通常の Reactive Swift の実装通りです。

Value の指定を Completable にするだけです。

func hoge() -> SignalProducer&lt;Completable, HogeError> {
  return SignalProducer { observer, disposable in
    // 何かしらの処理...
    
    // Send completed.
    observer.sendCompleted()
  }
}

// 使う時
hoge()
  .startWithCompleted {
    print("Completed.")
  }

まとめ

導入した結果としては、とても良かったと思います。

きっかけ でも触れましたが、PRレビューの際の

  • completed イベントのみしかこない実装ではないか?
  • value イベントが通知される前提の実装になっていないか?

などの、メソッドの定義をだけでなく実装の中身まで遡って見る必要がなくなったと思います。

  = PRレビューでの不要な指摘も減る(する側もされる側もハッピー😉)
 

実装するときも CompletableTypeGuards.swift の設定のおかげで間違った実装を防げるのも地味に良かったりします。

慣れていけば 使えない ことはわかっていきますが、プロジェクトにJOINしてきた人たちにとってはWarning表示という目に見えるカタチで知らせてくれるのも良い感じです。

と、ここまで良かった点を書いてきましたが、念のために。

Completable の使用にあたっては用法/用量をまもって欲しいと思います。

便利だからといって 「本当に Completable の通知で良いのか?」 という「その機能の責務」を考えずに実装を進めることは、その時は良くても数週間後、数ヶ月後にタイヘンなことになるかも・・・