- Published on
๐ Swift - RxSwift Traits
- Authors
- Name
- ์ด์ฐฝ์ค
Traits
Traits
๋ Observable
์ ํ ์ข
๋ฅ๋ผ๊ณ ๋ณผ ์ ์์ต๋๋ค.
Traits๋ UI ์์ญ์์ ๋ณต์กํ๊ณ ๋ค์ํ ๊ธฐ๋ฅ์ด ์๋ Observable
๋์ ์ฌ์ฉ๋๊ธฐ ์ํด ๋ง๋ค์ด์ก์ต๋๋ค.
๋ฐ๋ผ์ RxCocoa์ ์์ฃผ ๋ฐ์ ํ ๊ด๋ จ์ด ์์ง๋ง, ๋ช๋ช ๊ธฐ๋ฅ๋ค์ RxSwift ์ ๋ฐ์ ์ผ๋ก ์ฌ์ฉ๋ ์ ์๊ธฐ ๋๋ฌธ์ RxSwift์ RxCocoa์ ๋๋์ด ๊ตฌํ๋์๋ค๊ณ ํฉ๋๋ค.
๋ฑ์ฅ ์ด์
Traits๋ฅผ ์ ์ฌ์ฉํด์ผํ๋์ง๋ถํฐ ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
Traits๋ Observable
์ํ์ค๊ฐ ์์ ํ๊ฒ ํต์ ๋ ์ ์๋๋ก ๋์ต๋๋ค.
๋ชจ๋ ๋ฐฉ๋ฉด์์ ์ฌ์ฉ๋ ์ ์๋ Observable
์ ๋์ ํ์ฌ UI ์ฒ๋ฆฌ์ ํนํ๋ ๊ธฐ๋ฅ๊ณผ ๋ฌธ๋ฒ์ ์ ๊ณตํฉ๋๋ค.
๋ฐ๋ผ์ Traits๋ Observable
์ด ์ ๊ณตํ๋ ๊ธฐ๋ฅ์ ์ผ๋ถ๋ฅผ ๋ผ์ด๋ธ ๊ฒ๊ณผ ๋ค๋ฆ์๊ธฐ ๋๋ฌธ์ ์ฌ์ฉํ ์ง ๋ง์ง์ ์ฌ๋ถ๋ ์ฌ์ฉ์์๊ฒ ๋ฌ๋ ค์๋ค๊ณ ํฉ๋๋ค!
๊ธฐ๋ณธ ๊ฐ๋
Traits๋ ๋จ์ํ ํ๋์ read-only
ํ Observable
์ ๊ฐ์ธ๊ณ ์๋ struct
์
๋๋ค.
struct Single<Element> {
let source: Observable<Element>
}
struct Driver<Element> {
let source: Observable<Element>
}
Traits๋ฅผ Observable
๋ก ๋๋ ค๋๊ณ ์ถ์ ๋๋ .asObservable()
๋ก ๊ฐ๋จํ๊ฒ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
RxSwift Traits
Single
Single์ ๋ฌด์กฐ๊ฑด ํ๋์ ๊ฐ ๋๋ ์๋ฌ๋ฅผ ๋ฐฉ์ถํฉ๋๋ค.
์ง์์ ์ธ ๊ฐ์ด ์๋๋ผ ๋ฑ ํ ๋ฒ์ ๊ฒฐ๊ณผ๊ฐ์ด ํ์ํ ๋ ์ฌ์ฉ๋ฉ๋๋ค.
ํ ๋ฒ์ ๊ฐ๋ง์ ๋ฐฉ์ถํ๊ธฐ ๋๋ฌธ์ .completed
๊ฐ ์์ต๋๋ค.
์ฌ์ฉ๋๊ธฐ ๊ฐ์ฅ ์ข์ ๊ณณ์ HTTP ํต์ ์ ํ ๋์ด๋ฉฐ, request์ ํ๋์ ์๋ต๋ง์ด ๋์์ฌ ๋ ์ฌ์ฉํ๋ฉด ์ ์ฉํฉ๋๋ค.
func getRepo(_ repo: String) -> Single<[String: Any]> {
return Single<[String: Any]>.create { single in
let task = URLSession.dataTask(with: URL(string: "https://api.github.com/\(repo)")! { data, _, error in
if let error {
single(.failure(error))
return
}
guard let data,
let json = try? JSONSerialization.jsonObject(with: data, option: .mutableLeaves),
let result = json as? [String: Any] else {
single(.failure(DataError.cantParseJSON))
return
}
single(.success(result))
}
task.resume()
return Disposables.create { task.cancel() }
}
}
getRepo("ReactiveX/RxSwift")
.subscribe(onSuccess: { json in
print("JSON: ", json)
},
onError: { error in
print("Error: ", error)
})
.disposed(by: self.disposeBag)
Completable
Completable์ .completed
๋ .error
๋ง์ ๋ฐฉ์ถํฉ๋๋ค.
ํ๋ง๋๋ก ๊ฐ(element
)์ ๋ฐฉ์ถํ์ง ์์ต๋๋ค.
์์ ์ ๋ด์ฉ์ ์ค์ํ์ง ์๊ณ , ์ฑ๊ณต/์คํจ ์ฌ๋ถ๋ง ์ค์ํ ๊ฒฝ์ฐ์ ์ฌ์ฉ๋ฉ๋๋ค.
func cacheLocally() -> Completable {
return Completable.create { completable in
guard success else {
completable(.error(CacheError.failedCaching))
return Disposables.create { }
}
completable(.completed)
return Disposables.create { }
}
}
cacheLocally()
.subscribe(onCompleted: {
print("Completed with no error.")
},
onError: { error in
print("Completed with an error: \(error.localizedDescription)")
})
.disposed(by: self.disposebag)
Maybe
Maybe๋ Single๊ณผ Completable์ ์ค๊ฐ์ ์๋ Trait์ ๋๋ค.
ํ๋์ ๊ฐ์ ๋ฐฉ์ถํ๊ฑฐ๋ ๋ฐฉ์ถ ์์ด .complete
๋๊ฑฐ๋ .error
๋ฅผ ๋ฐฉ์ถํ ์ ์์ต๋๋ค.
func generateString() -> Maybe<String> {
return Maybe<String>.create { maybe in
maybe(.success("RxSwift"))
// or
maybe(.completed)
// or
maybe(.error(error))
return Disposables.create { }
}
}
generateString()
.subscribe(onSuccess: { element in
print("Completed with element \(element)")
},
.onError { error in
print("Completed with an error \(error.localizedDescription)")
},
onCompleted: {
print("Completed with no element")
})
.disposed(by: self.disposeBag)
RxCocoa Traits
Driver
๊ฐ์ฅ ๋ง์ด ์ฌ์ฉ๋๋ Trait์ ๋๋ค.
UI ๋ ์ด์ด๋ง์ ์ํด ํน๋ณํ๊ฒ ๊ฐ๋ฐ๋ ๊ธฐ๋ฅ์ ๋๋ค.
ํน์ง์ ์ ๋ฆฌํด๋ณด๋ฉด ์๋์ ๊ฐ์ต๋๋ค.
- ์๋ฌ๊ฐ ๋ฐฉ์ถํ์ง ์์ต๋๋ค
observe
๊ฐ Main Scheduler์์ ์ด๋ฃจ์ด์ง๋๋ค.- side effect๋ฅผ ๊ณต์ ํฉ๋๋ค.
let results = query.rx.text
.throttle(.milliseconds(300), scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
}
results
.map { "\($0.count)" }
.bind(to: resultCount.rx.text)
.disposed(by: self.disposeBag)
results
.bind(to: resultsTableView.rx.items(cellIdentifier: "Cell")) { _, result, cell in
cell.textLabel?.text = "\(result)"
}
.disposed(by: self.disposeBag)
์ ์์๋ ์ธ๊ฐ์ง ๋ฌธ์ ์ ๋ค์ ๊ฐ์ง๊ณ ์์ต๋๋ค.
fetchAutoCompleteItems
๊ฐ ์๋ฌ๋ฅผ ๋ฐฉ์ถํ ๊ฒฝ์ฐ, UI์ ๋ฐ์ธ๋ฉ๋์ด ์๋ ๊ฒ๋ค์ด ๋ชจ๋ unbind๋๋ฉด์ ์ดํ์ ์ฟผ๋ฆฌ๋ค์ ๋ ์ด์ UI๊ฐ ๋ณํํ์ง ์์ ๊ฒ์ ๋๋ค.fetchAutoCompleteItems
๊ฐ ๋ฉ์ธ์ฐ๋ ๋๊ฐ ์๋ ๋ฐฑ๊ทธ๋ผ์ด๋ ์ฐ๋ ๋์ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ ๊ฒฝ์ฐ, UI์ ๊ฒฐ๊ณผ๊ฐ์ ๋ฐ์ธ๋ฉํ๋ ์์ ์ด ๋ฐฑ๊ทธ๋ผ์ด๋ ์ฐ๋ ๋์์ ์ผ์ด๋ ์ ์๊ณ , ์ด๋ ์๊ธฐ์น ์์ ํฌ๋์๋ฅผ ๋ฐ์์ํฌ ์ ์์ต๋๋ค.- ๊ฒฐ๊ณผ๊ฐ์ด ๋ ๊ฐ์ UI ์ปดํฌ๋ํธ(TableView, UILabel)์ ๋ฐ์ธ๋ฉ๋์ด ์๊ธฐ ๋๋ฌธ์ ๊ฐ๊ฐ์ด ๋ฐ๋ก HTTP request๋ฅผ ์์ฒญํด ๋ถํ์ํ ์ค๋ณต ์์ฒญ์ด ๋ฐ์ํฉ๋๋ค.
์ด๋ฅผ ํด๊ฒฐํ ์ฝ๋๋ ์๋์ ๊ฐ์ต๋๋ค.
let results = query.rx.text
.throttle(.milliseconds(300), scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
.observeOn(MainScheduler.instance)
.catchErrorJustReturn([])
}
.share(replay: 1)
์์์์๋ ์ด๋ฐ ๊ณผ์ ์ด ์ฌ์ธ ์ ์์ผ๋, ์ค์ ์์ ํ๋ก์ ํธ๊ฐ ์ปค์ง์ ๋ฐ๋ผ ์ด๋ฐ ์ฌ์ํ ๋ฌธ์ ์ ๋ค์ ๋ฐ๊ฒฌํ๋ ๊ฒ์ ์ด๋ ค์ธ ์ ์์ต๋๋ค.
๋ฐ๋ผ์ RxCocoa๋ ์ด๋ฐ ๋ฌธ์ ๋ค์ ํด๊ฒฐํ UI ๋ ์ด์ด ์ ์ฉ์ Trait์ ์ ๊ณตํ๋๋ฐ, ๊ทธ๊ฒ์ด ๋ฐ๋ก Driver์ธ ๊ฒ ์ ๋๋ค.
let results = query.rx.text.asDriver()
.throttle(.milliseconds(300), scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
.asDriver(onErrorJustReturn: [])
}
results
.map { "\($0.count)" }
.drive(resultCount.rx.text)
.disposed(by: self.disposeBag)
results
.drive(resultsTableView.rx.items(cellIdentifier: "Cell")) { _, result, cell in
cell.textLabel?.text = "\(result)"
}
.disposed(by: self.disposeBag)
์ด ์ธ ๊ตฐ๋ฐ๋ง ์ฃผ๋ชฉํ๋ฉด ๋ฉ๋๋ค!
query.rx.text.asDriver()
asDriver
๋ฉ์๋๋ ControlProperty
๋ฅผ Driver
๋ก ๋ณํํฉ๋๋ค.
Driver
๋ ControlProperty
์ ๋ชจ๋ ํ๋กํผํฐ๋ฅผ ๊ฐ๊ธฐ ๋๋ฌธ์ ๋ฐ๋ก ์ ๊ฒฝ์ธ ๋ถ๋ถ์ ์์ด ๋ณํ๋ง ํ๋ฉด ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค.
.asDriver(onErrorJustReturn: [])
์ด์ ์ ์ดํด๋ดค๋ ๋ค์ ์ธ ๊ฐ์ง ์กฐ๊ฑด๋ง ๋ง์กฑํ๋ค๋ฉด, ์ด๋ค Observable
์ด๋ Driver
๋ก ๋ณํํ ์ ์์ต๋๋ค.
- ์๋ฌ๊ฐ ๋ฐฉ์ถํ์ง ์์ต๋๋ค
observe
๊ฐ Main Scheduler์์ ์ด๋ฃจ์ด์ง๋๋ค.- side effect๋ฅผ ๊ณต์ ํฉ๋๋ค. (
.share(replay:, scope:)
)
let safeSequence = xs
.observeOn(MainScheduler.instance)
.catchErrorJustReturn(onErrorJustReturn)
.share(replay: 1, scope: .whileConnected)
return Driver(raw: safeSequence)
์ ๋ฆฌํด๋ณด๋ฉด ์ ๊ณผ์ ๊ณผ asDriver(onErrorJustReturn: [])
์ ๋์ผํฉ๋๋ค.
.drive()
๋ง์ง๋ง์ผ๋ก bind(to:)
๋์ drive()
๋ฅผ ์ฌ์ฉํฉ๋๋ค.
drive()
๋ฅผ ์ฌ์ฉํจ์ผ๋ก์ UI์ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ฒ ๋ฐ์ธ๋ฉํ ์ ์์ต๋๋ค.
Signal
๊ตฌ๋
๊ณผ ๋์์ ๊ฐ์ฅ ๋ง์ง๋ง ์ด๋ฒคํธ๋ฅผ replay
ํ์ง ์๋๋ค๋ ์ ์ธ์๋ Driver์ ๋์ผํฉ๋๋ค.
ํ์ง๋ง sequence๋ฅผ ๊ณต์ ํ๋ค๋ ์ ์ ๋ณํ์ง ์๊ธฐ ๋๋ฌธ์ share
๋ฉ์๋๋ฅผ ํตํด ์ํ ๋๋ ๊ฐ์ ๊ณต์ ๋ฐ์ ์ ์์ต๋๋ค.
ControlProperty / ControlEvent
ControlProperty
UI ์ปดํฌ๋ํธ์ ํ๋กํผํฐ๋ฅผ ๋ด์ฉ์ผ๋ก ๊ฐ๋ Observable
/ ObservableType
์
๋๋ค.
- ์คํจํ์ง ์์ต๋๋ค.
share(replay: 1)
- Stateful ํฉ๋๋ค. (๊ตฌ๋ ๊ณผ ๋์์ ๋ง์ง๋ง ๊ฐ์ ํ ๋ฒ ๋ฐฉ์ถํฉ๋๋ค.)
- ์๋ฌ๋ฅผ ๋ฐ์์ํค์ง ์์ต๋๋ค.
- ๋ฉ์ธ ์ฐ๋ ๋์์ ๋์ํฉ๋๋ค.
extension Reactive where Base: UISearchBar {
public var value: ControlProperty<String?> {
let source: Observable<String?> = Observable.deferred { [weak searchBar = self.base as UISearchBar] () -> Observable<String?> in
let text = searchBar?.text
return (searchBar?.rx.delegate.methodInvoked(#selector(UISearchBarDelegate.searchBar(_:textDidChange:))) ?? Observable.empty())
.map { a in
return a[1] as? String
}
.startWith(text)
}
let bindingObserver = Binder(self.base) { (searchBar, text: String?) in
searchBar.text = text
}
return ControlProperty(values: source, valueSink: bindingObserver)
}
}
ControlEvent
UI ์ปดํฌ๋ํธ์ ์ด๋ฒคํธ๋ฅผ ๋ด์ฉ์ผ๋ก ๊ฐ๋ Observable
/ ObservableType
์
๋๋ค.
- ์คํจํ์ง ์์ต๋๋ค.
- ๊ตฌ๋ ์ด ์ด๋ฃจ์ด์ก์ ๋ ์ด๊ธฐ๊ฐ์ ๋ฐฉ์ถํ์ง ์์ต๋๋ค.
- ์๋ฌ๋ฅผ ๋ฐฉ์ถํ์ง ์์ต๋๋ค.
- ๋ฉ์ธ ์ฐ๋ ๋์์ ๋์ํฉ๋๋ค.
extension Reactive where Base: UICollectionView {
public var itemSelected: ControlEvent<IndexPath> {
let source = delegate.methodInvoked(#selector(UICollectionViewDelegate.collectionView(_:didSelectItemAt:)))
.map { a in
return a[1] as! IndexPath
}
return ControlEvent(events: source)
}
}
References