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.

Avatar for Guillermo Gonzalez

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