Calling API in Swift
Nowadays, 99% apps need connect to the server via API. So deal with networking is quite important. How do we manage calling API with ease?
I am sure, your answers are Alamofire
. You're right. Alamofire
is so good, it's must-know library in Swift.
Alamofire
, URLSession
or any other libraries are connectors to server. But how to use them efficiently is another story. I tell you today.
APIConnector
This is the static struct you use everyday to interact with server. Have a look at its content.
1. Header
static private func getHeaders() -> [String: String]? {
return [
"Content-Type": "application/json",
"access_token": "your access token or api key"
]
}
Most of requests need headers, where you need to pass access_token
or api_key
to authorize your requests. getHeaders
will do this. Add your authorization factors here and it will be attached to your requests, no need to care anymore.
What happen if you want to customize the header? Later.
2. API URL
static private func getUrl(from api: String) -> URL? {
let BASE_URL = ""
let apiUrl = api.contains("http") ? api : BASE_URL + api
return URL(string: apiUrl)
}
You don't want to repeat https://your_website.com/api/v1
everywhere you call API, right?
Put your BASE_URL
inside getUrl
, only add /users/:id
or /transactions
. Don't repeat yourself.
No
"/"
at the end ofBASE_URL
, and always start API with"/"
If you want to connect other BASE_URL, you can pass the complete API URL into, like https://custom_website.com/api/v2/users
. Remember, API URL must start with http
or https
.
3. Request
The most important part is here.
static func request(_ api: String,
method: HTTPMethod,
params: [String: Any]? = nil,
headers: [String: String]? = nil,
successJsonAction: ((_ result: AnyObject) -> Void)? = nil,
successDataAction: ((Data) -> Void)? = nil,
failAction: ((_ error: knError) -> Void)? = nil) {
let finalHeaders = headers ?? getHeaders()
let apiUrl = getUrl(from: api)
connector.newRequest(withApi: apiUrl,
method: method,
params: params,
headers: finalHeaders,
successJsonAction: successJsonAction,
successDataAction: successDataAction,
failAction: failAction)
}
Let's see the params.
-
api
: Pass your API here, API only or full API URL is accepted. I told you in part 2. -
method
:HTTPMethod
: .get, .post, .put, .delete... -
params
: Default is nil. You can ignore it if no params need to send. In the demo, I ignore it. -
headers
: You can add custom headers here. If it's nil, default headers will be attached. -
successJsonAction
,successDataAction
: I support 2 ways to handle response: Manually parse JSON Object and JSONDecoder.
I like parsing JSON manually, I can handle complex objects, multiple nested levels object, add custom properties I want.
But can't deny that,JSONDecoder
is really powerful. Simple objects or results you want all properties,JSONDecoder
will be the best choice. -
failAction
: obviously, you need handle your request when it fails.
4. Connector
static private var connector = AlamofireConnector()
Yes, you're right. I put a connector as a middleware to library. I suppose, one day, Matt removes Alamofire
, and we just add other Connector here. It will not affect to other code.
AlamofireConnector
Now we use Alamofire or any libraries to connect to our server.
There are 2 functions in any Connector.
1. Run
func run(withApi api: URL?,
method: HTTPMethod,
params: [String: Any]? = nil,
headers: [String: String]? = nil,
successJsonAction: ((_ result: AnyObject) -> Void)? = nil,
successDataAction: ((Data) -> Void)? = nil,
failAction: ((_ error: knError) -> Void)?) {
guard let api = api else {
failAction?(InvalidAPIError(api: "nil"))
return
}
let encoding: ParameterEncoding = method == .get ?
URLEncoding.queryString :
JSONEncoding.default
Alamofire.request(api, method: method,
parameters: params,
encoding: encoding,
headers: headers)
.responseJSON { (returnData) in
self.answer(response: returnData,
successJsonAction: successJsonAction,
successDataAction: successDataAction,
failAction: failAction)
}
}
You need to update encoding here if your server is accepted different encoding. In my 6 years working in iOS, only one time I had to change it.
2. Answer
private func answer(response: DataResponse<Any>,
successJsonAction: ((_ result: AnyObject) -> Void)? = nil,
successDataAction: ((Data) -> Void)? = nil,
failAction fail: ((_ error: knError) -> Void)?) {
if let statusCode = response.response?.statusCode {
if statusCode == 500 {
return
}
// handle status code here: 401 -> show logout; 500 -> show error
}
if let error = response.result.error {
let err = knError(code: "unknown", message: error.localizedDescription)
fail?(err)
return
}
guard let json = response.result.value as AnyObject?, let data = response.data else {
// handle unknown error
return
}
// handle special error convention from server
// ...
if let successDataAction = successDataAction {
successDataAction(data)
return
}
successJsonAction?(json)
}
You need to handle the responses here. It's different by projects. If you want to debug the responses, you need to put break points here.
Error Handler
The object Error
in response lacks of information. So I usually create new Error type.
class knError {
var code: String = "unknown"
var message: String?
var rawData: AnyObject?
var displayMessage: String {
return message ?? code
}
init() {}
init(code: String, message: String? = nil, rawData: AnyObject? = nil) {
self.code = code
self.message = message
self.rawData = rawData
}
}
class InvalidAPIError: knError {
private override init() {
super.init()
}
private override convenience init(code: String, message: String? = nil, rawData: AnyObject? = nil) {
self.init()
}
convenience init(api: String) {
self.init()
code = "404"
message = "Invalid API Endpoint"
rawData = api as AnyObject
}
}
You can add UnauthorizationError
or ForbiddenError
to respond to your controllers. It's easier to understand than Error
, right?
Conclusion
This is how I manage networking in my 10 projects. Feel good with it and hope it useful for you guys.
Please access to the demo at my https://github.com/nguyentruongky/CallingAPISwift.
Please feel free to share your ways or comments here. Looking forward to your opinions.
Enjoy coding.