SwiftChainingの解説記事その6です。
SwiftChainingとは何か?というのは、こちらの最初の記事を参考にしてください。
今までの記事ではSwiftChaining
で用意されたクラスを使用して、バインディングする方法を紹介してきました。
これまでそれら用意されたクラスに関して「監視対象」とか「値を返せる・返せない」といった表現で説明をしていたのですが、実際のところは何のプロトコルに適合しているかという話になります。今回はそのプロトコルについて解説します。
プロトコル
SwiftChaining
でイベントの送受信してバインディングできるようにするために用意しているプロトコルがあります。
- Chainable
- Sendable
- Fetchable
- Syncable (Sendable & Fetchable)
- Receivable
独自に作ったクラスでも、これらのプロトコルに適合させることでSwiftChaining
でバインディング対象として扱うことができます。
Chainable
クラスを監視対象とするための基本となるプロトコルです。定義は以下のようになっています。
public protocol Chainable: class {
// 送信するイベントの型
associatedtype ChainValue
// chain()で返す型(あまり気にしなくて良い)
typealias FirstChain = Chain<ChainValue, Self>
}
extension Chainable {
// バインディング処理の構築を開始する(実装済。そのまま使う)
public func chain() -> FirstChain { ... }
// Observerに保持させるためのオブジェクトを返す(実装済み。そのまま使う)
public func retain() -> Retainer<Self> { ... }
}
Chainable
だけに適合させてもchain()
が呼べてバインディング処理が書けるだけの状態です。イベントを送信できるようにするためには、以下のSendable
やFetchable
に適合させる必要があります。
Sendable
Sendable
はChainable
を継承しており、適合させるとイベントの送信ができます。
定義は以下のような感じです。
public protocol Sendable: Chainable {
}
extension Sendable {
public func broadcast(value: ChainValue) { ... }
}
実装例として、Int
を送ることに限定したNotifier
を書いてみると、このようになります。
class IntNotifier: Sendable {
typealias ChainValue = Int
func notify(value: Int) {
self.broadcast(value: value)
}
}
イベントとして送る値をChainValue
で定義し、イベントを送信するときはbroadcast(value:)
を呼びます。必要な実装はこれだけです。
IntNotifier
は、以下のように使うことができます。
let notifier = IntNotifier()
// chain()を呼べるので、イベントが送信された時に実行される処理を構築できる
let observer = notifier.chain().do { print($0) }.end()
// 値が送信されてdoが実行され、1がログに出力される
notifier.notify(value: 1)
Fetchable
Fetchable
もChainable
を継承しており、適合させるとsync()
が呼ばれた時に値を引き出して送信することができます。
定義は以下のようになっています。
public protocol Fetchable: Chainable {
// 送信する値を返す(要実装)
func fetchedValue() -> ChainValue
// 値が返せるかをBoolで返す(必要に応じて実装)
func canFetch() -> Bool
}
extension Fetchable {
public func canFetch() -> Bool {
// デフォルトではtrueを返す
return true
}
}
fetchedValue()
を必ず実装する必要があります。返り値に返すのはsync()
を呼んだ時に送信される値です。
fetchedValue()
で値を引き出せないことがある場合にcanFetch()
でfalse
を返すとfetchedValue()
は呼ばれません。デフォルトはtrue
なので、必要があれば実装をすると良いでしょう。
こちらも実装例として、Int
を返すIntFetcher
を書いてみるとこのようになります。
class IntFetcher: Fetchable {
typealias ChainValue = Int
var value: Int = 0
func fetchedValue() -> Int {
return self.value
}
}
以下のように使うことができます。
let fetcher = IntFetcher()
// sync()で送信され0がログに出力される
let observer = fetcher.chain().do { print($0) }.sync()
sync()
を呼んだタイミングでfetchedValue()
で返した値がイベントとして送信されます。
ただ、今の所sync()
を呼んだ時にしか動作しないので、Fetchable
だけに適合してもあまり使い所がないと思います。実際にはSendable
と組み合わせて使うことになるでしょう。
Syncable
Syncable
は、Sendable
とFetchable
を組み合わせてtypealias
で定義した型です。
public typealias Syncable = Fetchable & Sendable
Fetchable & Sendable
と直接書いても良いのですが、よく使うので組み合わせたものを作っておいている感じです。
値が変更された時の送信と、監視を開始した時の値の取得を組み合わせていて、値の同期に使えるということでSyncable
という名前にしています。
例として、Int
を保持して監視対象とするクラスを作ると以下のようになります。
class IntHolder: Syncable {
typealias ChainValue = Int
var value: Int = 0 {
didSet {
if self.value != oldValue {
self.broadcast(value: self.value)
}
}
}
func fetchedValue() -> Int {
return self.value
}
}
使い方は以下のようになります。
let holder = IntHolder()
// sync()で送信され0がログに出力される
let observer = holder.chain().do { print($0) }.sync()
// 値が変更されてイベントが送信されたので1が出力される
holder.value = 1
Receivable
Receivable
に適合したクラスは、sendTo()
でイベントを受信することができます。
定義は以下の通りです。
public typealias Receivable = ValueReceivable & ReceiveReferencable
public protocol ValueReceivable: class {
// 受信するイベントの型
associatedtype ReceiveValue
// 受信した時の処理(要実装)
func receive(value: ReceiveValue)
}
// 内部的な処理に使う
public protocol ReceiveReferencable {
associatedtype ReceiveObject: ValueReceivable = Self
func reference() -> Reference<ReceiveObject>
}
受け取るイベントの型をReceiveValue
で定義し、receive(value:)
を実装する必要があります。イベントで送信されてきた値がvalue
に入ってきますので、受け取った時の処理を書いてください。
例として、Syncable
の例のIntHolder
をさらに拡張してみたいと思います。
class IntHolder: Syncable, Receivable {
typealias ChainValue = Int
typealias ReceiveValue = Int
var value: Int {
didSet {
if self.value != oldValue {
self.broadcast(value: self.value)
}
}
}
init(_ initial: Int) {
self.value = initial
}
func fetchedValue() -> Int {
return self.value
}
func receive(value: Int) {
self.value = value
}
}
以下のように使うことができます。
let sender = IntHolder(0)
let receiver = IntHolder(1)
// syncで値が同期され、receiverは0になる
let observer = sender.chain().sendTo(receiver).sync()
// 0がログに出力される
print(receiver.value)
// イベントが送信され、receiverは2になる
sender.value = 2
// 2がログに出力される
print(receiver.value)