Codementor Events

Calling API in Swift

Published Jun 25, 2019

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 of BASE_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.

Discover and read more posts from Ky Nguyen
get started