REST API Models in Swift

by

There are a lot of APIs out there, a lot of networking layers, a lot of abstractions, I’m going to offer just one way to start building Swift models backed by a RESTful API. Out of personal preference, PromiseKit will be used instead of callbacks and ObjectMapper will be used to convert between JSON and Swift objects.

A REST API

Want to code along? Let’s setup a simple REST API on your computer.

mkdir tmp
cd tmp
echo "{\"email\":\"garret@garret.com\", \"name\":\"Garret\"}" >> me.json
python -m SimpleHTTPServer 8000

If that worked, you should be able to view the JSON at http://localhost:8000/me.json

The Swift

Get the code here: https://github.com/griddle/blog-swift-rest-api.

First, we need to import the dependencies, which will be explained below.

import Alamofire
import ObjectMapper
import AlamofireObjectMapper
import PromiseKit

We’ll start building by creating a Mappable user model.

class User: Mappable {
  var name: String!
  var email: String!

  init() {
  }

  convenience required init?(map: Map) {
    self.init()
  }

  func mapping(map: Map) {
    name <- map["name"]
    email <- map["email"]
  }
}

You can learn more about ObjectMapper and Mappable objects here: https://github.com/Hearst-DD/ObjectMapper.

The basic idea is now you can take JSON and convert it into a User object with let user = User(JSONString: JSONString). And you can convert that user object back to JSON with user.toJSONString().

To take this a step further, the library AlamofireObjectMapper will convert JSON retrieved through the Alamofire networking library to any Mappable type you provide. It allows us to write a method on a User model like this:

class User: Mappable {
  ...
  static var me: Promise {
    return Promise { fulfill, reject in
      Alamofire.request(
        "http://localhost:8000/me.json",
        method: .get
      )
      .validate(statusCode: 200..<300)
      .responseObject { (response: DataResponse) in
        switch response.result {
        case .success:
          fulfill(response.result.value!)
        case .failure(let error):
          reject(error)
        }
      }
    }
  }
  ...
}

If you’re cringing at how everything is hard-coded, I’ll fix that later. But right now, we can already say:

User.me.then{ user -> Void in
  // do something with our user
}

The me method could easily be abstracted back to support more URLs, methods, parameters, headers (including auth), and it could even support any Type. For simplicity, I’m going to skip headers and assume no authentication is required with the API. Here’s an example BaseAPIModel protocol:

protocol BaseAPIModel: Mappable {
}
extension BaseAPIModel {
  static func api(
    _ path: String,
    method: HTTPMethod = .get,
    parameters: Parameters? = nil
    ) -> Promise
  {
    let url = "http://localhost:8000" + path

    return Promise { fulfill, reject in
      Alamofire.request(
        url,
        method: method,
        parameters: parameters
      )
      .validate(statusCode: 200..<300)
      .responseObject { (response: DataResponse) in
        switch response.result {
        case .success:
          fulfill(response.result.value!)
        case .failure(let error):
          reject(error)
        }
      }
    }
  }
}

Now our User model is back to its original Mappable definition, but instead of conforming to Mappable it will conform to BaseAPIModel. And it will leverage the new api method:

class User: BaseAPIModel {
  ...
  static var me: Promise {
    return User.api("/me.json")
  }
}

And hopefully you can see how this could easily start applying to more methods and models. For example, you might have a Dog model:

class Dog {
  ...
  func update() -> Promise {
    return Dog.api("/dogs.json", method: .post, parameters: self.toJSON())
  }
  static func get(_ id: Int) -> Promise {
    return Dog.api("/dogs/\(id).json")
  }
  static func create(parameters: HTTPParameters) -> Promise {
    return Dog.api("/dogs.json", method: .post, parameters: parameters)
  }
}

With simple models encapsulating all the work to map to the REST API, you can go crazy in your controllers without getting messy.

class DogViewController: UIViewController {
     ...
     var dog: Dog! {
       didSet {
         // updateUI for dog
       }
     }

     override func viewDidLoad() {
       super.viewDidLoad()
       Dog.get(10).then { _dog in
         dog = _dog
       }
     }

     @IBAction func saveDog(_ sender: Any) {
       dog.name = nameInput.text
       dog.update().then { _ in
         print("dog updated!")
       }
     }

}

Leave a Reply

Your email address will not be published. Required fields are marked