Consuming Web Services
with Swift and Rx
 @gonzalezreal
Forget Alamofire
(at least for 1 hour)
Let's build a
Web API Client
from scratch
→ Model requests using enum
→ Map JSON without any 3rd party library
→ Use RxSwift to compose our API calls
Bordersgithub.com/gonzalezreal/Borders
https://restcountries.eu/rest/v1/name/Germany?
fullText=true
[
{
"name": "Germany",
"borders": [
"AUT",
"BEL",
"CZE",
...
],
"nativeName": "Deutschland",
...
}
]
https://restcountries.eu/rest/v1/alpha?
codes=AUT;BEL;CZE
[
{
"name": "Austria",
"nativeName": "Österreich",
...
},
{
"name": "Belgium",
"nativeName": "België",
...
},
{
"name": "Czech Republic",
"nativeName": "Česká republika",
...
}
]
Modeling the API
https://restcountries.eu/rest/v1/name/Germany?fullText=true
→ GET
→ https://restcountries.eu/rest/v1
→ name/Germany
→ fullText=true
enum Method: String {
case GET = "GET"
...
}
protocol Resource {
var method: Method { get }
var path: String { get }
var parameters: [String: String] { get }
}
Resource → NSURLRequest
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
}
}
enum CountriesAPI {
case Name(name: String)
case AlphaCodes(codes: [String])
}
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(";")]
}
}
}
Demo
Simple JSON decoding
typealias JSONDictionary = [String: AnyObject]
protocol JSONDecodable {
init?(dictionary: JSONDictionary)
}
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
}
struct Country {
let name: String
let nativeName: String
let borders: [String]
}
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] ?? []
}
}
Demo
5 min intro to
RxSwift
An API for asynchronous programming
with observable streams
Observable streams
→ Taps, keyboard events, timers
→ GPS events
→ Video frames, audio samples
→ Web service responses
Observable<Element>
--1--2--3--4--5--6--|
--a--b--a--a--a---d---X
--------JSON-|
---tap-tap-------tap--->
Next* (Error | Completed)?
[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, +)
Array<Element>
↓
Observable<Element>
The API Client
enum APIClientError: ErrorType {
case CouldNotDecodeJSON
case BadStatus(status: Int)
case Other(NSError)
}
NSURLSession
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: configuration)
let task = self.session.dataTaskWithRequest(request) { data, response, error in
// Handle response
}
task.resume()
NSURLSession
&
RxSwift
final class APIClient {
private let baseURL: NSURL
private let session: NSURLSession
init(baseURL: NSURL, configuration: NSURLSessionConfiguration) {
self.baseURL = baseURL
self.session = NSURLSession(configuration: configuration)
}
...
}
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()
}
}
}
Demo
Let's add JSONDecodable
to the mix
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
}
}
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))
}
}
Chaining requests
flatMap
The ViewModel
typealias Border = (name: String, nativeName: String)
class BordersViewModel {
let borders: Observable<[Border]>
...
}
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)
The View(Controller)
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)
}
Questions?
Comments?
@gonzalezreal
https://github.com/gonzalezreal/Borders
http://tinyurl.com/consuming-web-services

Consuming Web Services with Swift and Rx