52
Consuming Web Services with Swift and Rx  @gonzalezreal

Consuming Web Services with Swift and Rx

Embed Size (px)

Citation preview

Page 1: Consuming Web Services with Swift and Rx

Consuming Web Serviceswith Swift and Rx

 @gonzalezreal

Page 2: Consuming Web Services with Swift and Rx

Forget Alamofire

Page 3: Consuming Web Services with Swift and Rx

(at least for 1 hour)

Page 4: Consuming Web Services with Swift and Rx

Let's build aWeb API Client

from scratch

Page 5: Consuming Web Services with Swift and Rx

→ Model requests using enum→ Map JSON without any 3rd party library→ Use RxSwift to compose our API calls

Page 6: Consuming Web Services with Swift and Rx

Bordersgithub.com/gonzalezreal/Borders

Page 7: Consuming Web Services with Swift and Rx

https://restcountries.eu/rest/v1/name/Germany?fullText=true

Page 8: Consuming Web Services with Swift and Rx

[ { "name": "Germany", "borders": [ "AUT", "BEL", "CZE", ... ], "nativeName": "Deutschland", ... }]

Page 9: Consuming Web Services with Swift and Rx

https://restcountries.eu/rest/v1/alpha?codes=AUT;BEL;CZE

Page 10: Consuming Web Services with Swift and Rx

[ { "name": "Austria", "nativeName": "Österreich", ... }, { "name": "Belgium", "nativeName": "België", ... }, { "name": "Czech Republic", "nativeName": "Česká republika", ... }]

Page 11: Consuming Web Services with Swift and Rx

Modeling the API

Page 12: Consuming Web Services with Swift and Rx

https://restcountries.eu/rest/v1/name/Germany?fullText=true

→ GET

→ https://restcountries.eu/rest/v1

→ name/Germany

→ fullText=true

Page 13: Consuming Web Services with Swift and Rx

enum Method: String { case GET = "GET" ...}

protocol Resource { var method: Method { get } var path: String { get } var parameters: [String: String] { get }}

Page 14: Consuming Web Services with Swift and Rx

Resource → NSURLRequest

Page 15: Consuming Web Services with Swift and Rx

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

Page 16: Consuming Web Services with Swift and Rx

enum CountriesAPI { case Name(name: String) case AlphaCodes(codes: [String])}

Page 17: Consuming Web Services with Swift and Rx

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(";")] } }}

Page 18: Consuming Web Services with Swift and Rx

Demo

Page 19: Consuming Web Services with Swift and Rx

Simple JSON decoding

Page 20: Consuming Web Services with Swift and Rx
Page 21: Consuming Web Services with Swift and Rx

typealias JSONDictionary = [String: AnyObject]

protocol JSONDecodable { init?(dictionary: JSONDictionary)}

Page 22: Consuming Web Services with Swift and Rx

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}

Page 23: Consuming Web Services with Swift and Rx

struct Country { let name: String let nativeName: String let borders: [String]}

Page 24: Consuming Web Services with Swift and Rx

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] ?? [] }}

Page 25: Consuming Web Services with Swift and Rx

Demo

Page 26: Consuming Web Services with Swift and Rx

5 min intro toRxSwift

Page 27: Consuming Web Services with Swift and Rx

An API for asynchronous programming with observable streams

Page 28: Consuming Web Services with Swift and Rx

Observable streams

→ Taps, keyboard events, timers→ GPS events

→ Video frames, audio samples→ Web service responses

Page 29: Consuming Web Services with Swift and Rx

Observable<Element>

Page 30: Consuming Web Services with Swift and Rx

--1--2--3--4--5--6--|--a--b--a--a--a---d---X

--------JSON-|---tap-tap-------tap--->

Page 31: Consuming Web Services with Swift and Rx

Next* (Error | Completed)?

Page 32: Consuming Web Services with Swift and Rx

[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, +)

Page 33: Consuming Web Services with Swift and Rx

Array<Element>↓

Observable<Element>

Page 34: Consuming Web Services with Swift and Rx

The API Client

Page 35: Consuming Web Services with Swift and Rx

enum APIClientError: ErrorType { case CouldNotDecodeJSON case BadStatus(status: Int) case Other(NSError)}

Page 36: Consuming Web Services with Swift and Rx

NSURLSession

let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()let session = NSURLSession(configuration: configuration)

let task = self.session.dataTaskWithRequest(request) { data, response, error in // Handle response}

task.resume()

Page 37: Consuming Web Services with Swift and Rx

NSURLSession&

RxSwift

Page 38: Consuming Web Services with Swift and Rx

final class APIClient { private let baseURL: NSURL private let session: NSURLSession

init(baseURL: NSURL, configuration: NSURLSessionConfiguration) { self.baseURL = baseURL self.session = NSURLSession(configuration: configuration) } ...}

Page 39: Consuming Web Services with Swift and Rx

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

Page 40: Consuming Web Services with Swift and Rx

Demo

Page 41: Consuming Web Services with Swift and Rx

Let's add JSONDecodable to the mix

Page 42: Consuming Web Services with Swift and Rx

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

Page 43: Consuming Web Services with Swift and Rx

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

Page 44: Consuming Web Services with Swift and Rx

Chaining requests

Page 45: Consuming Web Services with Swift and Rx

flatMap

Page 46: Consuming Web Services with Swift and Rx
Page 47: Consuming Web Services with Swift and Rx

The ViewModel

Page 48: Consuming Web Services with Swift and Rx

typealias Border = (name: String, nativeName: String)

class BordersViewModel { let borders: Observable<[Border]> ...}

Page 49: Consuming Web Services with Swift and Rx

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)

Page 50: Consuming Web Services with Swift and Rx

The View(Controller)

Page 51: Consuming Web Services with Swift and Rx

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

Page 52: Consuming Web Services with Swift and Rx

Questions?Comments?@gonzalezreal

https://github.com/gonzalezreal/Bordershttp://tinyurl.com/consuming-web-services