はじめに
みなさんはSwift Bondを使ったことがありますか?
私は普段からSwift Bondを使って日々開発してるのですが本当に便利すぎて大好きになりました
そんな私のお気に入りライブラリであるSwift Bondについて
ある程度の知見が溜まったのでSwift Bondの簡単な仕組みと何が良いのか魅力を説明したいと思います
内容について
本記事はSwift Bondの表面上の簡単な仕組みを説明しているだけですが、とても長いです
さくっと実用的なパターンと説明を知りたい方はこちらSwift Bondの魅力 〜実用サンプル編〜
実用的なサンプルコードだけ見たい方はこちら
また、私が使い方を理解する上で一番腑に落ちたという理由で、かなりの独自概念で説明しています
(Streamとか川とか出てきません)
その独自概念ゆえ混乱をきたす方がいるやもしれません
Swift Bondとは
クライアント特有の目まぐるしく変わるデータや状態の変化とView(だけではない)をBindingするライブラリです
Swift Bond
概念と仕組み
Swift BondはFRPの概念を取り込んでいます
サンプルコード
//誕生年
let BirthYear = 1985
//西暦
let year = Observable<Int>(2016)
//現在の年齢
var currentAge = 31
year.observe { currentYear in
currentAge = currentYear - BirthYear
}
print(currentAge) //31
//2017年になると32歳になる
year.value = 2017
print(currentAge) //32
//2020年になると35歳になる
year.value = 2020
print(currentAge) //35
このようにyear(西暦)の変化に追従する形でage(年齢)が自動で更新されています
でも、FRPとか難解なのでSwift Bondを使う上で私は
対象(状態、入力ストリーム、オブジェクトetc..)に契約を締結する
一度、契約締結したらそれ以降、契約の破棄をしない限り対象の値が変化するごとに契約内容が実行される
という概念で使い方を覚えました
(「時間とともに変化する値」=「振る舞い」、振る舞いと振る舞いの関係性の宣言を記述することでプログラミングを行うパラダイムという認識がFRPでよく用いられているかなと思います)
※ 本記事で出てくる__契約__は契約プログラミングとは一切関係ありません
契約方法
私の概念でいう契約を締結しているのが先のサンプルコードの
year.observe { currentYear in
currentAge = currentYear - BirthYear
}
になります
まず、observeメソッドの呼び出し元のyear変数はSwift Bondで契約を締結するために必要なObservableというクラスのインスタンスです
let year = Observable<Int>(2016)
Observableクラスは下記のように実装されています
public final class Observable<Wrapped>: EventProducer<Wrapped> {
/// The encapsulated value.
public var value: Wrapped {
get {
// We've created buffer of size 1 and we own it so it is safe
// to force unwrap both the buffer and the last element.
return replayBuffer!.last!
}
set {
next(newValue)
}
}
/// Creates a observable with the given initial value.
public init(_ value: Wrapped) {
super.init(replayLength: 1)
next(value)
}
}
ObservableクラスはSwift Bondの核となるEventProducerというクラスを継承し、対象となる値を定義したり参照することのできるvalueというプロパティを生やしています
契約締結ができるのはEventProducerクラスを継承したクラスのみであり、value変数の実体はEventProducerクラスが保持しています
サンプルではInt型の2016という値を保持したObservableインスタンスを生成しています
そして契約締結をするobserveメソッドを呼び出しています
契約締結 observeメソッド
public func observe(observer: Self.EventType -> Void) -> DisposableType
契約締結をするobserveメソッドはObservableのスーパークラスであるEventProducerクラスのメソッドで、クロージャを引数にとりDisposableTypeを返します
/// Registers the given observer and returns a disposable that can cancel observing.
public func observe(observer: Event -> Void) -> DisposableType {
if lifecycle == .Managed {
selfReference?.retain()
}
let eventProducerBaseDisposable = addObserver(observer)
replayBuffer?.replayTo(observer)
let observerDisposable = BlockDisposable { [weak self] in
eventProducerBaseDisposable.dispose()
if let unwrappedSelf = self {
if unwrappedSelf.observers.count == 0 {
unwrappedSelf.selfReference?.release()
}
}
}
deinitDisposable += observerDisposable
return observerDisposable
}
これだけ見ても意味が分からないと思うのでobserveメソッドで何をしているのかを簡単に説明すると、
引数のクロージャ(observer)をEventProducerクラスのobserversという辞書(Dictionary)プロパティに追加しています
observeメソッドの引数に指定したクロージャが契約内容であり、契約内容をobserversというプロパティに溜めているのです
またobserveメソッドの返り値であるDisposableTypeを生成します
DisposableTypeとは
DisposableType自体は下記のように宣言されたプロトコルです
public protocol DisposableType {
/// Disposes or cancels a connection or a task.
func dispose()
/// Returns `true` is already disposed.
var isDisposed: Bool { get }
}
で、これが何なのかというとDisposableTypeは締結した契約を解除することのできる契約解除権です
この契約解除権の種類は幾つかありますが、すべてDisposableTypeに準拠しているのでdisposeメソッドとisDisposedプロパティは必ず存在します
isDisposedプロパティは、その契約が解除されているか?を知ることができます
そして、disposeメソッドを呼び出すと契約が解除されます
先ほどのサンプルコードでdisposeメソッドを実行した場合の挙動です
//誕生年
let BirthYear = 1985
//現在年
let year = Observable<Int>(2016)
//現在の年齢
var age = 31
let disposable = year.observe { currentYear in
age = currentYear - BirthYear
}
print(age) //31
//2017年になると
year.value = 2017
print(age) //32
disposable.dispose()
//2020年になると
year.value = 2020
print(age) //32
year.next(3000)
print(age) //32
observeメソッドの返り値であるDisposableTypeインスタンスを保持してyear変数の値を2020に変更する前にdisposeメソッドを実行しています
すると、その時点で契約が解除され、その後yearの値を2020に変更してもageの値が更新されなくなりました
Observableインスタンスが保持する値の変更 nextメソッド
さて、yearの値の変更といっていますが変更は既にサンプルコードを見てお気づきかと思いますが
year.value = 2017
で実行しています
Observableクラスを見るとvalueプロパティの変更は
public var value: Wrapped {
get {
// We've created buffer of size 1 and we own it so it is safe
// to force unwrap both the buffer and the last element.
return replayBuffer!.last!
}
set {
next(newValue)
}
}
となっておりyear.value = 2017とするとEventProducerクラスのnextメソッドを呼び出しています
nextメソッドが何をしているかというと、対象の値の変更と、先のobservableメソッドで説明した
observersプロパティの中に格納されている全クロージャ(契約内容)を変更後の値を引数にして実行します
//observersは全クロージャ(契約内容)が格納されているDictionaryのプロパティ
//値を変更するごとに下記の処理が実行される
for (_, send) in observers {
send(event) //eventは新しく書き換わった値
}
なので先ほどのサンプルコードだと
year.value = 2017
year.value = 2020
と値が変化するごとにobserveメソッドのクロージャで宣言した契約内容である
{ currentYear in
age = currentYear - BirthYear
}
が毎回実行されていたのです
これがSwift BondのObserverパターンである、値が変化するごとに契約内容が実行される を実現している仕組みです
もちろん1つの対象に複数の契約を締結することも可能です
let num = Observable<Int>(0)
num.observe { (num) -> Void in
print("契約1")
}
num.observe { (num) -> Void in
print("契約2")
}
num.next(1)
//契約1 が出力される
//契約2 が出力される
num.next(2)
//契約1 が出力される
//契約2 が出力される
observeメソッドの引数に渡したクロージャが契約内容であり、observeメソッドを呼び出すと契約内容が呼び出し側のobserversプロパティ(Dictionary)に溜められる(契約締結)
以降、呼び出し側の保持する値が変化するごとに契約内容が実行される
この概念はSwift Bondの仕組みを理解する上で重要です
なお契約締結であるobserveメソッドは、契約締結した瞬間に契約内容が実行されます
先ほどのサンプルコードを
var currentAge = 0
に変更するとobserveメソッドを呼んだ直後にcurrentAgeが変わっていることが分かります
//誕生年
let BirthYear = 1985
//西暦
let year = Observable<Int>(2016)
//現在の年齢
var currentAge = 0
year.observe { currentYear in
currentAge = currentYear - BirthYear
}
print(currentAge) //31
様々なBind(契約締結)方法
さて、Swift Bondのざっくりした仕組みを説明したので次はSwift BondのBind方法を幾つか紹介しようと思います
単方向Binding (片依存)
単方向BindingはbindToメソッドを使います
public func bindTo<B: BindableType where B.Element == EventType>(bindable: B) -> DisposableType
bindToメソッドは
私(呼び出し側)の保持している値が変化するごとに、あなた(引数)の保持している値も私の値で更新します
という契約です
単方向Binding物語
あるところにAさんという男性がいます
Aさんはとてつもなくオラオラな男です
そりゃもう酷い男です
チャラ男です
そんなAさんは、彼女であるBさんに対し「俺の趣味がお前の趣味だ!」と訳のわからないことを言い放ち契約させました
そんな契約を締結してしまった両者の関係を下記に示します
//Aさんの趣味
let hobbyA = Observable<String>("サッカー観戦")
//Bさんの趣味(この時点では趣味がない)
let hobbyB = Observable<String>("")
//オラオラAさんの趣味を彼女であるBさんの趣味にするよう契約締結
let disposable = hobbyA.bindTo(hobbyB)
print(hobbyB.value) //サッカー観戦
hobbyA.next("野球観戦")
print(hobbyB.value) //野球観戦
bindToメソッドを使う事でAさんが趣味を変更するとBさんの趣味も変更されていることが確認できます
ある日、ふと我に返ったBさん
私の本当の趣味は茶道なんだと思い出し自身の趣味を変更しました
するとどうなるでしょうか
//我に返ったBさんが趣味を茶道に変更
hobbyB.next("茶道")
print(hobbyB.value) //茶道
print(hobbyA.value) //野球観戦
Bさんの趣味は茶道に変更されましたが、Aさんの趣味は野球観戦のまま
AさんはBさんが趣味を変更しようと知ったこっちゃないのです
全くもって、わがままな男です
再びAさんが趣味を変更するとBさんの趣味も変わってしまいます
・・なんて切ない物語
bindToメソッドの解説
先ほどのAさんの身勝手な契約は
hobbyA.bindTo(hobbyB)
で締結されています
bindToメソッドの引数に[Bさんの趣味]を渡しています
bindToメソッドの中身を覗いてみると
public func bindTo<B: BindableType where B.Element == EventType>(bindable: B) -> DisposableType {
let disposable = SerialDisposable(otherDisposable: nil)
let sink = bindable.sink(disposable)
disposable.otherDisposable = observe { value in
sink(value)
}
return disposable
}
このようになっており、bindToの中で先ほど説明したobserveメソッドを呼んでいるのが分かります
ここでAさんの趣味が変化したらobserveメソッドの引数で指定したクロージャ(契約内容)が毎度実行されることを実現しています
値の変化と共に実行される契約内容は
//valueにはAさんの趣味が入ります
observe { value in
sink(value)
}
となっており、このsinkというのはbindToメソッドのobserve呼び出しの1行上で実行している
let sink = bindable.sink(disposable)
bindable.sink(disposable)の返り値です
bindable.sink(disposable)のsinkメソッドは下記のように実装されています
public func sink(disconnectDisposable: DisposableType?) -> Event -> Void {
if let disconnectDisposable = disconnectDisposable {
deinitDisposable += disconnectDisposable
}
return { [weak self] value in
self?.next(value)
}
}
ここではDisposableType(契約解除権)を追加したりしていますが、注目すべきは返り値であるクロージャの中身です
self?.next(value)
と記載されています
nextメソッドは対象の値を引数の値で変更するメソッドです
なので、このクロージャは渡された値で自身の値を変更することを表しています
bindable.sink(disposable)のbindableはbindToの引数で指定した[Bさんの趣味]のことです
なので、bindable.sink(disposable)メソッドの返り値のクロージャで実行されているself?.next(value)のselfは[Bさんの趣味]のことなのです
bindToメソッドの
disposable.otherDisposable = observe { value in
sink(value)
}
でsinkクロージャに渡しているvalueはAさんの趣味
これがAさんの趣味が変わるとBさんの趣味も変わるというbindToメソッドのざっくりな仕組みです
(Aさんの趣味を変更→observeの引数であるクロージャ(Bさんの趣味を変更する)が実行)
また、bindToメソッドの返り値として契約解除権が発行されているので
let disposable = hobbyA.bindTo(hobbyB)
disposable.dispose()
を実行することで、この悲しみを終わらせることができます
ちなみに、AさんとBさんの契約が締結している状態で、実はBさんにはCさんというヒモ男がいて、Cさんに対して「私の趣味が、あなたの趣味よ!」という契約をした場合どうなるでしょうか
print(hobbyB.value) //茶道
print(hobbyA.value) //野球観戦
let hobbyC = Observable<String>("")
hobbyB.bindTo(hobbyC)
print(hobbyC.value) //茶道
hobbyA.next("読書")
print(hobbyB.value) //読書
print(hobbyC.value) //読書
Bさんが趣味を変更すれば、当然Cさんの趣味も更新されます
注目すべきはAさんの趣味が変化した時にBさんの趣味が変化する影響でCさんの趣味も変化することです
これすなわち、CさんはAさんのヒモであるということが証明されます
分かりづらい例えで、すみません・・
単方向Bindingの実用的な例としてはサーバと通信して取得したEntityObjectの値を表示するだけのUILabelとかと結びつけるパターンが一般的だと思います
EntityObjectの値が変化するごとにUILabelの表示も自動で変わります
双方向Binding (共依存)
双方向BindingはbidirectionalBindToメソッドで実現します
public func bidirectionalBindTo<B: BindableType where B: EventProducerType, B.EventType == Element, B.Element == EventType>(bindable: B) -> DisposableType
bidirectionalBindToメソッドは
私(呼び出し側)の保持している値が変化するごとに、あなた(引数)の保持している値も私の値で更新します
あなたの保持している値が変化するごとに私の保持している値も、あなたの値で更新します
という契約です
双方向Binding物語
BさんとKさんはバカップルです
バカップルというかお互いが好きすぎて病気の域です
二人の口癖は「あなたが死ぬなら私も死ぬ」です
そして本当に「あなたが死ぬなら私も死ぬ」と契約しました
この契約は下記で実現します
//状態
enum State {
case Living
case Death
}
let stateB = Observable<State?>(.Living)
let stateK = Observable<State?>(nil)
//契約締結
stateB.bidirectionalBindTo(stateK)
stateB.next(.Death)
print(stateK.value!) //Death
stateK.next(.Living)
print(stateB.value!) //Living
このように単方向Bindingとは違いbidirectionalBindToの引数となっているKさんの状態を変更するとBさんの状態も変更されます
まさに一心同体
bidirectionalBindToメソッドの中を覗いてみると
public func bidirectionalBindTo<B: BindableType where B: EventProducerType, B.EventType == Element, B.Element == EventType>(bindable: B) -> DisposableType {
let d1 = bindTo(bindable)
let d2 = bindable.bindTo(self)
return CompositeDisposable([d1, d2])
}
このようになっており、両者に対して単方向BindingであるbindToを実行することで双方向Bindingが実現されています
Bさん.bindTo(Kさん)
Kさん.bindTo(Bさん)
使う上での注意点としてはbidirectionalBindToを呼び出した時の初期値でしょうか
先の例だとBさんの状態は.Living Kさんの状態はnilで宣言されており、Bさん側でbidirectionalBindToを呼び出すと、Kさんの状態がBさんの状態と同じ.Livingになっています
逆にbidirectionalBindToをKさんから呼び出すと、直後Bさんの状態が.Livingからnilになります
let stateB = Observable<State?>(.Living)
let stateK = Observable<State?>(nil)
stateK.bidirectionalBindTo(stateB)
print(stateB.value) // nil
これはbidirectionalBindToの中で
let d1 = bindTo(bindable)
let d2 = bindable.bindTo(self)
をしているので1つ目のbindToで引数側(bindable)の値が書き換えられているので
(bindToの中のobserveメソッドは契約締結と同時に契約内容を実行するため)
2つ目のbindToでは、既にbindableの値が呼び出し元と同じ値で書き換えられた状態で更にself(呼び出し元)の値を書き換えているためです
stateB.bidirectionalBindTo(stateK)をした直後の挙動は
stateBの値でstateKを書き換え、stateKの値でstateBを書き換える
すなわちstateBはstateBの値で再度更新が発生しています
また、bidirectionalBindTo両者の値が同期されるので、bidirectionalBindToの呼び出し元と引数となる値の型はオプショナルも含め厳密に同じ型でないといけません
let stateB = Observable<State>(.Living)
let stateK = Observable<State?>(nil)
stateK.bidirectionalBindTo(stateB) //型が違うので成立しない
実用的な例としては
サーバと通信して取得したEntityObjectの値をUITextFieldの初期値として使い、ユーザーがUITextFieldの値を変更するごとにEntityObjectの値も自動で変わることを実現する時に使うかと思います
observeNewメソッド
public func observeNew(observer: EventType -> Void) -> DisposableType
observeNewメソッドは
私(呼び出し側)とあなた(引数)の契約が締結されても、契約内容(observeNewメソッドの引数クロージャ)の実行は締結と同時ではなく次に私の保持している値が変化するごとに実行します
という契約です
既に記載していますが、契約を締結するobserveメソッドは契約締結と同時に契約内容を実行します
observeNewメソッドを使うと
下記サンプルの通り契約締結後に対象となる値が変化した時に契約内容が実行され、契約締結の段階では契約内容が実行されていないことが分かります
サンプルコード
//誕生年
let BirthYear = 1985
//西暦
let year = Observable<Int>(2016)
//現在の年齢
var currentAge = 0
year.observeNew { currentYear in
currentAge = currentYear - BirthYear
}
print(currentAge) //0
//対象値が変化した時に契約内容が実行される
year.value = 2017
print(currentAge) //32
observeNewの中を覗いてみると
public func observeNew(observer: EventType -> Void) -> DisposableType {
var skip: Int = replayLength
return observe { value in
if skip > 0 {
skip--
} else {
observer(value)
}
}
}
となっていてobserveメソッドによって契約の締結はしているが、skipというInt型の変数が0以下の場合のみ契約内容であるobserveメソッドのクロージャを実行するようになっています
詳しくは説明しませんがskip変数の初期値であるreplayLengthはObservableクラスのインスタンスの場合は全て1です
なので契約を締結しても1度だけ契約内容の実行がスキップされるので契約締結と同時の実行を防いでいるのです
mapメソッド
public func map<T>(transform: EventType -> T) -> EventProducer<T>
mapメソッドは
引数に変換処理を指定して、変換処理の返り値の型の値を保持する仲介役(EventProducer)を新たに生成する
私(呼び出し側)の保持している値が変化するごとに、仲介役の保持している値も、私の保持している値を、引数の変換処理を通した値で更新される
という契約です
簡潔な表現をするとmapメソッドを使うとBind対象となる値を変換することができます
具体的な例は、先で説明した単方向・双方向Bindingは呼び出し元と引数のBind対象の値の型が一致していなければなりませんでしたが、このmapメソッドをかますことで、呼び出し側の値に変換を加え引数側と型が一致するEventProducerを生成することができます
こうすることで型が違う同士でも単方向、双方向Bindingが下記サンプルコードのように実現できるようになります
サンプルコード
//ageとformatAgeは型が違う
let age = Observable<Int>(31)
let formatAge = Observable<String>("**歳")
print(age.value) //31
print(formatAge.value) //**歳
//mapをかますことでbindToが可能になる
age.map { (age) -> String in
return "\(age)歳"
}.bindTo(formatAge)
print(formatAge.value) //31歳
age.next(40)
print(formatAge.value) //40歳
mapメソッドの実装は下記のようになっています
public func map<T>(transform: EventType -> T) -> EventProducer<T> {
return EventProducer(replayLength: replayLength) { sink in
return observe { event in
sink(transform(event))
}
}
}
まず、今までのメソッドとの違いとして、返り値がEventProducerになっていることです
mapメソッドの中で新たにEventProducerクラスのインスタンス(仲介役)を生成して返しています
呼び出し元が保持する値が変化するごとに、mapメソッド内で生成したEventProducerの保持する値も更新するようになっていますが、その値は、呼び出し元の保持している変更後の値を引数にして、mapメソッドの引数で指定した変換処理(transformクロージャ)を実行した結果が入ります
先ほどのmapメソッドを使ったサンプルプログラムをもう少し分かりやすく書くと下記のようになります
let age = Observable<Int>(31)
let ageFormat = Observable<String>("**歳")
print(age.value) //31
print(ageFormat.value) //**歳
//mapメソッドを実行すると新たに仲介役(EventProducer)が生成される
let ageStr: EventProducer<String> = age.map { age -> String in
return "\(age)歳"
}
//仲介役であるageStrとageFormatがbindTo契約を締結
ageStr.bindTo(ageFormat)
print(ageFormat.value) //31歳
//仲介役(mapメソッドの返り値であるEventProducer)の値を直接変更した場合
ageStr.next("50さい")
print(ageFormat.value) //50さい
print(age.value) //31
age.next(90)
print(ageFormat.value) //90歳
mapメソッドを実行することで新たにEventProducerが生成されます
このEventProducerは呼び出し元であるageとの契約が締結されているEventProducerインスタンスです
契約内容は
age.map { age -> String in
return "\(age)歳"
}
age(私)の値が更新されたらInt型をString型に変更して、後ろに「歳」を追加するという内容です
age.nextなどで値が変わると契約内容が実行されageStrが変化します
mapメソッドのサンプルコードとして最初に書いた
age.map { (age) -> String in
return "\(age)歳"
}.bindTo(formatAge)
は
let ageStr: EventProducer<String> = age.map { age -> String in
return "\(age)歳"
}
ageStr.bindTo(ageFormat)
と同じ意味であり、mapメソッドはageとageFormatがbindToできるようにageの保持している値を元に、新たなEventProducerインスタンスageStrを生成して仲介していたのです
結局のところ、mapメソッドで生成されたEventProducerのインスタンスであるageStrがageFormatとbind契約を締結しているのです
ageとageFormat間に契約は一切ありません
その証拠にageStrの値を変化させてみるとageを置き去りにしてageFormatの値が変化します
//mapメソッドの返り値であるEventProducerの値を直接変化させる
ageStr.next("50さい")
print(ageFormat.value) //50さい
print(age.value) //31
仮にbindToがbidirectionalBindToになって
ageFormat.next("aaaa")
をした場合でもage変数には何の影響もありません
(ageFormatと契約しているのはageStrであり、ageStrとageの契約はageからの単方向のものであるため)
filterメソッド
public func filter(includeEvent: EventType -> Bool) -> EventProducer<EventType>
filterメソッドは
引数で条件を指定すると、仲介役(EventProducer)が生成される
私(呼び出し側)の保持する値が変化するごとに、指定した条件を満たしているか判定し、満たしていた場合のみ、仲介役の保持する値を私と同じ値で更新します
という契約です
下記では変化後の値がfilterメソッドの引数に指定した条件(クロージャ)を満たせば、filterで生成されたEventProducer(仲介役)の保持する値が変化し、仲介役の値が変化するごとにprintが実行されます
サンプルコード
let age = Observable<Int>(20)
age.filter { (age) -> Bool in
return age >= 20
}.observeNew { (age) -> Void in
print("お酒が飲める\(age)歳です")
}
age.next(10)
age.next(20)
//お酒が飲める20歳です が表示される
age.next(30)
//お酒が飲める30歳です が表示される
age.next(19)
age.next(50)
//お酒が飲める50歳です が表示される
このように条件を満たした場合のみ契約内容を実行することができます
filterメソッドの実装は
public func filter(includeEvent: EventType -> Bool) -> EventProducer<EventType> {
return EventProducer(replayLength: replayLength) { sink in
return observe { event in
if includeEvent(event) {
sink(event)
}
}
}
}
先ほどのmapと同じようにEventProducerクラスを返します
filterメソッドの引数に条件となるクロージャを渡し、filterメソッドの呼び出し元の保持する値が変化するごとに条件のクロージャが実行され、この条件を満たした場合のみ、返り値で渡したEventProducerインスタンスの保持する値が変化する仕組みです
先ほどのサンプルコードをもう少し分かりやすく書くと下記になります
let age = Observable<Int>(20)
//仲介役が生成される
let fulfillAge: EventProducer<Int> = age.filter { (age) -> Bool in
return age >= 20
}
fulfillAge.observeNew { (age) -> Void in
print("\(age)歳です")
}
age.next(10)
age.next(20)
//20歳です が表示される
age.next(30)
//30歳です が表示される
age.next(19)
age.next(50)
//50歳です が表示される
filterメソッドの返り値であるEventProducerインスタンスをfulfillAgeという変数に格納します
let fulfillAge: EventProducer<Int> = age.filter { (age) -> Bool in
return age >= 20
}
は、ageの保持する値が変化した際にageが20以上だった場合のみfulfillAgeの保持する値もageと同じ値で更新するという契約内容です
fulfillAge.observeNew { (age) -> Void in
print("\(age)歳です")
}
そしてfulfillAgeは、上記のように、自身の値が変化するごとに
print("\(age)歳です")
が実行される契約を締結しています
このことから、
age.next(10) では、
20以上であるという条件を満たさないため
fulfillAge(仲介役)の保持している値は変化しません
なので、fulfillAgeの値が変化するごと実行される契約内容(observeNewメソッドの引数クロージャ)も当然実行されないのです
distinctメソッド
public func distinct() -> EventProducer<EventType>
distinctメソッドは
新たに仲介役(EventProducer)を生成する
私(呼び出し側)の保持する値が、現在の値とは違う値で変化するごとに仲介役の保持する値も変更後の私の値で更新します
という契約です
distinctメソッドは呼び出し側の値が変化しても変更前と変更後の値が同じだった場合は、契約内容(返り値のEventProducerインスタンスの保持する値を変更)を実行しません
サンプルコード
let str = Observable<String>("")
str.distinct().observeNew { (str) -> Void in
print("変化しました \(str)")
}
str.next("aaaa")
//変化しました aaaa が出力される
str.next("aaaa")
//何も出力されない
str.next("bbbb")
//変化しました bbbb が出力される
str.next("bbbb")
//何も出力されない
str.next("aaaa")
//変化しました aaaa が出力される
このように、今までは現在の値と同じ値であっても変化(nextメソッド等)が生じた時点で契約内容が実行されていましたが、同じ値であった場合に値を更新しないことで、後続の契約内容(observeNewのクロージャ)を実行せずにすみます
注意点としてdistinctメソッドが使えるのは、対象となる値がEquatableプロトコルに準拠した型である必要があります
public extension EventProducerType where EventType: Equatable
distinctOptionalメソッド
public func distinctOptional() -> EventProducer<EventType.WrappedType?>
distinctOptionalメソッドは名前の通り、先ほどのdistinctメソッドのOptional対応版です
サンプルコード
let str = Observable<String?>("")
str.distinctOptional().observeNew { str -> Void in
guard let str = str else {
print("nil")
return
}
print(str)
}
str.next("a")
//a が出力される
str.next("a")
//何も出力されない
str.next(nil)
//nilが出力される
str.next(nil)
//何も出力されない
str.next(nil)
//何も出力されない
str.next("b")
//bが出力される
ignoreNilメソッド
public func ignoreNil() -> EventProducer<EventType.WrappedType>
ignoreNilメソッドは
新たに仲介役(EventProducer)を生成します
私(呼び出し元)の保持する値が変化するごとに、その値がnilでなければ仲介役の保持する値を変更後の私の値で更新します
という契約です
(対象の値が変化した時に、その値がnilでなければEventProducerの値を更新する)
サンプルコード
let num = Observable<Int?>(nil)
num.ignoreNil().observe { (num) -> Void in
print(num)
}
num.next(1)
//1 が出力される
num.next(nil)
//何も出力されない
num.next(2)
//2 が出力される
このように変更した値がnilの場合はignoreNilメソッドで生成されたEventProducerの値が変化しないため、後続の「値が変化したら現在値を出力する」という契約が実行されません
public func ignoreNil() -> EventProducer<EventType.WrappedType> {
return EventProducer(replayLength: replayLength) { sink in
return observe { event in
if !event.isNil {
sink(event.value!)
}
}
}
}
実装は、このように変化後の値がnilかどうか判定しています
当然、対象となる値の型がOptionalでないとignoreNilメソッドは使えません
throttleメソッド
public func throttle(seconds: Queue.TimeInterval, queue: Queue) -> EventProducer<EventType> {
throttleメソッドは
引数にTimeInterval(秒数)とQueue(スレッド)を指定して、新たに仲介役(EventProducer)を生成します
私(呼び出し側)の保持している値が最初に変化してから指定したQueue(スレッド)で指定時間待機します
(待機中でも私の保持する値の変化は受け付けます)
指定時間が経過した時の最後の私の値で仲介役の値を更新します
という契約です
(呼び出し元の保持する値に変化が生じてから引数で指定した時間待機して、その時間が経過した際の最新の値を返り値のEventProducerインスタンスの値として更新します)
throttleメソッドの利点としては指定時間待機することで、その時間内の値の変化を溜めてから後続の処理を走らせることができます
サンプルコード
let textField = UITextField()
textField.bnd_text.throttle(1.0, queue: .Main).filter { (text) -> Bool in
return text?.characters.count > 5
}.observe { (text) -> Void in
//サーバとの通信処理
}
このサンプルでthrottleメソッドがない場合は、ユーザーがテキストフィールドの値を変化させるごとにfilterメソッドが実行されますが、throttleをつけることで、引数で指定した1秒間、ユーザーの入力値を溜めてから後続のfilterメソッドが走ります
(1文字ずつの判定ではなく、まとめて判定できる)
throttleメソッドの実装です
public func throttle(seconds: Queue.TimeInterval, queue: Queue) -> EventProducer<EventType> {
return EventProducer(replayLength: replayLength) { sink in
var shouldDispatch: Bool = true
var lastEvent: EventType! = nil
return observe { event in
lastEvent = event
if shouldDispatch {
shouldDispatch = false
queue.after(seconds) {
sink(lastEvent)
lastEvent = nil
shouldDispatch = true
}
}
}
}
}
このようにthrottleメソッドの呼び出し元の値が変化すると、throttleの引数で指定したスレッドで指定秒数分、呼び出し元の値の変化を溜めておき、指定秒経過した最後の値でthrottleの返り値であるEventProducerの値が書き換えられます
ちなみに引数で指定しているQueue.TimeIntervalとQueueはSwift Bondの構造体でスレッドなどを指定します
public struct Queue {
public typealias TimeInterval = NSTimeInterval
public static let Main = Queue(queue: dispatch_get_main_queue());
public static let Default = Queue(queue: dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0))
public static let Background = Queue(queue: dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0))
public private(set) var queue: dispatch_queue_t
public init(queue: dispatch_queue_t = dispatch_queue_create("com.swift-bond.Bond.Queue", DISPATCH_QUEUE_SERIAL)) {
self.queue = queue
}
public func after(interval: NSTimeInterval, block: () -> Void) {
let dispatchTime = dispatch_time(DISPATCH_TIME_NOW, Int64(interval * NSTimeInterval(NSEC_PER_SEC)))
dispatch_after(dispatchTime, queue, block)
}
public func async(block: () -> Void) {
dispatch_async(queue, block)
}
public func sync(block: () -> Void) {
dispatch_sync(queue, block)
}
}
deliverOnメソッド
public func deliverOn(queue: Queue) -> EventProducer<EventType>
deliverOnメソッドは
引数にQueue(スレッド)を指定して仲介役(EventProducer)を生成します
私(呼び出し元)の保持している値が変化するごとに指定したスレッドで仲介役の保持している値を変化後の私の値で更新します
という契約です
deliverOnメソッドは先ほどのQueueを指定することで保持している値を変化するスレッドを指定することができます
実用的な例では、画像URLに変更があった場合に画像を取得すべくサーバと通信をするが、通信処理はバックグラウンドで処理して取得したらメインスレッドで画像を置き換えるといった場合です
テーブルCell上のImageViewに画像を表示させる時によくやる処理でしょうか
サンプルコード
var imageView = UIImageView()
let imageURL = Observable<String>("http://xxxx")
imageURL.deliverOn(.Background).map { (imageURL) -> UIImage in
//何らかの通信処理で取得したUIImageを返す
return UIImage()
}.deliverOn(.Main).bindTo(imageView.bnd_image)
対象となる値の変化ごとに次の処理に流れるので非同期処理をスムーズに書くことができます
skipメソッド
public func skip(var count: Int) -> EventProducer<EventType>
skipメソッドは
引数にcount(Int型)を指定して新たに仲介役(EventProducer)を生成します
私(呼び出し元)の保持している値が変化しても指定したcount回目の変化から仲介役の保持している値を私の値で更新します
という契約です
(skipメソッドの引数で指定したcount回目の変化から返り値のEventProduceインスタンスの値が変化します)
サンプルコード
let num = Observable<Int>(1)
let skipNum: EventProducer<Int> = num.skip(3)
var numValue = 0
skipNum.observe { (value) -> Void in
numValue = value
}
num.next(2)
print(numValue)
num.next(3)
print(numValue)
//skipメソッドで指定した3回目の変化でskipNumの値が変わったことでnumValueの値が変わる
num.next(4)
print(numValue)
skipメソッドの契約内容は、呼び出し元の値が変化した時にskipメソッドの引数で指定した数をデクリメントして0以下になるとskipメソッド内で生成したEventProducerインスタンスの値が変化するというものです
また契約締結(observeを宣言)した時点で今までは契約内容が実行されていましたが、EventProducerのイニシャライザの引数であるreplayLengthに0を指定すると契約締結時でも契約内容は実行されません
public func skip(var count: Int) -> EventProducer<EventType> {
return EventProducer(replayLength: max(replayLength - count, 0)) { sink in
return observe { event in
if count > 0 {
count--
} else {
sink(event)
}
}
}
}
combineLatestWithメソッド
public func combineLatestWith<U: EventProducerType>(other: U) -> EventProducer<(EventType, U.EventType)>
combineLatestWithメソッドは
私(呼び出し側)とあなた(引数)の双方の値をタプルで持つ仲介役(EventProducer)を生成します
私の値もしくは、あなたの値が変化するごとに仲介役のタプルの値も変化します
という契約です
(呼び出し側と引数で指定したEventProducerインスタンスの双方の値をタプルで持つEventProducerインスタンスを生成します)
サンプルコード
let num = Observable<Int>(100)
let str = Observable<String>("abcd")
let both: EventProducer<(Int, String)> = num.combineLatestWith(str)
both.observeNew { (num, str) -> Void in
print("num: \(num) str: \(str)")
}
num.next(20)
//num: 20 str: abcd が出力される
str.next("hachinobu")
//num: 20 str: hachinobu が出力される
呼び出し側であるnumの値だけでなく引数で指定したstrの値の変化もトリガーとしてbothの値が更新されます
combineLatestWithメソッドの実装をみるとまさに上記を実現するコードになっています
public func combineLatestWith<U: EventProducerType>(other: U) -> EventProducer<(EventType, U.EventType)> {
return EventProducer(replayLength: min(replayLength + other.replayLength, 1)) { sink in
var myEvent: EventType! = nil
var itsEvent: U.EventType! = nil
let onBothNext = { () -> Void in
if let myEvent = myEvent, let itsEvent = itsEvent {
sink((myEvent, itsEvent))
}
}
let myDisposable = observe { event in
myEvent = event
onBothNext()
}
let itsDisposable = other.observe { event in
itsEvent = event
onBothNext()
}
return CompositeDisposable([myDisposable, itsDisposable])
}
}
また、combineLatestWithメソッドでは呼び出し側と引数の合計2つのEventProducerの値をタプルで格納したEventProducerが生成されますが当然2つでなく、もっと沢山のEventProducerの値を持つタプルのEventProducerを生成したい場合があると思います
そのためにSwift Bondではグローバル関数が用意されています
これは3〜11のEventProducerの値をタプルで持つEventProducerを生成できます
下記は最大11個のEventProducerの値をタプルで持つEventProducerを生成するグローバル関数
public func combineLatest<A: EventProducerType, B: EventProducerType, C: EventProducerType, D: EventProducerType, E: EventProducerType, F: EventProducerType, G: EventProducerType, H: EventProducerType, I: EventProducerType, J: EventProducerType, K: EventProducerType>
( a: A, _ b: B, _ c: C, _ d: D, _ e: E, _ f: F, _ g: G, _ h: H, _ i: I, _ j: J, _ k: K) -> EventProducer<(A.EventType, B.EventType, C.EventType, D.EventType, E.EventType, F.EventType, G.EventType, H.EventType, I.EventType, J.EventType, K.EventType)>
{
return combineLatest(a, b, c, d, e, f, g, h, i, j).combineLatestWith(k).map { ($0.0, $0.1, $0.2, $0.3, $0.4, $0.5, $0.6, $0.7, $0.8, $0.9, $1) }
}
ここまでの説明を踏まえて
下記のように契約を繋いでいるコードも、繋ぎの部分には呼び出し元のEventProducerとの契約を締結した仲介役のEventProducerがいるということです
let num = Observable<Int?>(100)
let numStr = Observable<String>("")
num.distinctOptional().ignoreNil().map { (num) -> String in
return "[\(num)]"
}.bidirectionalBindTo(numStr)
print(numStr.value) //[100] が出力される
num.next(3000)
print(numStr.value) //[3000] が出力される
これはnumとnumStrに直接契約が締結されているのではなく、その間にいる仲介役達との契約の伝搬により、あたかもnumとnumStrが契約関係にあるような動きをするのです
Swift Bondの仕組みについて、おわりに
本記事では主にSwift Bondの仕組みについて書いてみました
ストリームとか川とかではなく独自の概念で説明してしまったので、伝えたいことが伝わっているか甚だ疑問が残りますが少しでも参考になれば幸いです
次は本記事で紹介したことをもとに、Swift Bondを使って実例的なサンプルを説明します
長くなりすぎてしまったので続きはこちらSwift Bondの魅力 〜実用サンプル編〜