This code is inspried by Austin Feight's Evolution of a Network Layer. I made it possiable to handle multipart request and deal with response that is ignorable according to my situation.
There is a artical explain why should we wrap network request like this in chinese. We won't go too deep here. If you don't understand what we are doing here. I sugguest you to read this artical first or Evolution of A Networking Layer for a english version.
- swift 4.x
- xcode 9.x
- iOS 9 or above
Through Cocoapods
pod 'NetworkRequestKit', '~> 2.1.2'
Sample project is located in the file NetworkRequestExample
. If you don't want to read the instruction below, just open and play with the sample project.
Notice: Remember to install pod before opening sample project.
What I will do here is to create a new file in your project naming it NetworkRequestConfig
. Inside this file, I will indicate my base url
and also set reqeust header
if you use Oauth
by throwing your access token
through header
.
NetworkRequestConfig.swift
import NetworkRequestKit
extension NetworkRequest {
var baseURL: String { return "http://httpbin.org" }
var accessToken: String { return "SOMETOKEN" }
var headers: [String : String] { return ["access_token": accessToken] }
}
Let's assume we have to fetch user information from server. So let's create a struct User
. User
will have it's name and id. It will be like this.
public struct User {
let name: String
let id: Int
}
In order to conform to Codable protocol, just add Codable protocol to your data model, then you're all done! For advance usage or complex nested json, please see Ben Scheirman's Ultimate Guide to JSON Parsing with Swift 4
extension User : Codable {}
In this situation, we are going to fetch user information from server.
final public class FetchUser : NetworkRequest {
}
We will have to tell which endpoint and method this api call should go and perform. Assume we need to perform a get here, and the end point is at /user
.
Also, we need to explicitly tell the request what will the response type be. Here, our ResponseType
is User
.
final public class FetchUser : NetworkRequest {
public typealias ResponseType = User
public var endpoint: String { return "/user" }
public var method: HTTPMethod { return .get }
}
After telling all things we need to perform a request, we are firing the api call right now. Perform method is the method to fire api, and will return a promise with a response.
How do we make a api call using NetworkRequest
? This will be really simple. If we conform to NetworkRequest
, we will get a networkClient, reponseHandler for free. NetworkClient can help us make the request. ResponseHandler will then transform the data we get from the request into ResponseType
we specific above. All these works are chained in Promise chain.
final public class FetchUser : NetworkRequest {
public typealias ResponseType = User
public var endpoint: String { return "/user" }
public var method: HTTPMethod { return .get }
public func perform() -> Promise<ResonseType> {
return networkClient.performRequest(self).then(responseHandler)
}
}
Assume you need to fetch user's information by it's user id. I recommand you to pass in user's id while performing api call like this. Let's make some adjust to our FetchUser
.
I recommand you to have a private id inside your class. So, no one can revise this id during your api call is performing.
final public class FetchUser : NetworkRequest {
public typealias ResponseType = User
public var endpoint: String { return "/user" }
public var method: Alamofire.HTTPMethod { return .get }
public var parameters: [String : Any] { return ["id": id] }
public var encoding: Alamofire.ParameterEncoding { return URLEncoding.default }
private id: Int = 0
public func perform(id: Int) -> Promise<ResonseType> {
self.id = id
return networkClient.performRequest(self).then(responseHandler)
}
}
We will love the benefits NetworkRequst
gave us. Unfortunately, we need to specify our ReponseType
at first. We can specify it to Void
type ()
, but this doesn't look good. Therefore, I make a typealias IgnorableResult
inside NetworkRequst
.
public typealias IgnorableResult = ()
If the response is not important, make ResponseType
equal to IgnorableRequslt
, and the response will not be parsed.
final public class FetchUser : NetworkRequest {
public typealias ResponseType = IgnorableRequslt
public var endpoint: String { return "/user" }
public var method: HTTPMethod { return .get }
public func perform() -> Promise<ResonseType> {
return networkClient.performRequest(self).then(responseHandler)
}
}
In many situation, the response is returned in a json array. What you need to do here is to add angle bracket to your data model type. That's all :)
final public class FetchUsers : NetworkRequest {
public typealias ResponseType = [User]
public var endpoint: String { return "/users" }
public var method: HTTPMethod { return .get }
public func perform() -> Promise<ResonseType> {
return networkClient.performRequest(self).then(responseHandler)
}
}
In some situation, you just want a json back and handle everything by your self, or don't want to create another model file. Use RawJSONResult
instead, RawJSONResult
is typealias of JSON
. Also need rseponse handler here. Notice that json is a dictionary in JS, so it is impossible to have an array response handler here.
final public class FetchUsers : NetworkRequest {
public typealias ResponseType = RawJSONResult
public var endpoint: String { return "/users" }
public var method: HTTPMethod { return .get }
public func perform() -> Promise<[ResonseType]> {
return networkClient.performRequest(self).then(responseHandler)
}
}
Fetching a bunch of data form server is not always a go thing to do. It makes your user experience bad aslo lower the performance on server. So, this is why we need a paging.
It will be like this, conform to PagingEnabledRequest
protocol, then you have to specific which page are you on now. Execute response handler, then check if there is next page of data. Remember to track current page in your model or view controller.
PagingResult
is a typealias of
typealias PagingResult = (results: [Decodable], nextPage: NextPage)
This is for conveneince, typing such a long return type may be easy to get wrong.
final public class FetchUsers : NetworkRequest, PagingEnabledRequest {
public typealias ResponseType = IgnorableResult
public var endpoint: String { return "/users" }
public var method: HTTPMethod { return .get }
public var parameters: [String : Any]? { return ["page": page, "per_page": perPage] }
public var page: Int = 1
public func perform(page: Int) -> Promise<PagingResult> {
self.page = page
return networkClient.performRequest(self).then(responseHandler).then(checkHasNextPage)
}
}
NOTE
There is also a pagingParameters: [String : Any]
to get a packed dictionary if you like to combine two dictionary parameters
and pagingParameters
together.
Like uploading a large image to server, multipart request is often used in Web development. When it comes to iOS, it's not that easy.
MultipartNetworkRequest
and MultipartNetworkClient
can help you to do this work in a easy way.
MultipartNetworkRequest
conforms to NetworkRequest
, so all benefits you got in NetworkRequest
are also available here. But there are something you need to implement to conform to a MultipartNetworkRequest
protocl.
You will need to implement these getters when comforming to MultipartNetworkRequest
protocol:
public protocol MultipartNetworkRequest : NetworkRequest {
/// Data prepared to upload
var multipartUploadData: Data { get }
/// e.g. "avatar"
var multipartUploadName: String { get }
/// e.g. "file"
var multipartUploadFileName: String { get }
/// e.g. "image/jpeg"
var multipartUploadMimeType: String { get }
}
This is sample class that you might do to create a multipart upload request:
final public class UploadTask : MultipartNetworkRequest {
public typealias ResponseType = IgnorableResult
public var endpoint: String { return "/user/199/uploadAvatar" }
public var method: HTTPMethod { return .post }
public var encoding: ParameterEncoding { return URLEncoding.default }
public var multipartUploadData: Data { return data }
public var multipartUploadName: String { return "new_avatar" }
public var multipartUploadFileName: String { return "file" }
public var multipartUploadMimeType: String { return "image/jpeg" }
private var data: Data!
func perform(avatarData: Data) -> Promise<ResponseType> {
self.data = avatarData
return networkClient.performUploadRequest(self).then(responseHandler)
}
}
There are some predefined errors I often use in my project.
- If there is something wrong when parsing data, will return
.decodingError
or.jsonParsingError
error. - If there is something wrong while making the api call, will return
.apiUnaccepatable
error. Error information is attached. - If there is no network, will return
.unknownError
error. You should fire request everytime nomatter network is available or not. This is recommanded in Alamofire's document. If you want to track the network state, check Alamofire's reachability document.
/// NetworkRequestError
///
/// - decodingError: Fail to decode.
/// - jsonParsingError: Fail to parse JSON.
/// - requestFailed: Reqeust failed, with error information attached.
/// - unknownError: Unknown error.
/// - noNetwork: No network connection.
public enum NetworkRequestError: Error {
case decodingError(error: DecodingError)
case jsonParsingError(error: Error)
case requestFailed(information: RequestErrorInformation)
case unknownError
case noNetwork
}
Handling an error is easy. RequestErrorInformation
contains the information you may need to handle an error. error
is Alamofire's returned error. statusCode
is something like 404, 401. reponseBody
contains full information returned by server.
public struct RequestErrorInformation {
/// Error returned from alamofire.
let error: Error
/// Error status code.
let statusCode: Int?
/// More error information, this could be returned error information from backend.
let responseBody: JSON
init(error: Error, data: Data?, urlResponse: HTTPURLResponse?) {
self.error = error
responseBody = JSON(data: data ?? Data())
statusCode = urlResponse?.statusCode
}
}
You can handle the error message by using switch
or if-let
.
let fetchUser = FetchUser()
fetchUser.perform().catch({ e in
// if-let statement
if case let apiUnacceptable(errorInformation: errorInfo) = e {
print(errorInfo)
}
// switch statement
switch(e) {
case let apiUnacceptable(errorInformation: errorInfo):
print(errorInfo)
default:
break
}
})
- GET method with parameters will need to change the encoding to URLEnconding.default, or it's gonna fail with no reason. This maybe Alamofire's bug or is just a RESTful api rule. Its also default setting in
NetworkRequest.swift
. - If you use OAuth 2.0, you can add your access token inside
NetworkRequest
extension. - Remember to set your base url, this url maybe an ip or a url to your server.
- endpoint DO have to start with a
/
. - Beware of
endpoint
andendPoint
. Sometimes you get an error that's telling you, you do not conform toNetworkRequest
protocol, endpoint must be all lowercased characters!!!