brunch

매거진 ReactiveX

You can make anything
by writing

C.S.Lewis

by Tilltue Apr 26. 2017

RxSwift Traits(특성)

Single, Completeable, Maybe

* 이 포스트는 RxSwift 4.3.1, swift 4.2 버전을 기준으로 작성되었습니다.

* Traits 는 RxSwift 3.3.0 에서 추가되었습니다.


참조 : https://github.com/ReactiveX/RxSwift/blob/master/Documentation/Traits.md


RxSwift 에서 Traits를 통해 명확한 이벤트 발생규칙을 가진 Observable 을 사용할 수 있도록 지원한다.

코드의 명확함과 직관성을 가지고자 할때 선택적으로 사용하면 된다. 코드의 의도를 분명히 할수 있다는 장점이 있다.

- RxSwift framework 의 PrimitiveSequence.swift 파일에 구현되어있다.



1. Single

http://reactivex.io/documentation/single.html

- Single Observable 의 이벤트 타입.

public enum SingleEvent<Element> {

    case success(Element)

    case error(Swift.Error)

}


설명

1~n의 무한한 이벤트 스트림 처리가 필요하지 않고, 하나의 결과값 or 에러 를 처리하고자 하는 모델에서 사용한다. 일반 Observable 에서 처리하는 onNext, onError, onComplete 세가지 처리가 필요없으며 Success, Error 처리만 하면된다.

기본적으로 Observable 이므로, 기타 Observable 연산자들을 사용할 수 있다.

( map flatmap just merge concat 등등 )


예로 Http request 응답을 처리할때처럼, 하나의 응답혹은 에러를 처리하려할때 사용하면 유용하다.


- RxSwift 문서에서 제공한 사용 예


func getRepo(_ repo: String) -> Single<[String: Any]> {

    return Single<[String: Any]>.create { single in

        let task = URLSession.shared.dataTask(with: URL(string: "https://api.github.com/repos/\(repo)")!) { data, _, error in

            if let error = error {

                single(.error(error))

                return

            }

            guard let data = data,

                  let json = try? JSONSerialization.jsonObject(with: data, options: .mutableLeaves),

                  let result = json as? [String: Any] else {

                single(.error(DataError.cantParseJSON))

                return

            }

            single(.success(result))

        }

        task.resume()

        return Disposables.create { task.cancel() }

    }

}


- subscribe

( 또는 onSuccess: onError: 를 사용할 수 있다. )

getRepo("ReactiveX/RxSwift")

    .subscribe { event in

        switch event {

            case .success(let json):

                print("JSON: ", json)

            case .error(let error):

                print("Error: ", error)

        }

    }

    .disposed(by: disposeBag)


2. Completeable


- Completeable Observable 의 이벤트 타입.

public enum CompletableEvent {

    case error(Swift.Error)

   case completed

}


Observable 의 완료만 의미가 있고, 결과값이 없거나, 필요하지 않을때 완료 그 자체에 의미를 가진 모델에서 사용하면 되며, Observable<Void> 에서 이벤트를 emit 하지 않는 것과 같은 의미로 이해하면 된다.


- RxSwift 문서에서 제공한 사용 예

func cacheLocally() -> Completable {

    return Completable.create { completable in

       // Store some data locally

       ...

       ...

       guard success else {

           completable(.error(CacheError.failedCaching))

           return Disposables.create {}

       }

       completable(.completed)

       return Disposables.create {}

    }

}


- subscribe

( 또는 onCompleted: onError: 를 사용할 수 있다. )

cacheLocally()

    .subscribe { completable in

        switch completable {

            case .completed:

                print("Completed with no error")

            case .error(let error):

                print("Completed with an error: \(error.localizedDescription)")

        }

    }

    .disposed(by: disposeBag)


3. Maybe

- Maybe Observable 의 이벤트 타입.

public enum MaybeEvent<Element> {

    case success(Element)

    case error(Swift.Error)

    case completed

}

Maybe 는 Completeable , Single 을 합쳐놓은 Observable 이다.

하나의 이벤트가 발생되거나, 이벤트가 없이 완료되거나, 에러가 발생하거나 셋중의 하나의 결과만을 발생하며 이후 종료된다.

일반 Observable 을 asMaybe() 로 변형 할 수도 있다.


- RxSwift 문서에서 제공한 사용 예


func generateString() -> Maybe<String> {

    return Maybe<String>.create { maybe in

        maybe(.success("RxSwift"))

        // OR

        maybe(.completed)

        // OR

        maybe(.error(error))

        return Disposables.create {}

    }

}


- subscribe

( 또는 onSuccess: onError: onCompleted 를 사용할 수 있다. )

generateString()

    .subscribe { maybe in

        switch maybe {

            case .success(let element):

                print("Completed with element \(element)")

            case .completed:

                print("Completed with no element")

            case .error(let error):

                print("Completed with an error \(error.localizedDescription)")

        }

    }

    .disposed(by: disposeBag)


RxCocoa traits 는 Driver 가 있다.

RxCocoa framework 의 Driver.swift 에 구현되어있다.

2.6 버전 글 에서 Driver 에 대해 언급한 적이 있다. 업데이트된 내용이 많아 다시 정리했다.


* Driver (RxCocoa)

UI 요소들을 바인딩 할때 메인스레드에서 수행해야 하며, 이를 직관적으로 알수 있도록 만들어진 traits 이다.

다음의 특징이 있다.

- 메인스레드에서 수행.

- 에러를 발생하지 않음.

- 사이드 이펙트 공유.

UI에 연결할때 사용한다고 이해하면된다.


- RxSwift 문서에서 제공한 사용 예

잘못된 사용예

let results = query.rx.text

    .throttle(0.3, scheduler: MainScheduler.instance)

    .flatMapLatest { query in

        fetchAutoCompleteItems(query)

    }

results

    .map { "\($0.count)" }

    .bind(to: resultCount.rx.text)

    .disposed(by: disposeBag)

results

    .bind(to: resultsTableView.rx.items(cellIdentifier: "Cell")) { (_, result, cell) in

        cell.textLabel?.text = "\(result)"

    }

    .disposed(by: disposeBag)


text 입력을 감지해서 auto complete item 들을 가져와서 label 에 카운트 갯수를 보여주고,

tableview 에 결과리스트를 보여주는 코드로 보인다.


이 코드는 다음과 같은 문제점이 있다.

- 이 코드에서 만약 fetchAutoCompleteItems 에서 에러가 발생하면 연결된 biding 은 모두 끊기고 UI 는 더이상 갱신되지 않는다.

- 만약 fetchAutoCompleteItems 요청이 백그라운드에서 수행되고, 결과가 백그라운드 스레드를 통해 전달된다면 UI 요소에 binding된 동작이 백그라운드에서 일어나게 되는 문제가 발생한다.

- results 에 2개의 UI 요소에 binding 을 했기때문에 두번의 http 요청이 발생하게 된다.


적절한 사용 예

let results = query.rx.text

    .throttle(0.3, scheduler: MainScheduler.instance)

    .flatMapLatest { query in

        fetchAutoCompleteItems(query)

            .observeOn(MainScheduler.instance)  // results are returned on MainScheduler

            .catchErrorJustReturn([])                       // in the worst case, errors are handled

    }

    .shareReplay(1)                                               // HTTP requests are shared and results replayed

                                                                                // to all UI elements

results

    .map { "\($0.count)" }

    .bind(to: resultCount.rx.text)

    .disposed(by: disposeBag)

results

    .bind(to: resultsTableView.rx.items(cellIdentifier: "Cell")) { (_, result, cell) in

        cell.textLabel?.text = "\(result)"

    }

    .disposed(by: disposeBag)


변경된 내용

flatMapLatest 는 fetchAutoCompleteItems Observable 을 메인스레드에서 수행하게 하고 에러가 감지되면 빈 결과값을 리턴하도록 수행해서 에러가 발생해도 binding이 끊어지지 않도록 했다.

또한 shareReplay(1) 을 통해 subscription 공유를 통해 여러 UI 요소에 바인딩되어도 하나의 http 요청만 발생하도록 했다.


Driver의 사용

let results = query.rx.text.asDriver()

    .throttle(0.3, scheduler: MainScheduler.instance)

    .flatMapLatest { query in

        fetchAutoCompleteItems(query)

            .asDriver(onErrorJustReturn: [])

    }

results

    .map { "\($0.count)" }

    .drive(resultCount.rx.text)

    .disposed(by: disposeBag)

results

    .drive(resultsTableView.rx.items(cellIdentifier: "Cell")) { (_, result, cell) in

        cell.textLabel?.text = "\(result)"

    }

    .disposed(by: disposeBag)


- ControlProperty 특성을 asDriver() 를 통해 Driver 특성으로 변환했다.

Driver 특성때문에 sharerelay 를 하지 않아도 되고, 메인스레드 지정을 하지 않아도 된다.




브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari