Skip to content

Instantly share code, notes, and snippets.

@KentarouKanno
Last active April 19, 2020 11:32
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/613f339ffa22c02da44431b5f6eca70f to your computer and use it in GitHub Desktop.
Save KentarouKanno/613f339ffa22c02da44431b5f6eca70f to your computer and use it in GitHub Desktop.

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

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: WikipediaSearchViewModelType = WikipediaSearchViewModel(wikipediaAPI: WkipediaDefaultAPI(URLSession: .shared))

    override func viewDidLoad() {
        super.viewDidLoad()
        
        searchBar.rx.text.orEmpty
            .subscribe(onNext: { [unowned self]  in
                self.viewModel.input.searchTextChanged($0)
            }).disposed(by: disposeBag)
        
        viewModel.output.searchDescription
            .bind(to: navigationItem.rx.title)
            .disposed(by: disposeBag)
        
        viewModel.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)

        viewModel.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 WikipediaSearchViewModelInput {
    func searchTextChanged(_ searchText: String)
}

protocol WikipediaSearchViewModelOutput {
    var searchDescription: Observable<String> { get }
    var wikipediaPages: Observable<[WikipediaPage]> { get }
    var error: Observable<Error> { get }
}

protocol WikipediaSearchViewModelType {
    var input: WikipediaSearchViewModelInput { get }
    var output: WikipediaSearchViewModelOutput { get }
}

final class WikipediaSearchViewModel: WikipediaSearchViewModelOutput {
    
    private let disposeBag = DisposeBag()
    private let wikipediaAPI: WikipediaAPI
    private let scheduler: SchedulerType
    
    let searchDescription: Observable<String>
    let wikipediaPages: Observable<[WikipediaPage]>
    let error: Observable<Error>
    
    private let searchTextChangedProperty = BehaviorRelay<String>(value: "")
    
    init(wikipediaAPI: WikipediaAPI,
         scheduler: SchedulerType = ConcurrentMainScheduler.instance) {
        
        self.wikipediaAPI = wikipediaAPI
        self.scheduler = scheduler
        
        let _wikipediaPages = PublishRelay<[WikipediaPage]>()
        self.wikipediaPages = _wikipediaPages.asObservable()
        
        let _error = PublishRelay<Error>()
        self.error = _error.asObservable()
        
        let filterdText = searchTextChangedProperty.debounce(.milliseconds(300), scheduler: scheduler).share(replay: 1, scope: .forever)
        let _searchDescription = PublishSubject<String>()
        searchDescription = _searchDescription.asObservable()
        
        let sequence = filterdText
             .flatMapLatest { [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() }
         
         wikipediaPages.withLatestFrom(filterdText) { (pages, word) -> String in
             return "\(word) \(pages.count)"
         }
         .bind(to: _searchDescription)
         .disposed(by: disposeBag)
        
        wikipediaPages.bind(to: _wikipediaPages).disposed(by: disposeBag)
         
        let error = sequence.flatMap { $0.error.map(Observable.just) ?? .empty() }
        error.bind(to: _error).disposed(by: disposeBag)
    }
}

extension WikipediaSearchViewModel: WikipediaSearchViewModelInput {
    func searchTextChanged(_ searchText: String) {
        searchTextChangedProperty.accept(searchText)
    }
}

extension WikipediaSearchViewModel: WikipediaSearchViewModelType {
    var input: WikipediaSearchViewModelInput { return self }
    var output: WikipediaSearchViewModelOutput { return self }
}

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