Save 37% off PRO during our Black Friday Sale! »

Consuming Web Services with Swift and Rx

Consuming Web Services with Swift and Rx

Let’s forget Alamofire for a moment and build a web API client from scratch. In the process, we will learn how to model web API requests using an Enum, map JSON without any third-party library and use RxSwift to compose our API calls.

52edc3d953e8df9d23c2943542f151bc?s=128

Guillermo Gonzalez

March 02, 2016
Tweet

Transcript

  1. Consuming Web Services with Swift and Rx @gonzalezreal

  2. Forget Alamofire

  3. (at least for 1 hour)

  4. Let's build a Web API Client from scratch

  5. → Model requests using enum → Map JSON without any

    3rd party library → Use RxSwift to compose our API calls
  6. Borders github.com/gonzalezreal/Borders

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

  8. [ { "name": "Germany", "borders": [ "AUT", "BEL", "CZE", ...

    ], "nativeName": "Deutschland", ... } ]
  9. https://restcountries.eu/rest/v1/alpha? codes=AUT;BEL;CZE

  10. [ { "name": "Austria", "nativeName": "Österreich", ... }, { "name":

    "Belgium", "nativeName": "België", ... }, { "name": "Czech Republic", "nativeName": "Česká republika", ... } ]
  11. Modeling the API

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

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

    protocol Resource { var method: Method { get } var path: String { get } var parameters: [String: String] { get } }
  14. Resource → NSURLRequest

  15. 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 } }
  16. enum CountriesAPI { case Name(name: String) case AlphaCodes(codes: [String]) }

  17. 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(";")] } } }
  18. Demo

  19. Simple JSON decoding

  20. None
  21. typealias JSONDictionary = [String: AnyObject] protocol JSONDecodable { init?(dictionary: JSONDictionary)

    }
  22. 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 }
  23. struct Country { let name: String let nativeName: String let

    borders: [String] }
  24. 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] ?? [] } }
  25. Demo

  26. 5 min intro to RxSwift

  27. An API for asynchronous programming with observable streams

  28. Observable streams → Taps, keyboard events, timers → GPS events

    → Video frames, audio samples → Web service responses
  29. Observable<Element>

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

  31. Next* (Error | Completed)?

  32. [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, +)
  33. Array<Element> ↓ Observable<Element>

  34. The API Client

  35. enum APIClientError: ErrorType { case CouldNotDecodeJSON case BadStatus(status: Int) case

    Other(NSError) }
  36. NSURLSession let configuration = NSURLSessionConfiguration.defaultSessionConfiguration() let session = NSURLSession(configuration: configuration)

    let task = self.session.dataTaskWithRequest(request) { data, response, error in // Handle response } task.resume()
  37. NSURLSession & RxSwift

  38. final class APIClient { private let baseURL: NSURL private let

    session: NSURLSession init(baseURL: NSURL, configuration: NSURLSessionConfiguration) { self.baseURL = baseURL self.session = NSURLSession(configuration: configuration) } ... }
  39. 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() } } }
  40. Demo

  41. Let's add JSONDecodable to the mix

  42. 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 } }
  43. 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)) } }
  44. Chaining requests

  45. flatMap

  46. None
  47. The ViewModel

  48. typealias Border = (name: String, nativeName: String) class BordersViewModel {

    let borders: Observable<[Border]> ... }
  49. 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)
  50. The View(Controller)

  51. 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) }
  52. Questions? Comments? @gonzalezreal https://github.com/gonzalezreal/Borders http://tinyurl.com/consuming-web-services