A very basic example of how to handle the result of a request in Moya looks like this:
provider.request(.zen) { result in
switch result {
case let .success(moyaResponse):
let data = moyaResponse.data // Data, your JSON response is probably in here!
let statusCode = moyaResponse.statusCode // Int - 200, 401, 500, etc
// do something in your app
case let .failure(error):
// TODO: handle the error == best. comment. ever.
}
}
The result of a request is of the type: Result<Moya.Response, MoyaError>
.
Result
is basically an enum with associated values.
A simplified version is:
enum Result {
case success(Moya.Response)
case failure(Error)
}
Therefore we switch over the result.
Moya comes with useful reactive bindings, both for RxSwift
and ReactiveSwift
, which means the above example can be done in these ways as well:
Here is an example for RxSwift
:
provider.rx.request(.zen).subscribe { event in
switch event {
case .success(let response):
// do something with the data
case .error(let error):
// handle the error
}
}
// Or alternatively
provider.rx.request(.zen).subscribe(
onSuccess: { response in
// do something with the data
},
onError: { error in
// handle the error
}
)
The Response
object contains several properties:
data
: the data of the response as aData
object.statusCode
: the status code of the response as anInt
.request
: the request that resulted in the response as aURLRequest?
.response
: the raw response as aHTTPURLResponse?
.
The Response
object also has description
and debugDescription
to help with logging and debugging.
Furthermore it is possible to compare two Response
objects.
Response
objects are equal if status codes, data, and the raw response are equal.
Moya provides several useful extensions to Response
.
You can always extend with your own if necessary.
Usually you want to only handle responses within a limited set of status codes.
Moya has a couple of extensions for common cases: filterSuccessfulStatusCodes()
and filterSuccessfulStatusAndRedirectCodes()
filterSuccessfulStatusCodes()
throws a MoyaError.statusCode(Response)
error if it encounters a status code that is not 200-299.
filterSuccessfulStatusAndRedirectCodes()
will likewise throw the same error if it encounters a status code that is not 200-399.
A basic example is:
provider.request(.zen) { result in
switch result {
case let .success(moyaResponse):
do {
let filteredResponse = try moyaResponse.filterSuccessfulStatusCodes() // gives back a Response or throws an error.
// We know if we get past this that the status code is 200-299.
// Do something with the filteredResponse.
}
catch let error {
// TODO: handle the error == best. comment. ever.
}
case let .failure(error):
// TODO: handle the error == best. comment. ever.
}
}
If you have specific needs there are also the more general:
filter(statusCode: Int)
which only accepts a single status code and throws an error otherwise.filter(statusCodes: ClosedRange<Int>)
which accepts a range of status codes and throws an error if the response's status code doesn't fall within the range.
A basic example is:
provider.request(.zen) { result in
switch result {
case let .success(moyaResponse):
do {
let filteredResponse = try moyaResponse.filter(statusCodes: 200...299) // same as filterSuccessfulStatusCodes
// We know if we get past this that the status code is 200-299.
// Do something with the filteredResponse.
}
catch let error {
// TODO: handle the error == best. comment. ever.
}
case let .failure(error):
// TODO: handle the error == best. comment. ever.
}
}
These extensions are also available in RxSwift
, which makes the result handling a bit simpler:
provider.rx.request(.zen)
.filterSuccessfulStatusCodes()
.subscribe { event in
switch event {
case .success(let response):
// do something with the data
case .error(let error):
// handle the error, which can be an underlying error or a status code error
}
}
}
The benefit of the reactive extensions is that error handling can be done in a central place, rather than having to copy/paste or otherwise handle an error the same way.
Moya also has an extension to map your response into JSON called mapJSON()
.
mapJSON()
takes a single optional parameter (default: true), describing whether it should throw an error when data is empty or simply return NSNull
.
The error thrown is MoyaError.jsonMapping(Response)
.
A basic example:
provider.request(.zen) { result in
switch result {
case let .success(moyaResponse):
do {
let filteredResponse = try moyaResponse.filterSuccessfulStatusCodes()
let json = try filteredResponse.mapJSON() // type Any
// Do something with your json.
}
catch let error {
// Here we get either statusCode error or jsonMapping error.
// TODO: handle the error == best. comment. ever.
}
case let .failure(error):
// TODO: handle the error == best. comment. ever.
}
}
In RxSwift
:
provider.rx.request(.zen)
.filterSuccessfulStatusCodes()
.mapJSON()
.subscribe { event in
switch event {
case .success(let json):
// Notice that now we do not get a Response object anymore but rather the JSON object
// do something with the json
case .error(let error):
// handle the error, which can be an underlying error, a status code error, or an json mapping error
}
}
}
Moya supports extensions for Decodable
.
To understand how this works let us first describe the API and our objects.
Let's assume we have an API with an endpoint /users/:id
, which returns the following data:
{
"id": "jp",
"firstName": "James",
"lastName": "Potter"
}
We create a struct to handle this user on the client side:
struct User: Decodable {
let id: String
let firstName: String
let lastName: String
}
Moya allows us to easily get our User
from the response with the map<D: Decodable>(_: D.Type, atKeyPath: String?, using: JSONDecoder, failsOnEmptyData: Bool)
extension.
Both atKeyPath
and using
are optional, meaning in most cases you'll use map(_:)
.
The failsOnEmptyData
property (default: true), describes whether it should throw an error when data is empty or simply return Decodable
initialized with nil (note: your object must allow optionals or you'll still get thrown an error).
A basic example would be:
provider.request(.user("jp")) { result in
switch result {
case let .success(moyaResponse):
do {
let filteredResponse = try moyaResponse.filterSuccessfulStatusCodes()
let user = try filteredResponse.map(User.self) // user is of type User
// Do something with your user.
}
catch let error {
// Here we get either statusCode error or objectMapping error.
// TODO: handle the error == best. comment. ever.
}
case let .failure(error):
// TODO: handle the error == best. comment. ever.
}
}
In RxSwift
:
provider.rx.request(.user("jp"))
.filterSuccessfulStatusCodes()
.map(User.self)
.subscribe { event in
switch event {
case .success(let user):
// Notice that now we do not get a Response object anymore but rather the User object
// do something with the user
case .error(let error):
// handle the error, which can be an underlying error, a status code error, or an object mapping error
}
}
}
The above assumes your object is always at the root and that everything can be handled by the default JSONDecoder
.
But if it isn't, then it's not too difficult to change.
To show how this is done we will consider another endpoint in our API: /users
.
This endpoint returns a list of users under a key called "users"
.
Futhermore each user now has an updated
property, which is the unix timestamp.
The data returned looks like this:
{
"users": [
{
"id": "jp",
"firstName": "James",
"lastName": "Potter",
"updated": 1507709925 // Unix timestamp
},
{
"id": "lp",
"firstName": "Lily",
"lastName": "Potter",
"updated": 1507709926 // Unix timestamp
}
]
}
Our updated User
type looks like this:
struct User: Decodable {
let id: String
let firstName: String
let lastName: String
let updated: Date
}
Our handling of the result now has to do slightly more:
provider.request(.user) { result in
switch result {
case let .success(moyaResponse):
do {
let filteredResponse = try moyaResponse.filterSuccessfulStatusCodes()
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let users = try filteredResponse.map([User].self, atKeyPath: "users", using: decoder) // user is of type [User]
// Do something with your users.
}
catch let error {
// Here we get either statusCode error or objectMapping error.
// TODO: handle the error == best. comment. ever.
}
case let .failure(error):
// TODO: handle the error == best. comment. ever.
}
}
In RxSwift
this could look something like:
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
provider.rx.request(.users)
.filterSuccessfulStatusCodes()
.map([User].self, atKeyPath: "users", using: decoder)
.subscribe { event in
switch event {
case .success(let users):
// Notice that now we do not get a Response object anymore but rather an array of User objects
// do something with the user
case .error(let error):
// handle the error, which can be an underlying error, a status code error, or an object mapping error
}
}
}
The above assumes your backend always returns data and if it doesn't, throwns an error.
But if you don't want to receive an error, we can set failsOnEmptyData
to false.
The data returned looks like this:
{
"users": []
}
Our updated User
type looks like this:
struct User: Decodable {
let id: String?
let firstName: String?
let lastName: String?
let updated: Date?
}
Our handling of the result now has to do slightly more:
provider.request(.user) { result in
switch result {
case let .success(moyaResponse):
do {
let filteredResponse = try moyaResponse.filterSuccessfulStatusCodes()
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let users = try filteredResponse.map([User].self, atKeyPath: "users", using: decoder, failsOnEmptyData: false) // user is of type [User]
// Because the failsOnEmptyData is false and our user object allows optional, our array got initialized with an empty User object
// Do something with your users.
}
catch let error {
// Here we get either statusCode error or objectMapping error.
// TODO: handle the error == best. comment. ever.
}
case let .failure(error):
// TODO: handle the error == best. comment. ever.
}
}
In RxSwift
this could look something like:
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
provider.rx.request(.users)
.filterSuccessfulStatusCodes()
.map([User].self, atKeyPath: "users", using: decoder, failsOnEmptyData: false)
.subscribe { event in
switch event {
case .success(let users):
// Notice that now we do not get a Response object anymore but rather an array of User objects
// Because the failsOnEmptyData is false and our user object allows optional, our array got initialized with an empty User object
// do something with the user
case .error(let error):
// handle the error, which can be an underlying error, a status code error, or an object mapping error
}
}
}