14
\$\begingroup\$

I'm working on an iOS app which handles API request based on authentication. Authentication is done through OAuth and every API request is sent along with access_token. In certain cases, the access_token might be expired causing the API request to fail. I would need to call the refresh_token method to refresh the access_token followed by the API request. I have used NotificationCenter to handle callbacks. Are there better ways to handle this using closures or something else?

class DeviceInfo {

    func generateRefreshToken(callbackNotifier : String) -> Void {

        Alamofire.request(DeviceAPIRequest.GEN_REFRESH_TOKEN(refreshToken)).responseJSON {

            (request, response, results, error) -> Void in
            if (error == nil) {

                let jsonResponse = JSON(results!)
                self.accessToken = jsonResponse["access_token"].stringValue
                self.refreshToken = jsonResponse["refresh_token"].stringValue
                let refreshStatus = NSDictionary(object: "success", forKey: "status")

                NSNotificationCenter.defaultCenter().postNotificationName(callbackNotifier, object: self, userInfo: refreshStatus)

            } else {
                let refreshStatus = NSDictionary(object: "failed", forKey: "status")
                NSNotificationCenter.defaultCenter().postNotificationName(callbackNotifier, object: self, userInfo: refreshStatus)
            }
        }
    }

}

and

class UserAPI {

    func getUsers() -> Void {

        let tokenType:String = deviceInfo.tokenType
        let accessToken:String = deviceInfo.accessToken

        Alamofire.request(UserAPIRequest.GET_USER_PARTITION(tokenType, accessToken)).responseJSON() {

            (request, response, results, error) in

            if (error == nil) {

                // sucessfull in getting users...

            } else {

                // possible chances are access_token is expired.
                if response?.statusCode == 403 {
                    self.deviceInfo.generateRefreshToken("getUsers")
                } else {
                    DDLogError("Error while processing getUsers() : \(error)")
                }
            }
        }
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ What about checking if the token is still valid (by checking its expiration time) before actually making a new request? \$\endgroup\$
    – HepaKKes
    Commented Apr 15, 2015 at 21:11

2 Answers 2

20
\$\begingroup\$

Very regularly when dealing with asynchronous stuff (among other things) in iOS development, we have to make a decision between three tools.

  1. Closures (blocks, callbacks, etc)
  2. NSNotificationCenter
  3. The delegate pattern.

Which one is right for you depends on the scenario. It won't always be the same one. As a general rule, we can use the number of objects we want to respond to the result as a guideline for which pattern to use.

  1. Closures (blocks, callbacks, etc.) are good for when we want zero objects tied to waiting on the result. We just pass the closure and forget about it. 0 objects.
  2. NSNotificationCenter is a good approach when we want any number of unknown objects to be able to respond to the result. The thing working on producing the result knows nothing about these objects, it just creates a notification when it's complete. 0-many objects.
  3. The delegate pattern is good when we want exactly one object to respond to the result. Using the delegate pattern, the delegate and the worker object know quite a bit about each other. The worker object will define a protocol of set methods that its delegate must implement or can optionally implement, and the worker object can call any of these. 1 object.

It looks here like we most likely want the delegate pattern for our scenario, given that our generateRefreshToken method takes a custom notification string. But I will cover all three options lightly (and perhaps after picking one you for sure want to use, you can improve your code and post another question where we cover that topic in detail).


For all of these scenarios, we need to consider the same information--what do we need to pass to our completion handling code?

  1. Some indication of success vs failure.
  2. If failure, details on the failure.
  3. If success, the results.

Closures (blocks, callbacks, etc)

This is basically the approach that joekim's answer takes. Our method, generateRefreshToken just needs to take a single argument, a completion block. That completion block should be defined as a closure that accepts arguments that convey the information we outlined above, and return void. joekim's answer outlines a very good approach to this, but how you implement it specifically is up to you. Frequently these sorts of closures will simply take an NSData or NSDictionary object to represent the data (or perhaps just a regular Swift Dictionary) and an NSError optional to indicate success or failure (and hold failure details).


NSNotificationCenter

This is effectively the pattern you're currently using. But remember, I described NSNotificationCenter as being for when you want zero to many objects responding to completion. As such, when we use NSNotificationCenter, we probably should be accepting a string parameter to tell us the name of the notification. Instead, the file that declares this class should probably also expose two String constants that the object will use for its notification. One of these notifications is the "success" notification, and the other is the "error" notification. The "userInfo" passed with the "success" notification is simply the necessary data we want to collect, and the "userInfo" passed with the "failure" notification simply contains the error.

The reason that I recommend this approach (two notifications) is simply because NSNotificationCenter allows observers a lot of control and flexibility.

If I want to use a single method to observe both methods and just check to see what's in user info, I can.

If I want to use two methods and separate my error handling logic from my success handling logic, I can.

If I want to ignore failure (or success), I can. If I want to have one object handle success and another handle failure, I can.


The Delegate Pattern

This is the pattern I actually think is most appropriate for your scenario. For this pattern, we must start by declaring a protocol that defines the methods we'll be calling on our delegate. Something like this might work:

@objc protocol GenerateTokenDelegate {
    func tokenGeneratorDidSucceed(tokenGenerator: DeviceInfo)
    func tokenGeneratorDidFail(tokenGenerator: DeviceInfo)
}

Of course, this approach means we must declare our protocol as @objc, and our class DeviceInfo, must also be an Objective-C class (make it inherit from NSObject), and we must do this so we can hold a weak reference to our delegate. If we hold a strong reference to our delegate (which will typically already have a strong reference to us), we risk creating a retain cycle. The delegated object should hold a weak reference to its delegate.

So now we need to add a weak reference to our delegate to our class:

class DeviceInfo:NSObject{
    weak var delegate: GenerateTokenDelegate?
    // the rest of our class definition
}

Now, when we instantiate our class, we set its delegate property equal to the object we want it to call back to (which conforms to GenerateTokenDelegate protocol), and when we've completed our asynchronous code, we call back to it as such:

self.delegate?.tokenGeneratorDidFail(self)

And obviously, we can set our protocol up to define our methods in any such way and take any set of arguments (though we'll always want to pass self as the first argument, typically).

We can also declare optional methods. Suppose we maybe don't want to force the delegate to handle errors. We can let the "didFail" method be an optional part of the protocol.

optional func tokenGeneratorDidFail(tokenGenerator: DeviceInfo)

And now we'd call it as such:

self.delegate?.tokenGeneratorDidFail?(self)

Notice both sets of question marks. The delegate may or may not be nil, and even if it's not nil, it may or may not implement the method we're trying to call.

\$\endgroup\$
3
  • \$\begingroup\$ The protocol shouldn’t be of type class in order to weak reference it? \$\endgroup\$ Commented May 13, 2018 at 9:25
  • \$\begingroup\$ Swift has changed several times since this answer was originally posted @IvanCantarino \$\endgroup\$
    – nhgrif
    Commented May 14, 2018 at 13:22
  • \$\begingroup\$ yeah; I’ve missed the answer date though. Anyway, nice answer. \$\endgroup\$ Commented May 14, 2018 at 13:24
4
\$\begingroup\$

Notifications and delegates get the job done, but in the swift world I prefer to use enums and generics. You can write your code in a style where you have type safety and are less likely to forget to handle errors.

struct RefreshTokenResponse {
    var accessToken: String
    var refreshToken: String
}

struct ApiError {
    var error: NSError?
    var message: String?

    var description: String {
        if let error = error {
            return error.description
        }
        else if let message = message {
            return message
        }
        return "ApiError"
    }
}

struct User {

}

enum Result<U, T> {
    case Success(U)
    case Failure(T)
}

class DeviceInfo {

    class func generateRefreshToken(callback: (Result<RefreshTokenResponse, ApiError>) -> ()) -> Void {

        Alamofire.request(DeviceAPIRequest.GEN_REFRESH_TOKEN(refreshToken)).responseJSON {

            (request, response, results, error) -> Void in
            if (error == nil) {

                let jsonResponse = JSON(results!)
                if let accessToken = jsonResponse["access_token"].stringValue,
                    refreshToken = jsonResponse["refresh_token"].stringValue
                {
                    let response = RefreshTokenResponse(accessToken: accessToken, refreshToken: refreshToken)
                    let result =  Result.Success(response)
                    callback(result)
                } else {
                    let response = ApiError()
                    response.message = "access_token or refresh_token is nil"
                    let result = Result.Failure(response)
                    callback(result)
                }
            } else {
                let response = ApiError(error: error)
                let result = Result.Failure(response)
                callback(result)
            }
        }
    }

}

class UserAPI {

    class func getUsers(token: RefreshTokenResponse, callback: (Result<[Users], ApiError>) -> ()) -> Void {

        let refreshToken: String = token.refreshToken
        let accessToken: String = token.accessToken

        Alamofire.request(UserAPIRequest.GET_USER_PARTITION(refreshToken, accessToken)).responseJSON() {

            (request, response, results, error) in

            if (error == nil) {

                // sucessfull in getting users...
                var users = [Users]()
                callback(Result.Success(users))

            } else {

                // possible chances are access_token is expired.
                if response?.statusCode == 403 {
                    DeviceInfo.generateRefreshToken() {
                        result in

                        switch (result) {
                        case .Success(let refreshToken):
                            getUsers(refreshToken, callback: callback)
                        case .Failure:
                            callback(result)
                        }

                    }

                } else {
                    let response = ApiError()
                    response.error = error
                    response.message = "Error while processing getUsers() : \(error)"
                    let result = Result.Failure(response)
                    callback(result)
                }
            }
        }
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.