Edited at

RxSwiftのTraitについて

More than 1 year has passed since last update.

AbemaTVでiOSアプリ開発をしておりますshoheiyokoyamaです。

今回は、AbemaTV Advent Calendar 2017の20日目の記事を担当させていただきます。


RxSwift

RxSwiftはReactive programingを実現するためのSwift製フレームワークで、AbemaTVのiOSアプリでメインフレームワークとして使用されています。Reactive Programmingや、それに関連したObserverパターンについては、過去に記事でまとめたものがあるのでこちらで雰囲気だけでも掴んでいただければと思います。

さて、タイトルでTraitという言葉がでてきましたが、RxSwiftはTraitという様々なユースケースで使用できるObservableのラッパーを提供しています。iOSのcocoaフレームワーク用にRxCocoa traitsというものもありますが、読者のみなさんが眠くなってしまう危険もありますので、今回はRxSwift traitsの説明だけにとどめたいと思います。

RxCocoa traitsDriverに関しては、2日前に @inamiy さんがRxSwift.Driver についての個人的見解という素晴らしい記事を書いているので、ご覧いただければと思います。


RxSwift Trait

今回は以下3つのTraitについて触れたいと思います。


  • Single

  • Completable

  • Maybe

それぞれのTraitを説明する前に、RxSwift traitsについてもう少し説明します。

RxSwiftのドキュメントをみてみると、Traitは簡単なObservableのラッパーで、read-onlyなプロパティという説明があります。

また、RxCocoa traitsと異なり、副作用がないObservableです。

struct Single<Element> {

let source: Observable<Element>
}

しかしコードを追ってみると、正確にはtypealiasで実体はPrimitiveSequenceであることがわかります。

/// Represents a push style sequence containing 1 element.

public typealias Single<Element> = PrimitiveSequence<SingleTrait, Element>

/// Represents a push style sequence containing 0 elements.
public typealias Completable = PrimitiveSequence<CompletableTrait, Swift.Never>

/// Represents a push style sequence containing 0 or 1 element.
public typealias Maybe<Element> = PrimitiveSequence<MaybeTrait, Element>

このPrimitiveSequenceGenirics TypeとしてTraitElementをもつ構造体で、TraitTypeによって振る舞いを変える設計となっています。


PrimitiveSequence.swift


/// Observable sequences containing 0 or 1 element.
public struct PrimitiveSequence<Trait, Element> {
let source: Observable<Element>

init(raw: Observable<Element>) {
self.source = raw
}
}

extension PrimitiveSequence: PrimitiveSequenceType {
/// Additional constraints
public typealias TraitType = Trait
/// Sequence element type
public typealias ElementType = Element
...



Single.swift


extension PrimitiveSequenceType where TraitType == SingleTrait {
public static func create(subscribe: @escaping (@escaping SingleObserver) -> Disposable) -> Single<ElementType> {


Completable.swift

extension PrimitiveSequenceType where Self.ElementType == Never, Self.TraitType == RxSwift.CompletableTrait {

public static func create(subscribe: @escaping (@escaping PrimitiveSequenceType.CompletableObserver) -> Disposable) -> RxSwift.PrimitiveSequence<Self.TraitType, Self.ElementType>


Maybe.swift

public extension PrimitiveSequenceType where TraitType == MaybeTrait {

public static func create(subscribe: @escaping (@escaping MaybeObserver) -> Disposable) -> PrimitiveSequence<TraitType, ElementType>

RubyPythonなどのTraitのような実装パターンですね。RxSwift Traitの名前もここからきてるんでしょうか。

それぞれのTraitでcreateが実装されてますが、これはReactiveXで定義されているOperatorで、Observableのファクトリーメソッドです。API Design Guidelinesで記載があるようにSwiftのファクトリーメソッドの命名ではmake-prefixが推奨されてますが、あくまでここはReactiveXの慣習に習ってるようです。

ドキュメント:ReactiveX / create

標準のcreate operatorをは、カスタムのObservableを生成することができ、onNext, onError, onCompletedを自由に呼び出すことができます。

func customObservable() -> Observable<Element> {

return Observable.create { observer in
observer.onNext(element)
observer.onError(error)
observer.onCompleted()
return Disposables.create()
}
}


Single


A Single is a variation of Observable that, instead of emitting a series of elements, is always guaranteed to emit either a single element or an error. Traits (formerly Units)より一部抜粋


Singleは一回のみElementかErrorを送信することが保証されているObservableです。

一回イベントを送信すると、disposeされるようになってます。

var singleObservable: Single<Element> {

return Single.create { single in
fetchData { data, error in
if let error = error {
single(.error(error))
}

single(.success(data))
}

return Disposables.create()
}
}

少し内部のコードを追ってみましょう。

createの内部ではsubscribeとしてSingleObserver(=SingleEvent)を返すようになっており、SingleEventではResultのようにsuccesserrorのみ定義されているため、実装者はonComplete()を呼び出せないようになっています。

createの内部では標準のcreate operatorが使用されており、SingleEvent.successが呼び出されたら即座にon(.completed)していることがわかります。


Single.swift


public enum SingleEvent<Element> {
/// One and only sequence element is produced. (underlying observable sequence emits: `.next(Element)`, `.completed`)
case success(Element)

/// Sequence terminated with an error. (underlying observable sequence emits: `.error(Error)`)
case error(Swift.Error)
}

public typealias SingleObserver = (SingleEvent<ElementType>) -> ()

public static func create(subscribe: @escaping (@escaping SingleObserver) -> Disposable) -> Single<ElementType> {
let source = Observable<ElementType>.create { observer in
return subscribe { event in
switch event {
case .success(let element):
observer.on(.next(element))
observer.on(.completed) // Itemを送信したらcompletedする
case .error(let error):
observer.on(.error(error))
}
}
}

return PrimitiveSequence(raw: source)
}


observer.on(.error(error))の場合はobserver.on(.completed)を呼び出していませんが、on(_ event: Event<E>)の実装をみてみると、errorが放出された場合にはobserver.on(.completed)と同様dispose()が呼び出されることがわかります。

func on(_ event: Event<E>) {

#if DEBUG
_synchronizationTracker.register(synchronizationErrorMessage: .default)
defer { _synchronizationTracker.unregister() }
#endif
switch event {
case .next:
if _isStopped == 1 {
return
}
forwardOn(event)
case .error, .completed: // errorとcompletedの挙動は同じ
if AtomicCompareAndSwap(0, 1, &_isStopped) {
forwardOn(event)
dispose()
}
}
}

ここまで理解できれば、残りのTraitも簡単です。


Completable

Completableは名前の通り、errorcompletedのイベントのみ送信するObservableで、onNextによるイベントでItemがこないことが保証されています。

var completableObservable: Completable {

return Completable.create { completable in
excuteWork { result in
if case let .error(e) = result {
completable(.error(e))
}
completable(.completed)
}

return Disposables.create {}
}
}

errorcompletedが送信されるとObservableはdisposeされるので、一度のみEventを送信することも保証されてますね。


Completable.swift

public enum CompletableEvent {

/// Sequence terminated with an error. (underlying observable sequence emits: `.error(Error)`)
case error(Swift.Error)

/// Sequence completed successfully.
case completed
}

...

public static func create(subscribe: @escaping (@escaping CompletableObserver) -> Disposable) -> PrimitiveSequence<TraitType, ElementType> {
let source = Observable<ElementType>.create { observer in
return subscribe { event in
switch event {
case .error(let error):
observer.on(.error(error))
case .completed:
observer.on(.completed)
}
}
}

return PrimitiveSequence(raw: source)
}



Maybe

Maybeは通常のEvent同様next, error, completedを送信することができ、一度のみのEvent送信が保証されています。いずれかのEventが送信されると即座にObservabledisposeされます。

var maybeObservable: Maybe<String> {

return Maybe<String>.create { maybe in
// OR
maybe(.success("Maybe"))

// OR
// maybe(.completed)

// OR
// maybe(.error(error))

return Disposables.create {}
}
}


Maybe.swift

public enum MaybeEvent<Element> {

/// One and only sequence element is produced. (underlying observable sequence emits: `.next(Element)`, `.completed`)
case success(Element)

/// Sequence terminated with an error. (underlying observable sequence emits: `.error(Error)`)
case error(Swift.Error)

/// Sequence completed successfully.
case completed
}

...

public static func create(subscribe: @escaping (@escaping MaybeObserver) -> Disposable) -> PrimitiveSequence<TraitType, ElementType> {
let source = Observable<ElementType>.create { observer in
return subscribe { event in
switch event {
case .success(let element):
observer.on(.next(element))
observer.on(.completed) // nextのあとにcompletedされる
case .error(let error):
observer.on(.error(error))
case .completed:
observer.on(.completed)
}
}
}

return PrimitiveSequence(raw: source)
}


今回は、RxSwift traitsについて簡単に説明しました。

Eventを一度のみだけ受け付けることができる制約は非常に有用で、AbemaTVでもAPIによる値の取得やDBへの書き込みなどは積極的にtraitが使われています。

また、コードを追うことで理解が深まる上に、様々な実装パターンをみることができるのでおすすめです。

簡単ですが、サンプルコードはこちらをご覧ください

今年も残りわずかですが、2017年悔いが残らぬよう、精一杯RxSwift lifeをお楽しみください。