RxSwift + ViewModelを使用したAPIサンプル(Kickstarter)
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)
}
}
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 }
}
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
}
}
}
}
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
}
}