Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS/Swift: Good architecture approach for connecting REST APIs

I’m developing iOS Apps for quite a long time now. But in the end I was never satisfied with the architecture design for my network layer. Especially when it goes about connecting an API.


There exists a possible duplicate here, but I think my question is more specific as you will see.

Best architectural approaches for building iOS networking applications (REST clients)


I’m not looking for answers like "use AFNetworking/Alamofire". This question is regardless of which 3rd party framework is used.

I mean, often we have the scenario:

"Develop an app X that uses API Y"

And this includes mainly the same steps - everytime.

  1. Implement login / registration
  2. You get an authentication token, have to save it in the keychain and append it in every API call
  3. You have to re-authenticate and re-send the API request which failed with a 401
  4. You have error codes to handle (how to handle them centralized?)
  5. You implement the different API calls.

One problem with 3)

In Obj-C I used NSProxy for intercepting every API Call before it was send, re-authenticated the user if the token expired and and fired the actual request. In Swift we had some NSOperationQueue where we queued an auth call if we got a 401 and queued the actual request after successful refresh. But that limited us to use a Singleton (which I don’t like much) and we also had to limit the concurrent requests to 1. I like more the second approach - but is there a better solution?

Regarding 4)

How do you handle http status codes? Do you use many different classes for every error? Do you centralize general error handling in one class? Do you handle them all at the same level or do you catch server errors earlier? (Maybe in your API Wrapper of any 3rd party lib)


How are you developers trying to solve this problems? Have you figured out a "best match" design? How do you test your APIs? Especially how do you do this in Swift (with no real mocking possibility?).

Of course: Every use case, every app, every scenario is different - there is no "One solution fits them all". But I think these general problems re-appear so often, so I’m tempted to say "Yes, for these cases - there could be one and more solutions - which you can reuse every time".

Looking forward to interesting answers!

Cheers
Orlando 🍻

like image 489
orschaef Avatar asked Jun 16 '16 15:06

orschaef


People also ask

What is technique in iOS to secure your API call in Swift?

The main key of SSL pinning that server certificate will be saved in app bundle. Then, when client receives certificate from server, it then compares 2 certificates to make sure that they are the same before establishing the connection. You can read more here and here about how to achieve SSL Pinning in your swift app.

What are RESTful APIs in Swift?

REST APIs or RESTful web services in layman's terms are the way that the mobile apps or websites communicate and transmit data to servers and vice-versa. There are a set HTTP Requests posting and getting data to and from servers over internet.

How many types of Swift API are there?

There are four principal types of API commonly used in web-based applications: public, partner, private and composite. In this context, the API "type" indicates the intended scope of use. Public APIs.


1 Answers

But that limited us to use a Singleton (which I don’t like much) and we also had to limit the concurrent requests to 1. I like more the second approach - but is there a better solution?

I am using a few layers for authenticating with an API.

Authentication Manager


This manager is responsible for all authentication related functionality. You can think about authentication, reset password, resend verification code functions, and so on.

struct AuthenticationManager
{
    static func authenticate(username:String!, password:String!) -> Promise<Void>
    {
        let request = TokenRequest(username: username, password: password)

        return TokenManager.requestToken(request: request)
    }
}

In order to request a token we need a new layer called the TokenManager, which manages all things related to a token.

Token Manager


struct TokenManager
{
    private static var userDefaults = UserDefaults.standard
    private static var tokenKey = CONSTANTS.userDefaults.tokenKey
    static var date = Date()

    static var token:Token?
    {
        guard let tokenDict = userDefaults.dictionary(forKey: tokenKey) else { return nil }

        let token = Token.instance(dictionary: tokenDict as NSDictionary)

        return token
    }

    static var tokenExist: Bool { return token != nil }

    static var tokenIsValid: Bool
    {
        if let expiringDate = userDefaults.value(forKey: "EXPIRING_DATE") as? Date
        {
            if date >= expiringDate
            {
                return false
            }else{
                return true
            }
        }
        return true
    }

    static func requestToken(request: TokenRequest) -> Promise<Void>
    {
        return Promise { fulFill, reject in

            TokenService.requestToken(request: request).then { (token: Token) -> Void in
                setToken(token: token)

                let today = Date()
                let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: today)
                userDefaults.setValue(tomorrow, forKey: "EXPIRING_DATE")

                fulFill()
            }.catch { error in
                reject(error)
            }
        }
    }

    static func refreshToken() -> Promise<Void>
    {
        return Promise { fulFill, reject in

            guard let token = token else { return }

            let  request = TokenRefresh(refreshToken: token.refreshToken)

            TokenService.refreshToken(request: request).then { (token: Token) -> Void in
                setToken(token: token)
                fulFill()
            }.catch { error in
                reject(error)
            }
        }
    }

    private static func setToken (token:Token!)
    {
        userDefaults.setValue(token.toDictionary(), forKey: tokenKey)
    }

    static func deleteToken()
    {
        userDefaults.removeObject(forKey: tokenKey)
    }
}

In order to request a token we'll need a third layer called TokenService which handles all the HTTP calls. I use EVReflection and Promises for my API calls.

Token Service


struct TokenService: NetworkService
{
    static func requestToken (request: TokenRequest) -> Promise<Token> { return POST(request: request) }

    static func refreshToken (request: TokenRefresh) -> Promise<Token> { return POST(request: request) }

    // MARK: - POST

    private static func POST<T:EVReflectable>(request: T) -> Promise<Token>
    {
        let headers = ["Content-Type": "application/x-www-form-urlencoded"]

        let parameters = request.toDictionary(.DefaultDeserialize) as! [String : AnyObject]

        return POST(URL: URLS.auth.token, parameters: parameters, headers: headers, encoding: URLEncoding.default)
    }
}

Authorization Service


I am using an Authorisation Service for the problem you are describing here. This layer is responsible for intercepting server errors such as 401 (or whatever code you want to intercept) and fix them before returning the response to the user. With this approach everything is handled by this layer and you don't have to worry about an invalid token anymore.

In Obj-C I used NSProxy for intercepting every API Call before it was send, re-authenticated the user if the token expired and and fired the actual request. In Swift we had some NSOperationQueue where we queued an auth call if we got a 401 and queued the actual request after successful refresh. But that limited us to use a Singleton (which I don’t like much) and we also had to limit the concurrent requests to 1. I like more the second approach - but is there a better solution?

struct AuthorizationService: NetworkService
{
    private static var authorizedHeader:[String: String]
    {
        guard let accessToken = TokenManager.token?.accessToken else
        {
            return ["Authorization": ""]
        }
        return ["Authorization": "Bearer \(accessToken)"]
    }

    // MARK: - POST

    static func POST<T:EVObject> (URL: String, parameters: [String: AnyObject], encoding: ParameterEncoding) -> Promise<T>
    {
        return firstly
        {
            return POST(URL: URL, parameters: parameters, headers: authorizedHeader, encoding: encoding)

        }.catch { error in

            switch ((error as NSError).code)
            {
            case 401:
                _ = TokenManager.refreshToken().then { return POST(URL: URL, parameters: parameters, encoding: encoding) }
            default: break
            }
        }
    }
}

Network Service


The last part will be the network-service. In this service layer we will do all interactor-like code. All business logic will end up here, anything related to networking. If you briefly review this service you'll note that there is no UI-logic in here, and that's for a reason.

protocol NetworkService
{
    static func POST<T:EVObject>(URL: String, parameters: [String: AnyObject]?, headers: [String: String]?, encoding: ParameterEncoding) -> Promise<T>

}

extension NetworkService
{
    // MARK: - POST

    static func POST<T:EVObject>(URL: String,
                                 parameters: [String: AnyObject]? = nil,
                                 headers: [String: String]? = nil, encoding: ParameterEncoding) -> Promise<T>
    {
        return Alamofire.request(URL,
                                 method: .post,
                                 parameters: parameters,
                                 encoding: encoding,
                                 headers: headers).responseObject()
    }
 }

Small Authentication Demo


An example implementation of this architecture would be a authenticate HTTP request to login a user. I'll show you how this is done using the architecture described above.

AuthenticationManager.authenticate(username: username, password: password).then { (result) -> Void in

// your logic

}.catch { (error) in

  // Handle errors

}

Handling errors is always a messy task. Every developer has it's own way of doing this. On the web there are heaps of articles about error handling in for example swift. Showing my error handling will be of not much help since it's just my personal way of doing it, it's also a lot of code to post in this answer, so I rather skip that.

Anyway...

I hope I've helped you back on track with this approach. If there is any question regarding to this architecture, I'll be more than happy to help you out with it. In my opinion there is no perfect architecture and no architecture that can be applied to all projects.

It's a matter of preference, project requirements and expertise in within your team.

Best of luck and please do no hesitate to contact me if there's any problem!

like image 143
Kevin Vugts Avatar answered Sep 22 '22 09:09

Kevin Vugts