raywenderlich RxSwift Ch7~8
๊ธฐ์กด ์ฝ๋๋ฅผ ์ฝ์ผ๋ฉด์ ๊ฐ์ฅ ์ดํด๊ฐ ์๊ฐ๋!!!!!! ํํธ๊ฐ ์๋ค. ์ฌ๊ธฐ๋ฅผ ์ ๋๋ก ๊ณต๋ถํ๋๊ฒ ์ค์ํ ๊ฒ ๊ฐ๋ค.
๐ ์ผ๊ณฐ์ Swift Programming ์ Monad ๋ฅผ ์ฝ๊ณ ๋๋ flatMap ์ด ์กฐ๊ธ ๋ ์ดํด๋๋ ๊ฒ ๊ฐ๋ค!!
Observable ์์ ๋ฐฉ์ถ๋๋ ์์๋ค์ ๋ ๋ฆฝ์ ์ด์ง๋ง, ์ด๊ฑธ ๊ทธ๋ฃน์ผ๋ก ๋ฌถ์ด์ ๋ณด๊ณ ์ถ์ ๋๊ฐ ์๋ค.
์ผ๋ฐ์ ์ธ map ๊ณผ ๊ฐ๋ค. iterable ์์๋ฅผ ํ๋์ฉ ์ํํ๋ฉด์ ํจ์๋ฅผ ์ ์ฉํด์ฃผ๋ ๊ฒ์ Observable ์์ emit ๋๋ ์์๋ฅผ ํ๋์ฉ ํจ์ ์ ์ฉ์์ผ์ค.
ํด๋น Observable ๊ฐ์ value ๋ก ๊ฐ์ง๋ tuple ์ ๋ง๋ค์ด์ ๋ด๋ ค์ค๋ค.
Q. ๋ง์ฝ... Observable ์์ฑ์ด Observable ์ next ๋ก ๋ด๋ ค์จ๋ค๋ฉด..? ์ด๋ป๊ฒ ํ ๊น??
struct Student {
var score: BehaviorSubject<Int>
}
// Student ๊ฐ ๋ด๋ ค์จ๋ค๋ฉด?
A. flatMap ์ ์ฐ์ฐ์ ๋ด์ ๋ช ๊ฐ์ง ์ฐ์ฐ์๋ฅผ ๊ฐ์ง๊ณ ์์ด์, Observable ๋ด๋ถ์์ Observable ์ ์์ฑ์ ๊ฐ์ง๊ณ ์์ ํ ์ ์๋ค.
transform the items emitted by an Observable into Observables, then flatten the emissions from those into a single Observable ๋ญ๋ง์ผ๊น
์์ ๊ทธ๋ฆผ์ฒ๋ผ, flatMap์ ๊ฐ Observable ์ ์ง์ผ๋ณด๋ฉด์ ํ๋์ Observable Sequence ์์ ๋ด๋ ค์จ Observable Element ๋ค์ ๊ฐ๊ฐ ํผ์ณ์ Observable Sequence ๋ก ๋ง๋ค๊ณ , ๊ทธ๋ ๊ฒ ์๊ธด ์ฌ๋ฌ๊ฐ์ Sequence ๋ฅผ ํ๋์ Sequence๋ก ํฉ์ณ์ค๋ค. ๋ฌผ๋ก Map ์ด๋๊น ํจ์๋ ์ ์ฉ์์ผ์ค๋ค.
// ์ด๋ฐ ์์ผ๋ก nil ๊ฐ์ ์ฒ๋ฆฌํ๊ธฐ ์ํด์ ์ฌ์ฉํ๊ธฐ๋ ํ๋ค. .flatMap { $0 == nil ? Observable.empty() : Observable.just($0!) }
-> ์ด ๊ฒฝ์ฐ์๋ filter ๋ก๋ ์ถฉ๋ถํ ์ฒ๋ฆฌํ ์ ์์ ๊ฒ ๊ฐ์๋ฐ ์ flatMap์ ์ฌ์ฉํ๊ฑด๊ฐ ํ๋๋ฐ, ๋ณดํต filter + map ์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ์ flatMap ์ ์ฌ์ฉํ๋ฉด ์ฝ๋๋ฅผ ๊น๋ํ๊ณ ๊ฐ๋ ์ฑ ์๊ฒ ๋ง๋ค ์ ์๋ค.
flatMap + switchLatest
SwitchLatest : ๊ฐ์ฅ ์ต๊ทผ์ Observable ์์ ๊ฐ์ ์์ฑํ๊ณ ์ด์ Observable ์ ๊ตฌ๋ ์ ํด์ ํ๋ค.
flatMap ๊ณผ ๊ฐ์ผ๋ ๋ค์ด์ค๋ Sequence ์์ ํด๋น ์์ ๊ฐ์ฅ ์ต๊ทผ์ Observable Sequence ์์๋ง ๊ฐ์ ํผ์ณ์ ๋ด๋ ค์ค๋ค.
๊ฒ์์ฒ๋ผ v, e, l, o, g ๋ฅผ ์ ๋ ฅํ์ ๋ ์ ๊ฒ์์ ์คํํ๊ณ ์์ ๊ฒ์๊ฐ v, ve, vel, velo ๋ ๋ฌด์ํด์ผํ๋ ์ํฉ์ ์ฌ์ฉํ ์ ์๋ค๊ณ ๋ ํ์ง๋ง ๋ช ํํ๊ฒ ๊ฐ์ ์์จ๋ค.
.next(๊ฐ) .error(์๋ฌ)
์ด๋ ๊ฒ ๋ด๋ ค์ค๋๋ก ์ ์ด.let studentScore = student
.flatMapLatest {
$0.score.materialize()
}
์ด๋ฐ ํํ๋ก ๊ฐ์ด ๋ด๋ ค์ค๊ฒ ๋๋ค.
๊ทธ๋ฌ๋ฉด materialize ๋ ๊ฐ์ ์ด๋ป๊ฒ ๋ฐ์์ ์ฐ์ง?
let studentScore = student
.flatMapLatest {
$0.score.materialize()
}
studentScore
.filter {
guard $0.error == nil else {
print($0.error!)
return false
}
return true
}
// error ๋ฅผ ๊ฑฐ๋ฅด๊ณ dematerialize ๋ก ๋ฐ์์ ์ฌ์ฉํ๋ค.
.dematerialize()
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
์ด๋ฐ ์์ผ๋ก dematerialize ํ์ฌ ์ฌ์ฉํ๋ค.
์ค์ ์ํฉ์์ filter, map, flatMap ์ ์ฌ์ฉํด ๋ณด์๋ค. ๊นํ์์ RxSwift ๋ ํฌ์งํ ๋ฆฌ์ ์ต๊ทผ์ ๊ธฐ์ฌํ ์ฌ๋๋ค์ ๋ชฉ๋ก์ ๊ฐ์ ธ์์ ๋ณด์ฌ์ฃผ๋ ํ๋ก์ ํธ๋ฅผ ํ๋ ๋ง๋ค์๋ค.
// ActivityController > fetchEvent
let response = Observable.from([repo])
// urlString ์ URL ๋ก ๋ณํํ๊ณ
.map { urlString -> URL in
return URL(string: "https://api.github.com/repos/\(urlString)/events")!
}
// URL ์ URLRequest ๋ก ๋ณํํ๊ณ
.map { url -> URLRequest in
return URLRequest(url: url)
}
// Request ๋ฅผ ๋ณด๋ด์ response ๋ฅผ ๋ฐ์์จ๋ค.
.flatMap { request -> Observable<(response: HTTPURLResponse, data: Data)> in
return URLSession.shared.rx.response(request: request)
}
.share(replay: 1)
-> ๋ญ์๋ฆฌ์ผ? : flatMap ์ ์์ Observable ์์ ๋์ด์จ ๊ฒ๋ค์ flat + map ํด์ ์๋ก์ด Observable ์ emit ํ๋ค. ๊ทธ๋ฌ๋ฉด, ํ๋์ item ์ ๋ณํํ ์ฌ๋ฌ item ์ ๋ฐฉ์ถํ ์ ์๊ณ , Observable ์ emit ํ๊ธฐ ๋๋ฌธ์ flatMap ๋ค์์ ๋ณ๋ ฌ์ ์ธ ์คํ์ด ๊ฐ๋ฅํ๋ค.
๋ง์ฝ์ ์ด ์์ ์์ repo ๊ฐ ํ๋๊ฐ ์๋๋ผ ์ฌ๋ฌ๊ฐ์๋ค๋ฉด, Request ๋ฅผ ๋ณด๋ด๊ณ ํด๋น request ๋ฅผ ๋ค ๋ฐ์๋๊น์ง ๊ธฐ๋ค๋ฆด ํ์ ์์ด flatMap ์ด ๋น๋๊ธฐ์ ์ผ๋ก response ๋ฅผ ๋ฐ์์ค๊ณ , ๋จผ์ return ๋ Observable(response) ๋ฅผ ๊ฐ์ง๊ณ ์ง์ง๊ณ ๋ณถ๊ณ ์์ผ๋ฉด ๋๋ค. ๊ทธ๋์ repo ๋ฐฐ์ด๊ณผ response ์ ์์๋ ๋ณด์ฅ์ด ์๋๊ธฐ ๋๋ฌธ์ ์์๊ฐ ๋ณด์ฅ๋์ผ ํ๋ฉด concatMap ์ ์ฌ์ฉํ๋ค.
return URLSession.shared.rx.response(request: request)
์ด ์ฝ๋๋ ์๋ฒ์ ์์ฒญ์ ๋ณด๋ด๊ณ ์๋ต์ ๋ฐ์ผ๋ฉด, ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ .next ์ด๋ฒคํธ๋ก ๋ฑ ํ๋ฒ๋ง ๋ฐฉ์ถํ๊ณ complete ๋๋ค. completed ์ดํ์ ๋ค์ ๊ตฌ๋
์ ํ๊ฒ ๋๋ฉด ๋ request ๋ฅผ ๋ณด๋ด๊ณ .. ๋ฐ๊ณ ... ํ๋ ์๊ฐ์ด ๊ฑธ๋ ค์ ๋นํจ์จ์ .
share(replay: 1)
๋ก ๋ง์ง๋ง์ผ๋ก ๋ฐฉ์ถ๋ ์์๋ฅผ ๋ฒํผ์ ๊ฐ์ง๊ณ ์๋ค๊ฐ ์๋ก์ด ๊ตฌ๋
์๊ฐ ์๊ธฐ๋ฉด ๋ณด๋ด์ค๋ค.
share(replay: 1, scope: .whileConnected)
- ๊ตฌ๋
์๊ฐ ์์ด์ง๋ฉด ๋ฒํผ๊ฐ ์ฌ๋ผ์ง.
share(replay: 1, scope: .forever)
- ๊ณ์ ๋ฒํผ๋ฅผ ๊ฐ์ง๊ณ ์์.
response ๋ฅผ ๊ตฌ๋ ํด์, ๋ค ๋ฐ์์ค๋ฉด success ์ธ ๊ฒ ์ค empty ์ธ ๊ฒ์ ์ ์ธํ๊ณ Event ๋ฐฐ์ด๋ก decode ํด์ processEvent ํจ์๋ก ๋๊ธด๋ค (events relay ์ ๋ณด๋)
response
.filter { response, _ in
return 200..<300 ~= response.statusCode
}
// filter + map ์ ์ธ ๊ฒ์ compactMap ์ผ๋ก ํ๋ฐฉ์ ์ฒ๋ฆฌํ๋ค.
.compactMap { _, data -> [Event]? in
return try? JSONDecoder().decode([Event].self, from: data)
}
.subscribe(onNext: { [weak self] newEvents in
self?.processEvents(newEvents)
})
.disposed(by: bag)
// ActivityController.swift
private let eventsFileURL = self.cachedFileURL("events.plist")
// class ๋ฐ๊นฅ์!
func cachedFileURL(_ fileName: String) -> URL {
return FileManager.default
.urls(for: .cachesDirectory, in: .allDomainsMask)
.first!
.appendingPathComponent(fileName)
}
// processEvent ํ๋จ
// json ๋ฐ์์์ decode ํ์ผ๋ฉด ๊ทธ๊ฑธ ๋ค์ encode ํด์ ์ ์ฅํด๋๋ค.
let encoder = JSONEncoder()
if let eventsData = try? encoder.encode(updatedEvents) {
try? eventsData.write(to: eventsFileURL, options: .atomicWrite)
}
// viewDidLoad : refresh() ์ ์
// ์ ์ฅ๋ ๋ฐ์ดํฐ๊ฐ ์์ผ๋ฉด ๋ถ๋ฌ์์ events ์ ๋ณด๋ธ๋ค.
let decoder = JSONDecoder()
if let eventsData = try? Data(contentsOf: eventsFileURL) {
let persistedEvents = try? decoder.decode([Event].self, from: eventsData)
events.accept(persistedEvents)
}
header ์ Last-Modified (์๊ฐ String) ๋ฅผ ์ถ๊ฐํด์ Last-Modified ๋ณด๋ค ๋ ๋ค์ ์์ ๋ (์ ์ ๋ฐ์ง ์์) ์๋ก์ด ์ด๋ฒคํธ๋ง ์์ฒญํ๋๋ก ์ต์ ํํ๋ค. ํด๋น repository ์ ์๋ฌด ์ด๋ฒคํธ๋ ์ผ์ด๋์ง ์์์ผ๋ฉด ๊ทธ๋ฅ ๋น ์๋ต์ ๋ฐ๊ฒ ๋์ด์ ์์์ ์ ์ฝํ ์ ์๋ค.
// ActivityController.swift
private let modifiedFileURL = cachedFileURL("modified.txt")
private let lastModified = BehaviorRelay<String?>(value: nil)
// ActivityContrller > viewDidLoad
if let lastModifiedString = try? String(contentsOf: modifiedFileURL, encoding: .utf8) {
lastModified.accept(lastModifiedString)
}
// fetchEvents
// url -> URLRequest ์ header ๋ฅผ ์ถ๊ฐํ๋ ์ฝ๋ ์์ฑ
.map { [weak self] url -> URLRequest in
var request = URLRequest(url: url)
if let modifiedHeader = self?.lastModified.value {
request.addValue(modifiedHeader, forHTTPHeaderField: "Last-Modified")
}
return request
}
// ์๋ก์ด ๊ตฌ๋
์์ฑ. header ๊ฐ ์๋ ๊ฒ์ ๊ฑฐ๋ฅธ๋ค.
response
.filter { response, _ in
return 200..<400 ~= response.statusCode
}
.flatMap { response, _ -> Observable<String> in
guard let value = response.allHeaderFields["Last-Modified"] as? String else {
return Observable.empty()
}
return Observable.just(value)
}
.subscribe(onNext: { [weak self] modifiedHeader in
guard let self = self else { return }
self.lastModified.accept(modifiedHeader)
try? modifiedHeader.write(to: self.modifiedFileURL, atomically: true, encoding: .utf8)
})
.disposed(by: bag)
๊ฐ์ฅ ์ธ๊ธฐ๊ฐ ๋ง์ 5๊ฐ์ repository ๋ง ๊ฐ์ ธ์์ ๊ฐ repository ์์ ๋ฐ์ํ๋ ์ด๋ฒคํธ๋ค์ ๊ฐ๊ฐ ๊ฐ์ ธ์ด.
func fetchEvents(repo: String) {
// let response = Observable.from(["repo"])
let response = Observable.from(["https://api.github.com/search/repositories?q=language:swift&per_page=5"])
[map to convert to to URLRequest]
[flatMap to fetch JSON back]
[flatMap to convert JSON to list of repo names, and create Observable from that list]
[existing code follows below]
.map { urlString -> URL in
return URL(string: "https://api.github.com/repos/\(urlString)/events")!
}
// ์์ 5 ๋ ํ์งํ ๋ฆฌ๋ฅผ ์์ฒญ
.map { urlString -> URL in
return URL(string: urlString)!
}
.flatMap { request -> Observable<Any> in
let request = URLRequest(url: url)
return URLSession.rx.json(request: request)
}
.flatMap { response -> Observable<String> in
guard let response = response as? [String: Any],
let items = response["items"] as? [[String: Any]] else {
return Observable.empty()
}
return Observable.from(items.map { $0["full_name"] as! String })
}
// ๊ทธ๊ฒ์ ๋ํด ๊ฐ๊ฐ ์ด๋ฒคํธ (5ํ์ด์ง๋ง) ์์ฒญ
.map { urlString -> URL in
return URL(string: "https://api.github.com/repos/\(urlString)/events/per_page=5")!
}
์ถ์ฒ - https://github.com/fimuxd/RxSwift/tree/master/Lectures/07_Transforming%20Operators
https://github.com/fimuxd/RxSwift/blob/master/Lectures/08_Transforming%20Operators%20in%20Practice/Ch.8%20Transforming%20Operators%20in%20Practice.md
๊ธฐ์กด ์ฝ๋์์ flatMap ๋ง ๋์ค๋ฉด ์ด๊ฒ ๋ญ๋ฐ... ํ๊ณ ๋์ ๊ฐ์๋๋ฐ
์ด์ ์กฐ๊ธ์ ๋ญ์ง ์ ๊ฒ ๊ฐ๋ค! ๋ชจ๋ ๊ธ์์ ํ๊ณ ์๋ ๋ง์ด์ง๋ง ์ ์ฉ์.. ใ ใ ํ๋ ค๋ฉด ๋ง์ ์ฐ์ต์ด ํ์ํ ๊ฒ ๊ฐ๋ค. ๊ทธ๋๋ Observable type ์ด Obsevable ์ ํตํด์ ๋ด๋ ค์ฌ ๋ ๋ด๋ ค์จ Observable ์ Element ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์ flatMap ์ ์ฌ์ฉํ๋ ๊ฒ์ด๋ผ๋ ์ ์ ํ์คํ ์ก์ ๊ฒ ๊ฐ๋ค.
Challenge ๊ฐ ์ญ๋๊ธ์ผ๋ก.. ์ดํด๋ ๊ฐ๋๋ฐ... ๋๋ต ๋ฉํด์ง๋ ๋๋์ด์๋ค. ์๊ฐ๋ณด๋ค 7~8 ์ ๊ณต๋ถํ๋ ์ฌ์ด์ ์์ํ ๋ฒ๊ทธ ์ก๋ ์ ๋ฌด๋ฅผ ๊ฝค ๋ฐ์์ small chapter ๋จ์๋ก ๊ฑฐ์ ์ชผ๊ฐ ๊ณต๋ถ๋ฅผ ํด์ ๋ค์ ๋ฐ๋ผ๊ฐ๊ธฐ๊ฐ ํ๋ค์๋ค.