# [HowTo] RxSwift + RESTful API + MVVM

# URL API REQ

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

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을 이용하여 새로운 Observable생성 👍👍

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

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

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

.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라고 생각

# .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)

# 결론

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)
}

# RES

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

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

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

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

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

.filter { objects in
    return !objects.isEmpty
}

# 구독 요청

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

# 실전

How to use URLSession for the web API calls in reactive way in MVVM architecture.

# JSON

{
  "status": "ok",
  "totalResults": 19821,
  "articles": [
    {
      "source": {
        "id": null,
        "name": "Itc.ua"
      },
      "author": "Сергей Кулеш",
      "title": "Українські розробники презентували на IDEX-2021 плаваючий гусеничний броньований гібридний електромобіль «Шторм» [фото, відео]",
      "description": "Команда українського конструктора бронетехніки Олександра Кузнєцова представила на міжнародній оборонній виставці в Об’єднаних Арабських Еміратах IDEX-2021 свою розробку «Шторм». Це гусенична броньована платформа з гібридною тягою, що здатна розвивати швидкіс…",
      "url": "https://itc.ua/news/ukra%d1%97nski-rozrobniki-prezentuvali-na-idex-2021-plavayuchij-gusenichnij-bronovanij-elektromobil-shtorm-foto-video/",
      "urlToImage": "https://i0.wp.com/itc.ua/wp-content/uploads/2021/02/shtorm.jpg",
      "publishedAt": "2021-02-22T11:20:14Z",
      "content": "IDEX-2021 «».\r\n , 140 / .\r\n« «», . 1,5 . 140 / 18 36 . . , , , . , , », .\r\n, . , , . . , .\r\n. . Streit Group ’ .\r\n« . , 65 , . , Tesla. , , . , . . 70% », .\r\n, , . , .\r\n, 5% , .\r\n« . . , . , . , , «»… [+42 chars]"
    },

    ...

# Model

//
//  Article.swift
//  RxNews
//
//  Created by Ted on 2021/02/22.
//
import Foundation

struct ArticleResponse: Decodable {
    let articles: [Article]
}

struct Article: Decodable {
    let title: String
    let publishedAt: String
}

# ViewModel

//
//  ArticleViewModel.swift
//  RxNews
//
//  Created by Ted on 2021/02/22.
//

import Foundation
import RxSwift
import RxCocoa

struct ArticleListViewModel {
    
    let articlesVM: [ArticleViewModel]
}

extension ArticleListViewModel {
    
    init(_ articles: [Article]) {
        self.articlesVM = articles.compactMap(ArticleViewModel.init)
    }
}

extension ArticleListViewModel {
    
    func articleAt(_ index: Int) -> ArticleViewModel {
        return self.articlesVM[index]
    }
}


struct ArticleViewModel {
    
    let article: Article
    
    init(_ article: Article) {
        self.article = article
    }
}

extension ArticleViewModel {
    
    var title: Observable<String> {
        return Observable<String>.just(article.title)
    }
    
    var description: Observable<String> {
        return Observable<String>.just(article.description ?? "")
    }
}

# URLRequest+Extension

//  URLRequest+Extension.swift
//  RxNews
//
//  Created by Ted on 2021/02/22.
//

import Foundation
import RxSwift
import RxCocoa

struct Resource<T: Decodable> {
    let url: URL
}

extension URLRequest {
    
    static func load<T>(resource: Resource<T>) -> Observable<T> {
        return Observable.just(resource.url)
            .flatMap { url -> Observable<Data> in
            //flatMap make events to another observable
                let request = URLRequest(url: url)
                 //a simple way to make data type with RxSwift
                return URLSession.shared.rx.data(request: request)
            }.map { data -> T in
                return try JSONDecoder().decode(T.self, from: data)
            }
    }
}

# ViewController

//
//  NewsTableViewController.swift
//  RxNews
//
//  Created by Ted on 2021/02/21.
//

import UIKit
import RxSwift
import RxCocoa

...
//MARK: - Properties
private var articleListVM: ArticleListViewModel!

...

//MARK: - Helpers
private func populateNews() {
    
    let resource = Resource<ArticleResponse>(url: URL(string: "")!)
    
    URLRequest.load(resource: resource)
        .subscribe(onNext: { articleResponse in
            
            let articles = articleResponse.articles
            self.articleListVM = ArticleListViewModel(articles)
            
            DispatchQueue.main.async {
                self.tableView.reloadData()
            }
            
        }).disposed(by: disposeBag)
}

... 

extension NewsViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.articleListVM == nil ? 0 : self.articleListVM.articlesVM.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: ReuseIdentifier, for: indexPath) as! ArticleTableViewCell
        
        let articleVM = self.articleListVM.articleAt(indexPath.row)
        
        articleVM.title.asDriver(onErrorJustReturn: "")
            .drive(cell.titleLabel.rx.text)
            .disposed(by: disposeBag)
        
        articleVM.description.asDriver(onErrorJustReturn: "")
            .drive(cell.descriptionLabel.rx.text)
            .disposed(by: disposeBag)
        
        return cell
    }
}

# Info.plist

If you want to allow HTTP connections to any site, you should use this keys:

App Transport Security Settings
    Allow Arbitrary Loads -> Yes

RxNews Github Link (opens new window)