Skip to content

Instantly share code, notes, and snippets.

@KentarouKanno
Last active April 19, 2020 10:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save KentarouKanno/c2b08e3ac162121a10347097ffc8a0ed to your computer and use it in GitHub Desktop.
Save KentarouKanno/c2b08e3ac162121a10347097ffc8a0ed to your computer and use it in GitHub Desktop.

RxSwift + ViewModelを使用したAPIサンプル

ViewController

  
  import UIKit
import RxSwift
import RxCocoa

final class ViewController: UIViewController {
    
    @IBOutlet weak private var searchBar: UISearchBar!
    @IBOutlet weak private var tableView: UITableView!
    private let disposeBag = DisposeBag()
    
    private let viewModel = WikipediaSearchViewModel(wikipediaAPI: WkipediaDefaultAPI(URLSession: .shared))

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let input = WikipediaSearchViewModel.Input(searchText: searchBar.rx.text.orEmpty.asObservable())
        let output = viewModel.transform(input: input)
        
        output.searchDescription
            .bind(to: navigationItem.rx.title)
            .disposed(by: disposeBag)
        
        output.wikipediaPages.bind(to: tableView.rx.items(cellIdentifier: "Cell")) { index, result, cell in
            cell.textLabel?.text = result.title
            cell.detailTextLabel?.text = result.url.absoluteString
        }
        .disposed(by: disposeBag)

        output.error.subscribe(onNext: { error in
            if let error = error as? URLError,
                error.errorCode == URLError.notConnectedToInternet.rawValue {
                print(error)
            }
        })
        .disposed(by: disposeBag)
    }
}

WikipediaSearchViewModel

import Foundation
import RxSwift
import RxCocoa

protocol ViewModelType {
    associatedtype Input
    associatedtype Output
    func transform(input: Input) -> Output
}

final class WikipediaSearchViewModel {
    
    private let disposeBag = DisposeBag()
    private let wikipediaAPI: WikipediaAPI
    private let scheduler: SchedulerType
    
    init(wikipediaAPI: WikipediaAPI,
         scheduler: SchedulerType = ConcurrentMainScheduler.instance) {
        self.wikipediaAPI = wikipediaAPI
        self.scheduler = scheduler
    }
}

extension WikipediaSearchViewModel: ViewModelType {
    
    struct Input {
        let searchText: Observable<String>
    }
    
    struct Output {
        let wikipediaPages: Observable<[WikipediaPage]>
        let searchDescription: Observable<String>
        let error: Observable<Error>
    }
    
    func transform(input: Input) -> Output {
        let filterdText = input.searchText.debounce(.milliseconds(300), scheduler: scheduler).share(replay: 1, scope: .forever)
        
        let sequence = filterdText
            .filter { !$0.isEmpty }
            .flatMap { [unowned self] text -> Observable<Event<[WikipediaPage]>> in
                return self.wikipediaAPI
                    .search(from: text)
                    .materialize()
        }.share(replay: 1, scope: .forever)
       
        let wikipediaPages = sequence.flatMap { $0.element.map(Observable.just) ?? .empty() }
        let _searchDescription = PublishSubject<String>()
        
        wikipediaPages.withLatestFrom(filterdText) { (pages, word) -> String in
            return "\(word) \(pages.count)"
        }
        .bind(to: _searchDescription)
        .disposed(by: disposeBag)
        
        let error = sequence.flatMap { $0.error.map(Observable.just) ?? .empty() }
        
        return Output(wikipediaPages: wikipediaPages,
                      searchDescription: _searchDescription.asObserver(),
                      error: error)
    }
}

WikipediaAPI

import Foundation
import RxSwift

protocol WikipediaAPI {
    func search(from word: String) -> Observable<[WikipediaPage]>
}

final class WkipediaDefaultAPI: WikipediaAPI {
    
    private let host = URL(string: "https://ja.wikipedia.org")!
    private let path = "/w/api.php"
    private let URLSession: Foundation.URLSession
    
    init(URLSession: Foundation.URLSession) {
        self.URLSession = URLSession
    }
    
    func search(from word: String) -> Observable<[WikipediaPage]> {
        
        var components = URLComponents(url: host, resolvingAgainstBaseURL: false)!
        components.path = path
        
        let items = [
            URLQueryItem(name: "format", value: "json"),
            URLQueryItem(name: "action", value: "query"),
            URLQueryItem(name: "list", value: "search"),
            URLQueryItem(name: "srsearch", value: word)
        ]

        components.queryItems = items
        
        let request = URLRequest(url: components.url!)
        return URLSession.rx.response(request: request)
            .map { pair in
                do {
                    let response = try JSONDecoder().decode(WikipediaSearchResponse.self, from: pair.data)
                    return response.query.search
                } catch {
                    throw error
                }
        }
    }
}

WikipediaPage

import Foundation

struct WikipediaSearchResponse: Decodable {
    let query: Query
    
    struct Query: Decodable {
        let search: [WikipediaPage]
    }
}

struct WikipediaPage {
    let id: String
    let title: String
    let url: URL
}

extension WikipediaPage: Decodable {
    private enum CodingKeys: String, CodingKey {
        case id = "pageid"
        case title
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = String(try container.decode(Int.self, forKey: .id))
        self.title = try container.decode(String.self, forKey: .title)
        self.url = URL(string: "https://ja.wiipedia.org/w/index.php?curid=\(id)")!
    }
}

extension WikipediaPage: Equatable {
    static func == (lhs: WikipediaPage, rhs: WikipediaPage) -> Bool {
        return lhs.id == rhs.id
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment