TIL
🌱 난 오늘 무엇을 공부했을까?
📌 RxSwift - Why Rx? 문서보기
- Rx는 선언 방식으로 앱을 빌드할 수 있는 방법.
🔗 Bindings
Observable.combineLatest(firstName.rx.text, lastName.rx.text) { $0 + " " + $1 }
.map { "Greetings, \($0)" }
.bind(to: greetingLabel.rx.text)
viewModel
.rows
.bind(to: resultsTableView.rx.items(cellIdentifier: "WikipediaSearchCell", cellType: WikipediaSearchCell.self)) { (_, viewModel, cell) in
cell.title = viewModel.title
cell.url = viewModel.url
}
.disposed(by: disposeBag)
- 단순 바인딩에 필요하지 않더라도 항상 .disposed(by: disposeBag)를 사용하자.
🔗 Retries
- API는 실패할 가능성이 있다.
- 아래와 같은 코드가 실패했을때 재시도 하기 어렵다.
func doSomethingIncredible(forWho: String) throws -> IncredibleThing
doSomethingIncredible("me")
.retry(3)
🔗 Delegates
public func scrollViewDidScroll(scrollView: UIScrollView) { [weak self]
self?.leftPositionConstraint.constant = scrollView.contentOffset.x
}
self.resultsTableView
.rx.contentOffset
.map { $0.x }
.bind(to: self.leftPositionConstraint.rx.constant)
🔗 Notifications
@available(iOS 4.0, *)
public func addObserverForName(name: String?, object obj: AnyObject?, queue: NSOperationQueue?, usingBlock block: (NSNotification) -> Void) -> NSObjectProtocol
NotificationCenter.default
.rx.notification(NSNotification.Name.UITextViewTextDidBeginEditing, object: myTextView)
.map { }
....
🔗 Transient state
- 비동기 프로그램을 작성할 때 일시적인 상태에도 많은 문제가 있다. 예) 자동완성 검색 상자
- Rx를 사용하지 않고 자동완성 기능을 사용할 때 생기는 문제
- 'abc'의 'c'가 입력되고 'ab'에 대한 보류 중인 요청이 있을 때 보류 중인 요청이 취소된다
- 요청이 실패하면 지저분한 재시도 논리를 수행해야 한다
- 프로그램이 서버에 대한 요청을 실행하기 전에 일정 시간 동안 기다리지 못한다. 그로인해 매우 긴 내용을 입력하는 과정에 있을 경우 서버에 스팸 메일을 보내게 된다.
- 검색이 실행되는 동안 화면에 무엇을 표시해야 하는지, 모든 재시도에도 실패할 경우에 무엇을 표시해야 하는지에 대한 고민을 해야한다.
- 이 모든 것을 작성하고 적절하게 테스트하는 것은 지루하지만, 아래는 Rx로 풀어낸 소스.
searchTextField.rx.text
.throttle(.milliseconds(300), scheduler: MainScheduler.instance)
.distinctUntilChanged()
.flatMapLatest { query in
API.getSearchResults(query)
.retry(3)
.startWith([])
.catchErrorJustReturn([])
}
.subscribe(onNext: { results in
})
.disposed(by: disposeBag)
- 추가 플래그나 필드가 필요하지 않고, Rx는 모든 일시적인 혼란을 처리한다.
🔗 Compositional disposal
- 테이블 뷰에 흐린 이미지를 표시하려는 시나리오가 있다고 가정해보자. 먼저 이미지를 URL에서 가져온 다음 디코딩한 다음 흐리게 처리한다.
- 또한 블러 처리를 위한 대역폭과 프로세서 시간이 비싸기 때문에, 셀이 보여지는 테이블 뷰 영역을 벗어나면 전체 프로세스가 취소될 수 있으면 좋다.
- 사용자가 매우 빠르게 스와이프하면 많은 요청이 시작되고 취소될 수 있기 때문에 셀이 가시 영역에 들어가면 즉시 이미지 가져오기를 시작하지 않는 것도 좋을 것이다.
- 또한 이미지를 흐리게 하는 것은 비용이 많이 드는 작업이기 때문에 동시 이미지 작업의 수를 제한할 수 있다면 좋을 것이다.
- 위의 내용을 Rx를 사용하여 할 수 있는 방법
let imageSubscription = imageURLs
.throttle(.milliseconds(200), scheduler: MainScheduler.instance)
.flatMapLatest { imageURL in
API.fetchImage(imageURL)
}
.observeOn(operationScheduler)
.map { imageData in
return decodeAndBlurImage(imageData)
}
.observeOn(MainScheduler.instance)
.subscribe(onNext: { blurredImage in
imageView.image = blurredImage
})
.disposed(by: reuseDisposeBag)
- 이 코드는 모든 작업을 수행하며 imageSubscription이 삭제되면 종속된 모든 비동기 작업을 취소하고 불량 이미지가 UI에 바인딩되지 않도록 한다.
🔗 Aggregating network requests
- 네트워크 요청 시 둘 다 완료된 후 두개의 결과를 합산해야 할 때 유용하다.
zip
을 사용한 예시
let userRequest: Observable<User> = API.getUser("me")
let friendsRequest: Observable<[Friend]> = API.getFriends("me")
Observable.zip(userRequest, friendsRequest) { user, friends in
return (user, friends)
}
.subscribe(onNext: { user, friends in
})
.disposed(by: disposeBag)
- API가 백그라운드 스레드에서 결과를 반환하고 기본 UI 스레드에서 바인딩이 발생해야 하는 경우 유용하다.
observeOn
을 사용한 예시
let userRequest: Observable<User> = API.getUser("me")
let friendsRequest: Observable<[Friend]> = API.getFriends("me")
Observable.zip(userRequest, friendsRequest) { user, friends in
return (user, friends)
}
.observeOn(MainScheduler.instance)
.subscribe(onNext: { user, friends in
})
.disposed(by: disposeBag)
🔗 Easy integration
- 자신만의 옵저버블을 생성해야 한다면? 꽤 쉽다.
- 이 코드는 RxCocoa에서 가져왔으며 URLSession으로 HTTP 요청을 래핑하는 데 필요한 모든 것이다.
extension Reactive where Base: URLSession {
public func response(request: URLRequest) -> Observable<(Data, HTTPURLResponse)> {
return Observable.create { observer in
let task = self.base.dataTask(with: request) { (data, response, error) in
guard let response = response, let data = data else {
observer.on(.error(error ?? RxCocoaURLError.unknown))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
observer.on(.error(RxCocoaURLError.nonHTTPResponse(response: response)))
return
}
observer.on(.next(data, httpResponse))
observer.on(.completed)
}
task.resume()
return Disposables.create(with: task.cancel)
}
}
}
🔗 Benefits
- Rx는 컴포지션 닉네임이기 때문에 구성이 가능하다.
- 구성이 가능하기 때문에 재사용이 가능하다.
- 정의는 변경할 수 없고 데이터만 변경되기 때문에 선언형이다.
- 추상화 수준 높이기 및 일시적 상태 제거해서 이해하기 쉽고 간결함.
- Rx 코드는 철저하게 단위 테스트를 거쳐서 안정적이다.
- 단방향 데이터 흐름으로 애플리케이션을 모델링하기 때문에 상태 저장이 적다
- 리소스 관리가 쉽기 때문에 누출이 없다.
Rx의 모든 연산자들
마블 다이어그램
사용방법