SwiftChainingの解説記事その4です。
SwiftChainingとは何か?というのは、こちらの最初の記事を参考にしてください。
今までの記事で、SwiftChaining
で用意しているオブジェクトのchain()
という関数を呼ぶと、そのあとにオブジェクトの値が変更された時などに実行されるバインディング処理を書ける、ということを紹介しました。
今回はそのchain()
を呼んだ後に書く部分について少し詳しく解説しようと思います。
Chainとは
監視対象となるオブジェクトのchain()
関数を呼ぶとChain
という型のインスタンスを返します。Chain
は監視対象のオブジェクトがイベントを送信する時に実行されるバインディング処理を構築するクラスです。
(監視対象となるオブジェクトというのは、SwiftChaining
的にはChainable
というプロトコルに適合したオブジェクトということになるのですが、プロトコルに関しては別の記事で解説しています。)
Chain
の持つ関数を呼ぶとまたその返り値にChain
を返すという感じで、処理をメソッドチェーンでどんどん追加して繋げて書くことができるようになっています。
Chain
の関数はいろいろあるのですが、大きく分けると「バインディング処理を構築する」関数と「バインディング処理を確定する」関数があります。例えば以下のようなものです。
バインディング処理を構築する関数
- do
- map
- guard
- merge
- tuple
- combine
- sendTo
バインディング処理を確定する関数
- end
- sync
メソッドチェーンで処理をいくつもつなげて書いていけるのは「処理を構築する関数」の方です。今回はこの「処理を構築する関数」について解説します。
バインディング処理を構築する関数
do
do
は、イベントを受け取った時に登録したクロージャの処理を単に実行するものです。クロージャの引数には、受け取ったイベントの値が入ってきます。イベントの値はそのまま次の処理に渡されます。
let notifier = Notifier<Int>()
let observer = notifier
.chain()
.do({ (value: Int) in
// 何か処理をする
})
.end()
// "1"が送信されるので、doのクロージャの引数valueに1が渡され実行される
notifier.notify(value: 1)
真面目に書くとこのような感じになるのですが、クロージャの記述は普通に省略して書く事が出来るので、通常は以下のコードくらい省略する方が書きやすいでしょう。
.do { value in
// 何か処理をする
}
ちなみに、これまでのサンプルコードに出てきたsendTo
でオブジェクトに値を反映させるというのも、内部的にはdo
を使って値をセットしているだけです。何かしらイベントが来た時に、最終的にバインドするような処理を書くのは、基本的にdo
を使うということになります。
また、開発中に「この時点ではどんな値が来ているのか」と調べたい時も、do
を挟み込んで確認するという使い方もできると思います。
map
map
は、受け取ったイベントの値を変化させて次の処理に渡すことができます。Arrayのmap
と同じようなものと思っていただいて良いと思います。
例としてInt
をString
に変換する場合は、以下のようになります。
let notifier = Notifier<Int>()
let observer = notifier
.chain()
.map({ (intValue: Int) -> String in
return String(intValue)
})
.do({ (strValue: String) in
// 変換されたStringの値を受け取る
})
.end()
notifier.notify(value: 1)
こちらもクロージャの記述を省略する方が書きやすいでしょう。最大限まで省略すると以下のようになります。
.map { String($0) }
map
もdo
と同じくクロージャを実行するものではあるので、何でも書きたい処理は書けるのですが、map
では単に値を変換をする処理を書くだけに留めておくほうが良いと思います。
guard
guard
は、受け取ったイベントをそこで止めることができます。登録したクロージャでtrue
を返すとそのまま値を次の処理へ渡しますが、false
を返すと処理を打ち切ります。
例として、Int
が奇数の場合だけイベントを通すコードは以下のようになります。
let notifier = Notifier<Int>()
let observer = notifier
.chain()
.guard({ (value: Int) -> Bool in
return value % 2 != 0
})
.do({ value in
// valueが奇数の時だけ実行される
})
.end()
guard
もmap
と同じく関係のない処理を書かずに、単にイベントを通すか通さないかという条件だけを書いたほうが良いと思います。
merge
merge
は、2つの同じ型のChain
の処理の流れを同じ型のまま1つにまとめます。
let notifier1 = Notifier<Int>()
let notifier2 = Notifier<Int>()
let chain1 = notifier1.chain()
let chain2 = notifier2.chain()
let observer = chain1
.merge(chain2)
.do { value in
// 両方の値を受け取る
}
.end()
// どちらから送ってもdoが実行される
notifier1.notify(value: 1)
notifier2.notify(value: 2)
merge
が使えるのは同じ型のChain
同士の場合のみです。型が違う場合は以下に紹介するtuple
やcombine
を使うことになります。
tuple
tuple
は、2つのChain
の処理の流れをひとつのタプルにまとめます。
こちらはmerge
と違い別の型でもまとめることができます。その代わり2つのOptionalの要素を持ったタプルに変換されます。
let chain1 = notifier1.chain()
let chain2 = notifier2.chain()
let observer = chain1
.tuple(chain2)
.do { (value1: Int?, value2: String?) in
// value1かvalue2のどちらかだけ値が入って来る
}.end()
// doが実行されvalue1に値が渡される
notifier1.notify(value: 1)
// doが実行されvalue2に値が渡される
notifier2.notify(value: "2")
tuple
から出力されるタプルは、2つの要素両方に値が入ることはありません、どちらか一方に値があればもう一方は必ずnil
になります。2つの処理の流れが混ざらずに並走しているだけとイメージしてもらうと良いかもしれません。
ただ、2つの要素がバラバラに送られてきてもそのままでは扱いにくいと思うので、実際には次に紹介するcombine
を使うことが多くなるかもしれません。
combine
combine
は、2つのChain
の流れをひとつのタプルにまとめます。tuple
と違うのは、受け取った値を内部で保存しており、必ず2つの値が揃った状態でタプルのイベントが送信されることです。
ただ、タプルの中身はオプショナルではなく、どちらか一方がイベントを一度も受け取っていなければcombine
からイベントが送信されることはありません。
let holder1 = ValueHolder(1)
let holder2 = ValueHolder("2")
let chain1 = holder1.chain()
let chain2 = holder2.chain()
let observer = chain1
.combine(chain2)
.do { (value1: Int, value2: String) in
// 両方の値が揃って実行される
}
.sync()
上記のコード例のように、combine
でまとめる送信元がValueHolder
のように値を常に送信できる種類のものだと、sync()
を呼んだ時点で2つともから1度値が送信されるので、以後どちらかだけが変わったらcombine
の後に値が送信されます。
これがNotifier
のように送信元が値を持たない種類のものだと、敢えて明示的に値を送信しておかないとcombine
での値が揃わないので、注意が必要です。
let notifier1 = Notifier<Int>()
let notifier2 = Notifier<String>()
let observer = notifier1.chain()
.combine(notifier2.chain())
.do { (value1: Int, value2: String) in
// 両方の値が揃ってから実行される
}
.end()
// この時点ではdoは実行されない
notifier1.notify(value: 0)
// この時点でも片方しか値がないのでdoは実行されない
notifier1.notify(value: 1)
// 2つとも値が揃ったのでdoが実行される
notifier2.notify(value: "2")
sendTo
sendTo
は、引数に渡したオブジェクトにイベントの値を送る関数です。
let notifier = Notifier<Int>()
let holder = ValueHolder<Int>(0)
// notifierから値を送信したらholderが値を受け取るようにする
let observer = notifier.chain().sendTo(holder).end()
// notifierから値が送信されholderに1がセットされる
notifier.notify(value: 1)
具体的にどのようなオブジェクトが受け取れるのかというと、SwiftChaining
的にはReceivable
というプロトコルに適合したクラスということになります。プロトコルの具体的な説明はまた別の記事でしますが、今まで出てきたクラスでは以下のものが適合しています。
- ValueHolder
- KVOAdapter
- Notifier
ValueHolder
やKVOAdapter
は値を保持しているものなので、イベントを受け取ったら値をセットします。Notifier
はそれ自身が値を保持するものではないので、受け取った値をそのまま送信する感じの動作になります。
また、前回の記事にも書きましたが、sendTo
で送るイベントの型と受け取るオブジェクトの型がオプショナルの有無の違いだけであれば、map
などで変換しなくてもそのままつなげることができます。