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 파일에 구현되어있다.
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)
- 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)
- 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 에 대해 언급한 적이 있다. 업데이트된 내용이 많아 다시 정리했다.
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 를 하지 않아도 되고, 메인스레드 지정을 하지 않아도 된다.