ひとしれずひっそり

主にソフトに関することをメモしていきます。過程をそのまま書いていたりするので間違いが含まれます。鵜呑みしない様に。

Combine (3)

次はこちら

developer.apple.com

Combine framework

  • Combine frameworkはEventを宣言的なアプローチ(a declarative approach)で提供できる。
  • Delegateを使った複数のCallbackやHandlerを使用したClosure処理よりも、イベントを一つの処理chainとして作ることができ可能性(有用性?)がある。
  • Combine operatorで前の処理から引き継いで個別の処理をしていくことができる。

  • テーブルなどで検索する時に、AppKitでは検索文字を入れるたびに通知が行くがCombineを使用すると最終の検索結果の更新の仕方(内容やタイミング)をコントロールすることができる。

PublisherとSubscriberの接続

  • Publisherは発行する型をOutputで指定する。
  • Subscriberは受取る型をInputで指定する。
  • どちらも扱うErrorをFailureとして指定する。
  • PublisherとSubscriberを接続するにはInputとOutput、それぞれのFailureの型が一致していないといけない。
  • built-in subscribersを用いるとSubscriberのInputとFailuerを自動的にPublisherに一致させる。
  • sink()、assign()はSubscriberを生成する(built-in subscribersということか?)。

sink(receiveCompletion:receiveValue:)

  • sink()は2つのClosureを持つ
    • 一つはPublisherが終了したり異常で終了した際に Subscribers.Completion が発行された時の処理を入れる。
    • もう一つはPublisherから値を受取った時の処理を入れる。

assign(to:on:)

assign()はPublisherから受取った値を直ぐに、指定されたObjectのkey pathで指定されたPropertyに割当てる。

sink()を使った例

let sub = NotificationCenter.default
    .publisher(for: NSControl.textDidChangeNotification, object: filterField)
    .sink(receiveCompletion: { print ($0) },
          receiveValue: { print ($0) })
  • sink()、assign()どちらもPublisherに無制限に直ぐさま発行を要求する。
  • 発行周期をコントロールするには独自のSubscriber protocolを適用したSubscriberを準備する必要がある。

Playgroundで試してみる。ドキュメントのコードははmac os用のコードなのでiOS向けに変えている。

    var sub: AnyCancellable!

        sub = NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: textField)
            .sink(receiveCompletion: { print ($0) },
                  receiveValue: { print ($0) })

LearningCombine.playground · GitHub

Outputの型をOperatorで変える

  • 上の例ではNotificationオブジェクトが渡されてくる。
  • 最終段では文字列として扱う方が便利が良い。
  • Combineでは流れを変えるOperatorとしてmap(:)やflatMap(maxPublishers::)、reduce(::)が用意されている。
  • map() Operatorを使うと違う型を渡すことができる。
let sub = NotificationCenter.default
    .publisher(for: NSControl.textDidChangeNotification, object: filterField)
    .map( { ($0.object as! NSTextField).stringValue } )
    .assign(to: \MyViewModel.filterString, on: myViewModel)

Playgroundで試してみると、なるほどテキストで通知されている。 LearningCombine.playground · GitHub

assignを使うとpropertyに渡すことができる。

Playgroundでassign()でUILabel.textに渡してみる。
TextFieldで変更すると同じテキストがLabelにも反映される。
SwiftUIでやっていることに近いじゃないか。

        sub = NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: textField)
            .map( { ($0.object as! UITextField).text ?? "" } )
            .assign(to: \MyViewController.label.text, on: self)

LearningCombine.playground · GitHub

Operatorを使ったPublisherの拡張

Operatorを使ってPublisherを拡張できる。
3つの方法が挙げられている。

  • 一つはfilter(_:) で不適切な文字を弾いたり文字数制限するなどで使用する。
  • フィルター処理が高くつく(DBが大きいや問い合わせ回数が多いなど)場合にユーザーの入力が終わるのを待ってからEventを発行したい時には、 debounce(for:scheduler:options:)が使える。前回の発行からの次の発行までの最小時間を設定できるので間引くことができる。RunLoopクラスが時間を与える便利な機能を提供している。
  • UIを更新する場合にCallbackをmain threadで処理させたい時にreceive(on:options:) が使える。RunLoop クラスで提供されるSchedulerを最初の引数として渡すとmain loopで受け取ることができる様になる。
        sub = NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: textField)
            .map( { ($0.object as! UITextField).text ?? "" } )
            .filter( { $0!.unicodeScalars.allSatisfy({CharacterSet.alphanumerics.contains($0)}) } )
            .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
            .receive(on: RunLoop.main)
            .assign(to: \MyViewController.label.text, on: self)

filter()で英数字のみ受け付ける様になる。
debounce()で500ms経つとEventを発行する様に遅延できる。
receive()でmain threadで受け取る様にできる。

LearningCombine.playground · GitHub

今まで色々試行錯誤してた事がPublisherで実現できるのではないかという気がしてきた。

Publishingのキャンセル

  • Publisherは正常終了するか異常で終了するまで発行し続ける。
  • SubscriberでSubscribeが不要になったらPublisherに伝えることでキャンセルできる。
  • Cancellable protocolでcancel()メソッドが提供され、Subscriberに適用する。cancel() でキャンセルする。
  • sink()、assign()ではCancellable protocolが適用されている。
  • PublisherはSubscriberが最初にSubscribeした時に Subscription オブジェクトを送る。
  • Custom Subscriberを作る場合はCancellableを適用する必要がある。
  • Custom SubscriberでPublisherから渡されたSubscriptionオブジェクトを保有し、cancel()で保有したSubscriptionにcancel()を呼び出す必要がある。

Combineの理解がだいぶ進んだ。