Nikka
Nikka is a super simple Swift HTTP networking library that comes with many extensions to make it modular and really powerful.
Installation
###Requirements
- iOS 8.0+
- Xcode 8.0+
- Swift 3.0+
CocoaPods
Withuse_frameworks!
pod "Nikka"
Usage
Simple example:
In 99% of the cases your app will need to talk to an API, that API has a behavior and you would like to map that behavior to your app. To handle errors correctly for instance. Nikka has been designed with this in mind. It allows you to define a common behavior for an API, by defining a Provider
. Here's a simple example of what it looks like:
import Nikka
//Define your provider
class MyProvider:HTTPProvider {
var baseURL = URL(string:"https://my-website.com/api")!
}
...
//This will send a GET request to the endpoint https://my-website.com/api/me/friends
MyProvider().request(Route(path:"/me/friends")).responseJSON { (response:Response<Any>) in
//Parse here the object as an array or dictionary
}
What is great with Nikka, is that it's highly scalable, and modular. You can define your endpoints wherever you want. It's up to you, if you want them all in one file or if you prefer to split them among your different services. Here is a nice way of presenting your endpoints and using them:
//Endpoints relative to the user
extension Route {
static let me = Route(path:"/me", method: .get}
static let friends = Route(path:"/me/friends") //GET is the default method when it is not specified
static let user = {(id:String) in Route(path:"/user/\(id)")} //You can pass parameters by defining a closure that will return a Route
static let login = {(email:String, password:String) in Route(path:"/login", method:.post, params:["email":email, "password":password])}
}
...
//Then you can simply send a request by passing the route you defined above.
//This will send a POST request to the endpoint https://my-website.com/api/login with a json body `{"email":"[email protected]","password":"bar"}``
MyProvider().request(.login("[email protected]", "bar")).responseJSON { (response:Response<Any>) in
//Parse here the object as an array or dictionary
}
Routes
Route
is the object that allows you to define an endpoint and how you should talk to it. It requires at least a path that defines where the request is sent. GET
is the default method used. You can pass parameters and headers to be sent with the request. And finally you can define how the parameters should be encoded.
Examples
Here are a few examples of valid routes:
Route(path:"/me") //GET request to the relative path /me without any headers or parameters
Route(path:"/login", method:.post, params:["email":"[email protected]", "password":"qwerty12345"]) //POST request that use the default JSON encoding and will pass the the parameters in the request body.
Route(path:"/user/about", method:.put, params:["text":"Hey!!"], headers:["Authorization":"12345"], encoding:.form) //PUT request that sends its parameters using the form encoding
Nikka currently supports 3 types of encoding, which are json
, form
, and url
.
json
will encode your parameters in JSON and put them in the request bodyform
will encode your parameters as query parameters and put them in the request bodyurl
will encode your parameters as query parameters and append them to the URL
Multipart
A Route also supports multipart form. Here's a simple way to upload a multipart image:
//First you define the Route that will take the image in parameter put it into a Multipart form
extension Route{
static let uploadImage = {(image:UIImage) -> Route in
var form = MultipartForm()
form.append(data: UIImageJPEGRepresentation(image, 0.9)!, forKey: "image", fileName: "image.jpg")
return Route(path:"/profile/picture", method:.post, multipartForm:form)
}
}
...
//Then you just use the route as usual to send the request
let image = UIImage(named:"DSC_0025.JPG")!
MyProvider().request(.uploadImage(image)).uploadProgress { (sent, total) in
print("upload progress: \(sent)/\(total)")
}.responseJSON { (json) in
print("json: \(json)")
}
Provider
The Provider is a type that implements HTTPProvider
and that will map the behavior of your API. It can be highly customized:
Additional parameters and headers
If your API requires some headers or parameters for every request, you can set them in the declaration of the provider.
class MyProvider:HTTPProvider {
var baseURL = URL(string:"https://my-website.com/api")!
var additionalHeaders = ["Locale":"en-US"]
var additionalParameters = ["token":"d71106a0-dd44-4092-a72e"]
}
Validating a response
APIs have a lot of different ways of handling errors. Nikka allows you to create your own errors that propagate it through the app, if you get a response you don't expect.
Let's take for instance the Deezer API, that returns a HTTP code 200 when it cannot find a song with a given ID. In your basic provider, 200 doesn't throw an error, but content of the response can't be parsed either. Here's how to deal with it:
https://api.deezer.com/track/313555658769 will return:
{
"error": {
"type":"DataException",
"message":"no data",
"code":800
}
}
You should first define your own error that conforms to the NikkaError protocol:
struct DeezerError : NikkaError, Equatable{
var description:String
var code:Int
init(code:Int, description:String) {
self.code = code
self.description = description
}
public static func ==(lhs: DeezerError, rhs: DeezerError) -> Bool {
return lhs.code == rhs.code
}
}
Then when declaring your provider, you can implement the validate
method, that will be called when a response is received
class DeezerProvider:HTTPProvider {
var baseURL = URL(string:"https://api.deezer.com")!
func validate(response: HTTPURLResponse, data: Data, error: Error?) -> NikkaError? {
let jsonError = (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)) as? [String:Any]
if let error = json?["error"] as? [String:Any], let code = error["code"] as? Int, let desc = error["message"] as? String {
return DeezerError(code:code, description:desc)
}
return nil
}
}
Then when sending your request, if deezer returns a HTTP code 200 but with a json error in its body. It will go through the validator and send you back the error.
let myProvider = DeezerProvider()
myProvider.request(Route(path:"/track/313555658769").responseJSON { (response:Response<Any>) in
switch response.result{
case .success(let value):
print("success")
case .failure(let error):
print("error: \(error.description)")
}
}
//This will print "error: no data"
Stopping on error
In some cases, it is useful to define a certain behavior when an error is encountered. For instance if you receive a HTTP 401 error you might want to terminate the user session. This can be done by implementing the shouldContinue
method.
class MyProvider:HTTPProvider {
var baseURL:URL { return URL(string:"https://my-website.com/api")!}
func shouldContinue(with error: NikkaError) -> Bool {
if let err = error as? NikkaError , err == NikkaError.http(401){
print("should log out")
return false
}
return true
}
}
Without Provider
In some cases, it doesn't make sense to define a provider because you already have a full URL. You can use the Default Provider for that extent. It allows you to send a request by passing a route with its full path.
DefaultProvider.request(Route(path:"https://my-website.com/api/user/1")).responseJSON { (response:Response<Any>) in
switch response.result{
case .success(let json):
print("json: \(json)")
case .failure(let error):
print("error: \(error)")
}
}
Extensions
Nikka works very well with JSON, it currently supports the libraries below to parse your data.
- Gloss - documentation
- ModelMapper - documentation
- ObjectMapper - documentation
- StreemMapper - documentation
- Unbox - documentation
By using one of those extensions, you'll be able to send a request and get your object right away:
MyProvider().request(Route(path:"/user/1234")).responseObject { (response:Response<User>) in
//You can check the content of the response for a user
let user = response.result.value //This is a User?
//Or you can switch on the response result if you want to manage an error case
switch response.result{
case .success(let user):
print("success: user name is \(user.lastName)")
case .failure(let error):
print("error: \(error)") //Will print an error, if the User cannot be parsed for instance.
}
}
Additionally Nikka supports Futures and RxSwift with extensions that can be used with CocoaPods by adding this to your PodFile:
pod "Nikka/Futures"
pod "Nikka/Rx"
Note that when importing a module, the core dependency is automatically imported as well, so you don't need to have both one of the above and the Nikka single pod.
Futures
Futures come very handy in modern programming, it allows you to chain your requests neatly. The Futures module allows you to return a future when you send a request.
I would encourage you to use the Future module along with a JSON library mentioned above. It is more powerful. However if for some reason you you like to get a Future with a JSON object or with the data return by the request. You could do the following:
//With Data and HTTPURLResponse
let loginDataFuture:Future<(HTTPURLResponse,Data)> = myProvider.request(.login("[email protected]", "bar")).response()
loginDataFuture.onComplete { (result:Result<Any>) in
switch result{
case .success(let json):
print("json: \(json)")
case .failure(let error):
print("error: \(error)")
}
}
//With JSON
let loginJSONFuture:Future<Any> = myProvider.request(.login("[email protected]", "bar")).responseJSON()
loginJSONFuture.onComplete { (result:Result<(HTTPURLResponse, Data)>) in
expectation.fulfill()
switch result{
case .success(let response, let data):
print("response code was: \(response.statusCode)")
case .failure(let error):
print("error: \(error)")
}
}
RxSwift
Even better, the Rx extension. Similarly to the Future extension, it will return Rx Observable that can be chained.
I would encourage you to use the Rx module along with a JSON library mentioned above. It is more powerful. However if for some reason you you like to get a Observable with a JSON object or with the data return by the request. You could do the following:
//With Data and HTTPURLResponse
let loginDataObservable:Observable<(HTTPURLResponse,Data)> = myProvider.request(.login("[email protected]", "bar")).response()
loginDataObservable.subscribe(onNext: { (response:(HTTPURLResponse, Data)) in
print("response code was: \(response.0.statusCode)")
}).addDisposableTo(bag)
//With JSON
let loginJSONObservable:Observable<Any> = myProvider.request(.login("[email protected]", "bar")).responseJSON()
loginJSONObservable.subscribe(onNext: { json in
print("json is: \(json)")
}).addDisposableTo(bag)
Contributing
Contributions are more than welcome. Feel free to submit a pull request to add a new feature or to add support for your favorite JSON library.
License
Nikka is maintained by Emilien Stremsdoerfer and released under the Apache 2.0 license. See LICENSE for details