Help us understand the problem. What is going on with this article?

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をお楽しみください。

shoheiyokoyama
【横山 祥平 / @shoheiyokoyama 】 CyberAgent, Inc / AbemaTV / CATS / iOS Engineer Medium: https://medium.com/@shoheiyokoyama
https://github.com/shoheiyokoyama
cyberagent
サイバーエージェントは「21世紀を代表する会社を創る」をビジョンに掲げ、インターネットテレビ局「AbemaTV」の運営や国内トップシェアを誇るインターネット広告事業を展開しています。インターネット産業の変化に合わせ新規事業を生み出しながら事業拡大を続けています。
http://www.cyberagent.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした