### Rx를 사용하면 선언형 프로그래밍 방식으로 앱을 빌드 할 수 있습니다.
💡 Bindings
Observable.combineLatest(firstName.rx.text, lastName.rx.text) { $0 + " " + $1 }
.map { "Greetings, \($0)" }
.bind(to: greetingLabel.rx.text)
UITableView, UICollectionView에서도 작동합니다.
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)`을 사용할 필요 없는 간단한 binding이라도 항상 사용하는 것이 공식 제안입니다.
### 💡 Retries
API관련 메소드는 실패하는 상황이 종종 있습니다.
func doSomethingIncredible(forWho: String) throws -> IncredibleThing
이 메소드는 실패할 경우 재사용하기 힘듭니다.
Rx를 통해서 간단히 재사용하는 방법입니다.
doSomethingIncredible("me")
.retry(3)
💡 Delegates
지루하고 비표현적인 방법 대신에
public func scrollViewDidScroll(scrollView: UIScrollView) { [weak self] // what scroll view is this bound to?
self?.leftPositionConstraint.constant = scrollView.contentOffset.x
}
다음과 같이 사용할 수 있습니다.
self.resultsTableView
.rx.contentOffset
.map { $0.x }
.bind(to: self.leftPositionConstraint.rx.constant)
💡 KVO
#### Key-Value Observing
property의 변경사항을 다른 객체에 알리기 위해 사용하는 cocoa 프로그래밍 패턴
`rx.observe`, `rx.observeWeakly`를 사용합니다.
```swift view.rx.observe(CGRect.self, "frame") .subscribe(onNext: { frame in print("Got new frame \(frame)") }) .disposed(by: disposeBag) ```
someSuspiciousViewController
.rx.observeWeakly(Bool.self, "behavingOk")
.subscribe(onNext: { behavingOk in
print("Cats can purr? \(behavingOk)")
})
.disposed(by: disposeBag)
💡 Notifications
@available(iOS 4.0, *)
public func addObserverForName(name: String?, object obj: AnyObject?, queue: NSOperationQueue?, usingBlock block: (NSNotification) -> Void) -> NSObjectProtocol
대신
```swift NotificationCenter.default .rx.notification(NSNotification.Name.UITextViewTextDidBeginEditing, object: myTextView) .map { /*do something with data*/ } .... ```
다음 처럼 사용할 수 있습니다.
### 💡 Transient state
비동기 프로그램을 구현할때 transient state 관련 문제가 많이 발생합니다. 일반적응 예는 자동 완성 검색창 입니다.
Rx없이 자동완성을 구현할때 만나는 첫번째문제는
abc
중 ab
가 입력되고 ab
리퀘스트를 처리하는 동안에 C
가 입력되면 처리중이던 ab
입력 리퀘스트는 취소됩니다. pending request(ab
처리)에 대한 참조를 저장할 변수를 따로 생성해야합니다.
다음 문제는 만약 pending request가 실패하면 지저분한 재시도 logic을 작성해야합니다.
server에 request를 보내기전에 program이 일정시간 기다려준다면 좋을것입니다. 매우 긴 내용을 입력하는 경우 server에 불필요한 reqeust를 줄일 수 있습니다.
serch가 실행되는동안이나 모든 재시도에 실패한 경우 화면에 어떤 것을 보여줘야할지 의문입니다.
Rx를 통해 다음과 같이 구현할 수 있습니다.
searchTextField.rx.text
.throttle(.milliseconds(300), scheduler: MainScheduler.instance) // 0.3초동안 request 보내지 않는다.
.distinctUntilChanged() // 변화가 있을때만
.flatMapLatest { query in
API.getSearchResults(query)
.retry(3)
.startWith([]) // clears results on new search term
.catchErrorJustReturn([])
}
.subscribe(onNext: { results in
// bind to ui
})
.disposed(by: disposeBag)
💡 Compositional disposal
tableView에 blur image들을 표시한다고 생각해보면 URL에서 image들이 fetch되고 decoding한 후 blur 처리합니다.
blur처리 중 과도한 대역폭과 처리 시간 때문에 한 셀이 table view의 보여지는 영역 밖으로 나가면 모든 process 종료시킨다면 더 좋을 것 같습니다.
사용자가 빠르게 swipe한다면 많은 request가 발생되고 취소될 수 있기떄문에 우리는 cell이 보여지는 역역에 들어왔을때 바로 fetch를 실행하지 않으면 좋겠습니다.
blur처리는 무거운 작업이기 떄문에 동시에 실행하는 image 작업의 수를 제한할 수 있으면 좋겠습니다.
Rx를 통해 다음과 같이 구현할 수 있습니다.
// this is a conceptual solution
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이 dispose 됐을때, 관련된 모든 비동기 작업을 취소하고 rougue 이미지들을 UI에 바인딩되지 않도록 합니다.
### 💡 Aggregating network requests
2개의 request를 실행하고 둘다 완료됐을때 결과를 집계하려면 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
// bind them to the user interface
})
.disposed(by: disposeBag)
API가 result를 background thread에서 return 하고 binding은 main UI thread에서 발생해야한다면 `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
// bind them to the user interface
})
.disposed(by: disposeBag)
💡 State
mutaion을 허락한 언어는 global 상태에 쉽게 access하고 mutate할 수 있습니다. 공유된 global state의 통제되지않은 mutation들은 combinatorial explosion을 발생시킵니다.
combinatorial explosion을 막는 방법은 state를 가능한 단순하게 유지하고 단방향 data 흐름을 사용해 파생된 data를 modeling합니다.
💡 Easy integraion
자신만의 observable이 필요하다면 생성할 수 있습니다.
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
- Composable: Rx는 composition의 별명입니다.
- Reusable: composable 하기때문
- Declarative: definition들은 변경될 수 없고 data만 변경될 수 있다.
- Understandable and concise: 추상화 수준을 높이고 transient state를 제거합니다.
- Stable: Rx code는 철저하게 unit test를 거쳤습니다.
- Less stateful: 단방향 data 흐름으로 application을 모델링합니다.
- Without leaks: resource management가 쉽습니다.