관리 메뉴

김종권의 iOS 앱 개발 알아가기

[RxSwift] 8. Transforming Operators 실전(RESTful API) 본문

RxSwift/RxSwift 기본

[RxSwift] 8. Transforming Operators 실전(RESTful API)

jake-kim 2020. 6. 4. 19:26

1. URL API REQ

1) map을 이용하여 원소에 접근하여 최종적으로 URLRequest획득

// viewDidLoad().swoft
DispatchQueue.global(qos: .default).async { [weak self] in
    let response = Observable.from([repo])
    .map { urlString -> URL in
      return URL(string: "https://api.github.com/repos/\(urlString)/events")!
    }.map { url -> URLRequest in
      return URLRequest(url: url)
    }
}

2) flatMap을 이용하여 새로운 Observable생성

 *map이 아닌 flatMap을 사용하는 이유

- 비동기적으로 작동하는 observable을 통해 효과적으로 "대기"할수 있도록 함(==그 동안 다른 연결들은 계속 작동되게끔) 

   (웹의 response를 기다리기 위함)

- 문자열 또는 숫자 array들의 observable 같이 일시적으로 요소를 방출하고 완료된 observable들을 flatten

※ map vs flatMap : map은 이벤트를 변경, flatMap은 새로운 Observable을 생성하여 변경, 자세한 내용은 여기 참고

.flatMap { request -> Observable<(response: HTTPURLResponse, data: Data)> in
     return URLSession.shared.rx.response(request: request)
 }

- RxCocoa의 URLSession객체 내에 있는 response(request:)를 이용(RES를 서버로 부터 받으면 .next이벤트를 한 번 발생)

- response(request:)의 반환 값 : 앱이 웹 서버를 통해 full response를 받을 때 마다 Observable<(response: HTTPURLResponse, data:

   Data)>)를 반환

 - flatMap이 REQ이고 그의 반환값이 RES라고 생각

 

3) .share(replay:scope:)

- share이 없을경우 문제 : 위의 코드로 RES를 받게되면 .nextf를 emit할 때 1회성이므로 새로운 subscribed observer들은 .next이벤트를 받지 않음

- replay : replay로 emit한 요소를 가지고 있다가 새로운 subscriber가 생길 때 이를 제공

- scope : .whileConnected (n/w response buffer를 아무도 subscribe하고 있지 않을때까지 가지고 있는 것)

              .forever (n/w response buffer를 계속 가지고 있는 것, 새로운 subscriber는 buffer response를 갖음)

.share(replay: 1, scope: .whileConnected)

※ .share은 complete할 것으로 예상되는 sequence에 사용해야함(observable이 다시 생성되는 것을 방지할 수 있기 때문)

 

* REQ & RES, full code : map과 flatMap으로 표현

DispatchQueue.global(qos: .default).async { [weak self] in
    let response = Observable.from([repo])
    .map { urlString -> URL in
      return URL(string: "https://api.github.com/repos/\(urlString)/events")!
    }.map { url -> URLRequest in
      return URLRequest(url: url)
    }.flatMap { request -> Observable<(response: HTTPURLResponse, data: Data)> in
      return URLSession.shared.rx.response(request: request)
    } .share(replay: 1, scope: .whileConnected)
}

※ REQ에 대한 오류처리는 14챕터에서 확인

 

2. RES

1) response observable에 대한 구독을 만들어서 리스폰스 데이터를 객체로 변환

 response
     .filter { response, _ in
         return 200 ..< 300 ~= response.statusCode
 }

2) 서버로 부터 받은 JSON 형식의 데이터를 [Event]로 변환

- [[String:Any]]

- 데이터 디코드 : JOSNDecoder이용

.map { _, data -> [Event] in
  let decoder = JSONDecoder()
  let events = try? decoder.decode([Event].self, from: data)
  return events ?? []
}

3) 이벤트 객체가 존재하는 것만 filter

.filter { objects in
    return !objects.isEmpty
}

4) 구독 요청

- self?.processEvents(newEvents)메소드는 아직 구현 x

.subscribe(onNext: { [weak self] newEvents in
        self?.processEvents(newEvents)
      })
        .disposed(by: self?.bag ?? DisposeBag())

3. Reponse된 데이터 처리

- processEvents(newEvent:)처리

// ActivityController.swift, processEvents(_:)
// private let events = BehaviorRelay<[Event]>(value: [])

var updatedEvents = events.value + newEvents
  if updatedEvents.count > 50 {
    updatedEvents = [Event](updatedEvents.prefix(upTo: 50))
  }

events.accept(updatedEvents)

// tableView.reloadData()는 오직 메인쓰레드에서만 가능
DispatchQueue.main.async {
  self.tableView.reloadData()
}

결과

4. refresh 설정 (새로고침)

UITableView를 구현한 클래스에서, self.refreshControl사용

1) refreshControl 객체

// viewDidLoad()
self.refreshControl = UIRefreshControl()
let refreshControl = self.refreshControl!

refreshControl.backgroundColor = UIColor(white: 0.98, alpha: 1.0)
refreshControl.tintColor = UIColor.darkGray
refreshControl.attributedTitle = NSAttributedString(string: "Pull to refresh")
refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged)

refresh() // fetchData하는 메소드

2) 업데이트가 된 경우 애니메이션 삭제

- endRefreshhing()

// processEvents()
// self.tableView.reloadData() 밑에다 바로 적을 것
self.refreshControl?.endRefreshing()

5. .plist에 이벤트에 대한 정보 저장

디스크에서 파일을 읽고 표현하고 저장

1) .plist를 생성하는 함수 정의

// ActivityController.swift

func cachedFileURL(_ fileName: String) -> URL {
  return FileManager.default
    .urls(for: .cachesDirectory, in: .allDomainsMask)
    .first!
    .appendingPathComponent(fileName)
}

 

2) 파일 생성

// heap 영역

var eventsFileURL : URL {
  return self.cachedFileURL("events.json")
}

 

3) JSON을 Data형 변환한 후 disk에 저장

- processEvents(_:)에 추가

let encoder = JSONEncoder()
if let eventsData = try? encoder.encode(updatedEvents) {
  try? eventsData.write(to: eventsFileURL, options: .atomicWrite)
}

6. 수정된 시간정보를 header에 넣어서 REQ

modified date 정보를 넣은 REQ & RES

1) modified 변수

// heap

var modifiedFileURL : URL {
  return cachedFileURL("modified.txt")
}

let lastModified = BehaviorRelay<String?>(value: nil)

<chachedFileURL>

// ActivityController.swift

func cachedFileURL(_ fileName: String) -> URL {
  return FileManager.default
    .urls(for: .cachesDirectory, in: .allDomainsMask)
    .first!
    .appendingPathComponent(fileName)
}

 

2) lastModified에 이벤트 추가

// viewDidLoad()

if let lastModifiedString = try? String(contentsOf: modifiedFileURL, encoding: .utf8) 
  lastModified.accept(lastModifiedString)
}

// refresh() 호출 바로 위

3) fetchEvents()안의 response에 필터 및 observable.just

response
  .filter { response, _ in
    return 200..<400 ~= response.statusCode
  }

4) flatMap을 통해 Response 받음 (예전과 동일)

.flatMap { response, _ -> Observable<String> in
  guard let value = response.allHeaderFields["Last-Modified"] as? String else {
    return Observable.empty()
  }
  return Observable.just(value)
}

5) 구독 요청

.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: self?.bag ?? DisposeBag())

6) request에 modifiedHeader정보 추가

- 1번을 2번으로 교체

// fetchEvents(repo:)

// 1번
.map { url -> URLRequest in
  return URLRequest(url: url)
}

// 2번
.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
}

* 이점

 - 최신 데이터에만 관심(request, response), 최신 데이터만 update

'RxSwift > RxSwift 기본' 카테고리의 다른 글

[RxSwift] 10. share Operators  (0) 2020.06.24
[RxSwift] 9. Combining Operators  (0) 2020.06.05
[RxSwift] 7.Transforming Operators  (0) 2020.06.01
[RxSwift] 6.Filtering Operators 실습  (0) 2020.05.29
[RxSwift] 5.Filtering Operators  (0) 2020.05.26
Comments