[Swift][Combine] カスタム Operator の作り方

     
Combine で、カスタム operator を作ってみます。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Ventura 13.1 Beta 2
  • Xcode 14.1
  • iOS 16.0

Combine

Apple が iOS13/macOS 10.15 Catalina から導入したフレームワークです。

非同期イベントを処理することが declarative(宣言的)に記述することができます。

以前のざっくり紹介記事は、以下です。
[Combine] 難しそうで難しくない少し難しいCombine

そのほかにも、いろいろな視点から説明してます。 Combine で検索➡️ combine

Operator

Operator を使うと、Publisher からのデータをさまざまに、処理することができます。

この Operator があることで、非同期イベントの処理も declarative に記述することができ、見通しよく 処理を記述することができるようになります。

すでにさまざまな Operator が用意されていますが、いろいろな処理を記述していこうとすると、Oprator を複雑に組み合わせたくなってきます。
このような時に、複雑に組み合わせた Operator をまとめた Operator を定義することで、コードの見通しをよくすることができます。

また、ケースによっては、既存の Operator を組み合わせるだけでは 欲しい機能が実現できないこともあります。
そのような時には、自分で Operator を作りたくなることもあります。

この記事では、そのような時に、 どのように 自分の欲しい Operator を作るかを解説していきます。

何もしない Operator

渡されたものをそのまま publish する operator を作ってみます。

custome operator を作るためには、Publisher の extension として作成します。

返す型は、後続の operator を考慮して、AnyPublisher 型として定義します。
実際に返すときにも、eraseToAnyPublisher を使用して、型を消去します。

extension Publisher {
    func asIs() -> AnyPublisher {
        self.eraseToAnyPublisher()
    }
}

何も操作しないので、Output や Failure に前提条件は必要ありません。

使ってみると以下のようになります。

let cancellable = [1,2,3,4,5].publisher
    .asIs()
    .sink { value in
        print(value)
    }
// output
1
2
3
4
5

データを操作する Operator

次に、流れてくるデータに変更を加える Operator を作ってみます。

データを2倍にする Operator

例えばということで、データを2倍にする Operator です。

2倍にできるためには、入力されてくるデータが計算可能であること(実際には、2倍にできること)が必要です。

ここでは、Int が入力であるという制約を加えて作ってみます。

extension Publisher where Output == Int {
    func double() -> AnyPublisher {
        self.map({$0 * 2}).eraseToAnyPublisher()
    }
}

実行してみると以下のようになります。

let cancellable = [1,2,3,4,5].publisher
    .double()
    .sink { value in
        print(value)
    }
// print-out
2
4
6
8
10

条件付きでデータを操作する Operator

流れてきた値に応じて処理を変えることも簡単です。

条件付きで倍にする Operator

例えば、偶数であった時のみ 倍にする operator を作ってみます。

流れてきた値によって処理を切り替えることで実現できます。

extension Publisher where Output == Int {
    func doubleEven() -> AnyPublisher {
        self.map({ value in
            if value % 2 == 0 {
                return value * 2
            }
            return value
        }).eraseToAnyPublisher()
    }
}

使ってみると以下のようになります。


let cancellable = [1,2,3,4,5].publisher
    .doubleEven()
    .sink { value in
        print(value)
    }
// print-out
1
4
3
8
5

データを追加/削除する Operator

ここまでは、流れてきたデータに手を加えていましたが、データを追加したり、削除したりすることを考えてみます。

データを削除する Operator

上位から何が流れてきても何も流さない Operator を作ってみます。

何も流さない Publisher は、用意されています。Empty() です。
ですので、Empty に置き換えてしまうと、下流には何も流れなくなります。

extension Publisher {
    func vanish() -> AnyPublisher {
        Empty(outputType: Self.self.Output, failureType: Self.self.Failure)
            .eraseToAnyPublisher()
    }
}

使ってみると以下のようになります。当然、何も表示されません。

let cancellable = [1,2,3,4,5].publisher
    .vanish()
    .sink { value in
        print(value)
    }
// print-out
// なし

データを追加する Operator

データが流れててきた時に、流すデータを増加させる Operator を作ってみます。

今度は、数字を最後に追加する Operator です。

追加する数字は、15 を使用しています。

extension Publisher where Output == Int {
    func appendFavorite() -> AnyPublisher {
        self.append(15)
            .eraseToAnyPublisher()
    }

使ってみると以下のようになります。最後に 数字(15) が追加されていることがわかります。

let cancellable = [1,2,3,4,5].publisher
    . appendFavorite()
    .sink { value in
        print(value)
    }
// print-out
1
2
3
4
5
15

append は、最後に追加する Operator ですが、最初に追加するものとして、prepend というものもあります。

extension Publisher where Output == Int {
    func prependFavorite() -> AnyPublisher {
        self.prepend(15)
            .eraseToAnyPublisher()
    }
}
let cancellable = [1,2,3,4,5].publisher
    . prependFavorite()
    .sink { value in
        print(value)
    }
// print-out
15
1
2
3
4
5

新しい Operator を作る

ここまでは、既存の Operator を組み合わせて使ってきましたが、以下では 新しい Operator を Scratch から作ってみます。

新しい Operator を作る準備

Operator は、Publisher でありSubscriber でもあります。

新しい Operator を作るということは、この2つの要素を実装していくことになります。

・Publisher は、Subscriber を管理し、データを publish します
・Subscriber は、Publisher へ登録し、publish されるデータを受け取ります

Subscriber であるということ

Subscriber は、Protocol として定義されています。

Apple のドキュメントは、こちら

Subscriber の LifeCycle は、おおよそ以下の通りです。

Subscriber は、Publisher の subscribe() を呼び出して subscribe 登録します(後述)

Publisher が Subscriber の subscribe を確認すると、receive(subscription:) が呼び出されます。このとき、Publisher から Subscription が渡されてきます。

その後、Pubisher が値を publish すると Subscriber の receive(Self.Input) / receive() が呼び出されます。

Publisher が publish を終了すると、receive(completion:) が呼び出されます。

Publisher であるということ

Publisher も、Protocol として定義されています。

Apple のドキュメントは、こちら

Publisher の LifeCycle は、おおよそ以下の通りです。

Publisher は、Subscriber から subscribe(:) を呼び出され登録リクエストを受けます。登録した後、Subscriber の receive(subscription:) を呼び出し、subscription を渡します。

その後、値を publish するときには、登録されている Subscriber の receive を呼び出します。
終了する時には、登録されている Subscriber の receive(completion:) を呼び出して終了します。

Operator は、Publisher でありかつ Subscriber でもあるので、上記の両方を 実装していくことになります。

Subscription

Subscription は、Subscriber が Publisher に subscribe リクエストを行い、その後 呼び出される receive(subscription:) で渡されるものです。
Subscriber は、この Subscription 経由で要求やキャンセルを行うことが可能となります。

今回の実装では、Publisher から渡されるものを Subscriber にも渡すことにして、実装を省略しています。
キャンセル時に特定の処理を行う等が必要であれば、自分で実装することが必要となります。

前回値も流してくれる Operator

作る Operator は、以下のような感じです。

・Publisher から新しい値が流れた時は、(nil, 値) として流す
・それ以降で Publisher から新しい値が流れると (前回値,今回値) として流してくれる

イメージとしては、Publisher から新しい値が流れてくる時に、前回値があればその値との Tuple で流してくれる Operator です。

Publisher 部分

まずは、Publisher 部分を書きます。

(前回値、今回値)という Tuple でデータを渡すので、Output は Optional 要素を持つ Tuple になります。

struct Pair: Publisher {
    typealias Output = (P.Output?, P.Output?) // (1) 
    typealias Failure = P.Failure             // (2)

    let upstream: P                           // (3)

    init(upstream: P) {
        self.upstream = upstream
    }

    func receive(subscriber: S) where S : Subscriber, S.Input == Output, S.Failure == Failure { // (4)
        self.upstream.subscribe(PairInternal(downstream: subscriber))  // (5)
    }
    // inner class will come
}
コード解説
  1. Output は、Publisher の Output を Optional にして2つもつ Tuple になります
  2. Failure は、Publisher の Failure をそのまま使います
  3. 自分が subscribe する Publisher を記憶しておきます
  4. 自分のことを subscribe できる条件を設定します
  5. subscribe リクエストが来たら、上位の Publisher に同様に subscribe リクエストします

Subscriber 部分

内部クラスを作成して、Subscriber とします。

struct Pair: Publisher {
    typealias Output = (Upstream.Output?, Upstream.Output?)
    typealias Failure = Upstream.Failure

    let upstream: Upstream

    // ..ommit.. 
    class PairInternal: Subscriber where S: Subscriber, S.Input == Output, S.Failure == S.Failure { // (1)

        let downstream: S                 // (2)
        var prevValue: P.Output? = nil    // (3)

        init(downstream: S) {
            self.downstream = downstream
        }

        func receive(subscription: Subscription) { // (4)
            downstream.receive(subscription: subscription)
        }
        func receive(_ input: P.Output) -> Subscribers.Demand { // (5)
            defer { prevValue = input } // (6)
            return downstream.receive((prevValue,input))
        }

        func receive(completion: Subscribers.Completion) { // (7)
            _ = downstream.receive((prevValue, nil))  // (8)
            downstream.receive(completion: completion)
        }
    }
}
コード解説
  1. Subscriber の制約を記述します
  2. 自分を subscribe している Subscriber を記録します
  3. 前回値として後からも使用するので記憶しておきます。初期値は nil とします
  4. 自分のPublisher から subscribe 通知を受けたら、自分の subscriber にも通知します
  5. Publisher からデータが来たら、自分の subscriber にもデータを渡します
  6. 今回の値は、prevValueに記録し、前回値と今回値の tuple をsubscriber に渡します
  7. Publisher から completion 通知が来たら、(自分の) Subscriber に通知します
  8. Publisher から completion 通知が来た時、通知前に 記録されている prevValue と nil との tuple を subscriber に渡します

completion の時に、残っている 前回値を nil と一緒に publish していますが、この publish がないと、
[0,1,2] というデータに対して、(nil,0), (0,1), (1,2) というデータが生成され、(2,nil) が生成されません。

ケースによっては、不要かもしれませんが、自分の想定している使い方では必要なので、入れています。

まとめ

既存の Operator を組み合わせて作る Operator と、自分で Scratch から実装する Operator の作り方を説明しました。

XXXX
  • 既存の Operator を組み合わせて 独自の Operator を作るのは簡単
  • 独自 Operator を実装する時は、Publisher protocol と Subscriber protocol を実装する

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

SwiftUI ViewMatery

SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。

英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。

View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。

超便利です

SwiftUIViewsMastery

販売元のページは、こちらです。

SwiftUI 徹底入門

# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。

Swift学習におすすめの本

詳解Swift

Swift の学習には、詳解 Swift という書籍が、おすすめです。

著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。

最新版を購入するのがおすすめです。

現時点では、上記の Swift 5 に対応した第5版が最新版です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です