Slide 1

Slide 1 text

Consuming Web Services with Swift and Rx @gonzalezreal

Slide 2

Slide 2 text

Forget Alamofire

Slide 3

Slide 3 text

(at least for 1 hour)

Slide 4

Slide 4 text

Let's build a Web API Client from scratch

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

Borders github.com/gonzalezreal/Borders

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

Modeling the API

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

enum Method: String { case GET = "GET" ... } protocol Resource { var method: Method { get } var path: String { get } var parameters: [String: String] { get } }

Slide 14

Slide 14 text

Resource → NSURLRequest

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

Demo

Slide 19

Slide 19 text

Simple JSON decoding

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

typealias JSONDictionary = [String: AnyObject] protocol JSONDecodable { init?(dictionary: JSONDictionary) }

Slide 22

Slide 22 text

func decode(dictionaries: [JSONDictionary]) -> [T] { return dictionaries.flatMap { T(dictionary: $0) } } func decode(data: NSData) -> [T]? { guard let JSONObject = try? NSJSONSerialization.JSONObjectWithData(data, options: []), dictionaries = JSONObject as? [JSONDictionary], objects: [T] = decode(dictionaries) else { return nil } return objects }

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Demo

Slide 26

Slide 26 text

5 min intro to RxSwift

Slide 27

Slide 27 text

An API for asynchronous programming with observable streams

Slide 28

Slide 28 text

Observable streams → Taps, keyboard events, timers → GPS events → Video frames, audio samples → Web service responses

Slide 29

Slide 29 text

Observable

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Next* (Error | Completed)?

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

Array ↓ Observable

Slide 34

Slide 34 text

The API Client

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

NSURLSession let configuration = NSURLSessionConfiguration.defaultSessionConfiguration() let session = NSURLSession(configuration: configuration) let task = self.session.dataTaskWithRequest(request) { data, response, error in // Handle response } task.resume()

Slide 37

Slide 37 text

NSURLSession & RxSwift

Slide 38

Slide 38 text

final class APIClient { private let baseURL: NSURL private let session: NSURLSession init(baseURL: NSURL, configuration: NSURLSessionConfiguration) { self.baseURL = baseURL self.session = NSURLSession(configuration: configuration) } ... }

Slide 39

Slide 39 text

private func data(resource: Resource) -> Observable { 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() } } }

Slide 40

Slide 40 text

Demo

Slide 41

Slide 41 text

Let's add JSONDecodable to the mix

Slide 42

Slide 42 text

func objects(resource: Resource) -> Observable<[T]> { return data(resource).map { data in guard let objects: [T] = decode(data) else { throw APIClientError.CouldNotDecodeJSON } return objects } }

Slide 43

Slide 43 text

extension APIClient { func countryWithName(name: String) -> Observable { return objects(CountriesAPI.Name(name: name)).map { $0[0] } } func countriesWithCodes(codes: [String]) -> Observable<[Country]> { return objects(CountriesAPI.AlphaCodes(codes: codes)) } }

Slide 44

Slide 44 text

Chaining requests

Slide 45

Slide 45 text

flatMap

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

The ViewModel

Slide 48

Slide 48 text

typealias Border = (name: String, nativeName: String) class BordersViewModel { let borders: Observable<[Border]> ... }

Slide 49

Slide 49 text

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)

Slide 50

Slide 50 text

The View(Controller)

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

Questions? Comments? @gonzalezreal https://github.com/gonzalezreal/Borders http://tinyurl.com/consuming-web-services