Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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.

Guillermo Gonzalez

March 02, 2016
Tweet

More Decks by Guillermo Gonzalez

Other Decks in Programming

Transcript

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

    3rd party library → Use RxSwift to compose our API calls
  2. [ { "name": "Germany", "borders": [ "AUT", "BEL", "CZE", ...

    ], "nativeName": "Deutschland", ... } ]
  3. [ { "name": "Austria", "nativeName": "Österreich", ... }, { "name":

    "Belgium", "nativeName": "België", ... }, { "name": "Czech Republic", "nativeName": "Česká republika", ... } ]
  4. enum Method: String { case GET = "GET" ... }

    protocol Resource { var method: Method { get } var path: String { get } var parameters: [String: String] { get } }
  5. 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 } }
  6. 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(";")] } } }
  7. 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 }
  8. 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] ?? [] } }
  9. Observable streams → Taps, keyboard events, timers → GPS events

    → Video frames, audio samples → Web service responses
  10. [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, +)
  11. NSURLSession let configuration = NSURLSessionConfiguration.defaultSessionConfiguration() let session = NSURLSession(configuration: configuration)

    let task = self.session.dataTaskWithRequest(request) { data, response, error in // Handle response } task.resume()
  12. final class APIClient { private let baseURL: NSURL private let

    session: NSURLSession init(baseURL: NSURL, configuration: NSURLSessionConfiguration) { self.baseURL = baseURL self.session = NSURLSession(configuration: configuration) } ... }
  13. 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() } } }
  14. 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 } }
  15. 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)) } }
  16. 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)
  17. 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) }