Upload
guillermo-gonzalez
View
886
Download
3
Embed Size (px)
Citation preview
Consuming Web Serviceswith Swift and Rx
@gonzalezreal
Forget Alamofire
(at least for 1 hour)
Let's build aWeb API Client
from scratch
→ Model requests using enum→ Map JSON without any 3rd party library→ Use RxSwift to compose our API calls
Bordersgithub.com/gonzalezreal/Borders
https://restcountries.eu/rest/v1/name/Germany?fullText=true
[ { "name": "Germany", "borders": [ "AUT", "BEL", "CZE", ... ], "nativeName": "Deutschland", ... }]
https://restcountries.eu/rest/v1/alpha?codes=AUT;BEL;CZE
[ { "name": "Austria", "nativeName": "Österreich", ... }, { "name": "Belgium", "nativeName": "België", ... }, { "name": "Czech Republic", "nativeName": "Česká republika", ... }]
Modeling the API
https://restcountries.eu/rest/v1/name/Germany?fullText=true
→ GET
→ https://restcountries.eu/rest/v1
→ name/Germany
→ fullText=true
enum Method: String { case GET = "GET" ...}
protocol Resource { var method: Method { get } var path: String { get } var parameters: [String: String] { get }}
Resource → NSURLRequest
extension Resource { func requestWithBaseURL(baseURL: NSURL) -> NSURLRequest { let URL = baseURL.URLByAppendingPathComponent(path)
guard let components = NSURLComponents(URL: URL, resolvingAgainstBaseURL: false) else { fatalError("...") }
components.queryItems = parameters.map { NSURLQueryItem(name: String($0), value: String($1)) }
guard let finalURL = components.URL else { fatalError("...") }
let request = NSMutableURLRequest(URL: finalURL) request.HTTPMethod = method.rawValue
return request }}
enum CountriesAPI { case Name(name: String) case AlphaCodes(codes: [String])}
extension CountriesAPI: Resource {
var path: String { switch self { case let .Name(name): return "name/\(name)" case .AlphaCodes: return "alpha" } }
var parameters: [String: String] { switch self { case .Name: return ["fullText": "true"] case let .AlphaCodes(codes): return ["codes": codes.joinWithSeparator(";")] } }}
Demo
Simple JSON decoding
typealias JSONDictionary = [String: AnyObject]
protocol JSONDecodable { init?(dictionary: JSONDictionary)}
func decode<T: JSONDecodable>(dictionaries: [JSONDictionary]) -> [T] { return dictionaries.flatMap { T(dictionary: $0) }}
func decode<T: JSONDecodable>(data: NSData) -> [T]? { guard let JSONObject = try? NSJSONSerialization.JSONObjectWithData(data, options: []), dictionaries = JSONObject as? [JSONDictionary], objects: [T] = decode(dictionaries) else { return nil }
return objects}
struct Country { let name: String let nativeName: String let borders: [String]}
extension Country: JSONDecodable { init?(dictionary: JSONDictionary) { guard let name = dictionary["name"] as? String, nativeName = dictionary["nativeName"] as? String else { return nil }
self.name = name self.nativeName = nativeName self.borders = dictionary["borders"] as? [String] ?? [] }}
Demo
5 min intro toRxSwift
An API for asynchronous programming with observable streams
Observable streams
→ Taps, keyboard events, timers→ GPS events
→ Video frames, audio samples→ Web service responses
Observable<Element>
--1--2--3--4--5--6--|--a--b--a--a--a---d---X
--------JSON-|---tap-tap-------tap--->
Next* (Error | Completed)?
[1, 2, 3, 4, 5, 6].filter { $0 % 2 == 0 }
[1, 2, 3, 4, 5, 6].map { $0 * 2 }
[1, 2, 3, 5, 5, 6].reduce(0, +)
Array<Element>↓
Observable<Element>
The API Client
enum APIClientError: ErrorType { case CouldNotDecodeJSON case BadStatus(status: Int) case Other(NSError)}
NSURLSession
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()let session = NSURLSession(configuration: configuration)
let task = self.session.dataTaskWithRequest(request) { data, response, error in // Handle response}
task.resume()
NSURLSession&
RxSwift
final class APIClient { private let baseURL: NSURL private let session: NSURLSession
init(baseURL: NSURL, configuration: NSURLSessionConfiguration) { self.baseURL = baseURL self.session = NSURLSession(configuration: configuration) } ...}
private func data(resource: Resource) -> Observable<NSData> { let request = resource.requestWithBaseURL(baseURL)
return Observable.create { observer in let task = self.session.dataTaskWithRequest(request) { data, response, error in
if let error = error { observer.onError(APIClientError.Other(error)) } else { guard let HTTPResponse = response as? NSHTTPURLResponse else { fatalError("Couldn't get HTTP response") }
if 200 ..< 300 ~= HTTPResponse.statusCode { observer.onNext(data ?? NSData()) observer.onCompleted() } else { observer.onError(APIClientError.BadStatus(status: HTTPResponse.statusCode)) } } }
task.resume()
return AnonymousDisposable { task.cancel() } } }
Demo
Let's add JSONDecodable to the mix
func objects<T: JSONDecodable>(resource: Resource) -> Observable<[T]> { return data(resource).map { data in guard let objects: [T] = decode(data) else { throw APIClientError.CouldNotDecodeJSON }
return objects }}
extension APIClient { func countryWithName(name: String) -> Observable<Country> { return objects(CountriesAPI.Name(name: name)).map { $0[0] } }
func countriesWithCodes(codes: [String]) -> Observable<[Country]> { return objects(CountriesAPI.AlphaCodes(codes: codes)) }}
Chaining requests
flatMap
The ViewModel
typealias Border = (name: String, nativeName: String)
class BordersViewModel { let borders: Observable<[Border]> ...}
self.borders = client.countryWithName(countryName) // Get the countries corresponding to the alpha codes // specified in the `borders` property .flatMap { country in client.countriesWithCodes(country.borders) } // Catch any error and print it in the console .catchError { error in print("Error: \(error)") return Observable.just([]) } // Transform the resulting countries into [Border] .map { countries in countries.map { (name: $0.name, nativeName: $0.nativeName) } } // Make sure events are delivered in the main thread .observeOn(MainScheduler.instance) // Make sure multiple subscriptions share the side effects .shareReplay(1)
The View(Controller)
private func setupBindings() { ...
viewModel.borders .bindTo(tableView.rx_itemsWithCellFactory) { tableView, index, border in let cell: BorderCell = tableView.dequeueReusableCell() cell.border = border
return cell } .addDisposableTo(disposeBag)}
Questions?Comments?@gonzalezreal
https://github.com/gonzalezreal/Bordershttp://tinyurl.com/consuming-web-services