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などで変換しなくてもそのままつなげることができます。