HiveBrain v1.2.0
Get Started
← Back to all entries
patternswiftMinor

Avoiding callback chaining in authenticator

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
callbackauthenticatoravoidingchaining

Problem

I'm developing an iOS Application in Swift that uses Google Cloud Endpoints as its backend. For now, I've just written the code for the user to login with his Gmail account and I've refactored it as much as possible but the callback chain is making me nervous.

The reason I'm worried is because firstly, I'm fairly new to programming, and secondly, the company where I work now uses a similar callback-chain method to make network calls and I sort of dislike it because it takes a while for a new developer to figure out the sequence of events and it feels a bit like spaghetti code.

I would appreciate any suggestions for improving the code especially to avoid callback chaining or at least suggestions to make it more navigable.

P.S.: If you're wondering why I'm rebuilding the OAuth 2 authentication object from NSUserDefaults, please see this question.

```
class ViewController: UIViewController{

//Member variables

//Outlets
@IBOutlet weak var loginButton: UIButton!
@IBOutlet weak var detailsLabel: UILabel!

//Lifecycle Overrides

override func viewDidLoad() {

if let auth : GTMOAuth2Authentication = GoogleEndpointAssistant.rebuildGTMOAuth2AuthenticationFromUserDefaults(){

setAuthentication(auth)
}

}

//Methods

func setAuthentication(auth : GTMOAuth2Authentication){

//set OAuth2 as Google End Point service authorizer
ASSession.sharedSession.gtlService.authorizer = auth;

//save user's ID (Will be used to fetch user's information from server)
ASSession.sharedSession.user.identifier = NSNumber(longLong: (auth.userID as NSString).longLongValue);

configureSession()
}

/**
Configures the session instance (ASSession.configure()) and, if successsful, calls identifyUser()
*/
func configureSession(){

MBProgressHUD.showHUDAddedTo(self.view, animated: true)

ASSession.sharedSession.configure({ (error:NSError?) -> Void in

if

Solution

I tend to not like callback closures myself and I agree with your sentiment toward "spaghetti code". With time and experience, these will tend to make sense, and you kind of just know what they're doing, but they still tend to give me pause. I personally vastly prefer the delegation pattern which is a common pattern throughout Apple's libraries.

Unfortunately, not every library we use is written by Apple, and not every library we use is written solely with iOS developers in mind, and accepting callback closures is a pretty common pattern outside of iOS development (though not wholly uncommon within). So sometimes we're just stuck.

To a degree, we a stuck here... but we can abstract a bit of this into a seperate class and apply a delegation pattern over top of the blocks. That will certainly untangle our view controller code quite a bit, right?

So, for starters, let's get a sort of road map of what our new class will need to do.

What functionality does it need?

  • Set Authentication



  • Configure Session



  • Identify User



  • Authenticate User



This is simply a list of what you're doing with it now.

Meanwhile, what sort of callbacks should we tell our delegate about?

  • Configure Session failed.



  • Configure Session succeeded.



  • User Identified.



  • User not registered.



  • User identification failed.



  • User authenticated.



  • User authentication failed.



Now, with these two lists, we can pretty well write the interface for what our class will look like, right? First a protocol:

@objc protocol GoogleCloudManagerDelegate {
    func cloudManager(manager: GoogleCloudManager, didFailSessionConfiguration error: NSError!)
    func cloudManagerDidConfigureSession(manager: GoogleCloudManager)
    func cloudManager(manager: GoogleCloudManager, didIdentifyUser ticket:GTLServiceTicket!, response:AnyObject!)
    func cloudManagerFoundUnregisteredUser(manager: GoogleCloudManager)
    func cloudManager(manager: GoogleCloudManager, didFailIdentifyingUser error: NSError!)
    func cloudManager(manager: GoogleCloudManager, didFinishAuthentication auth: GTMOAuth2Authentication, viewController: GTMOAuth2ViewControllerTouch)
    func cloudManager(manager: GoogleCloudManager, authenticationDidFailWithError error: NSError!)
}


So, that's a first draft at the protocol. May take some tweaking, but from here, I think you'll start to get the hang of it. (We're marking it with @objc so we can use weak in the class.)

Now we need a class that can perform all of this stuff and handle the call backs.

We need a weak var for our delegate variable, and then we need to implement methods for each of the four things we'll want to let the user do.

The gist of it go something like this:

class GoogleCloudManager : NSObject {
    weak var delegate: GoogleCloudManagerDelegate?

    init(delegate: GoogleCloudManagerDelegate? = nil) {
        self.delegate = delegate
    }

    func configureSession() {
        ASSession.sharedSession.configure() { (configError:NSError?) -> Void in
            if let error = configError  {
                self.delegate?.cloudManager(self, didFailSessionConfiguration: error)
            }else{
                self.delegate?.cloudManagerDidConfigureSession(self)
            }
        }
    }
}


From here, you should be able to figure out how to implement the other methods. The point is, we're putting all of the block-based code in this data class that doesn't do anything at all with the view. In the callback blocks, we're doing very minimal logic, to determine which methods we should call on our delegate.

In that configureSession() method, we may even want to add a optional func cloudManagerWillConfigureSession(manager: GoogleCloudManager) to our protocol, and optionally call that on our delegate just before configuring:

func configureSession() {
    self.delegate?.cloudManagerWillConfigureSession?(self)
    ASSession.sharedSession.configure() { (configError:NSError?) -> Void in
        if let error = configError  {
            self.delegate?.cloudManager(self, didFailSessionConfiguration: error)
        }else{
            self.delegate?.cloudManagerDidConfigureSession(self)
        }
    }
}


Now, for the final step, putting all the pieces together. Our view controller needs to declare itself as conforming to our nearly created GoogleCloudManagerDelegate protocol, implement the required method, optionally implement the optional methods, add an instance variable for the manager class, and call its functions.

```
class ViewController : UIViewController, GoogleCloudManagerDelegate {
let cloudManager = GoogleCloudManager(delegate: self)

override func viewDidLoad() {
if let auth: GTMOAuth2Authentication = GoogleEndpointAssistant.rebuildGTMOAuth2AuthenticationFromUserDefaults(){
self.cloudManager.setAuthentication(auth)
self.cloudManager.configureSession()
}
}

func cloudManager(manager: GoogleCloudManager, didFail

Code Snippets

@objc protocol GoogleCloudManagerDelegate {
    func cloudManager(manager: GoogleCloudManager, didFailSessionConfiguration error: NSError!)
    func cloudManagerDidConfigureSession(manager: GoogleCloudManager)
    func cloudManager(manager: GoogleCloudManager, didIdentifyUser ticket:GTLServiceTicket!, response:AnyObject!)
    func cloudManagerFoundUnregisteredUser(manager: GoogleCloudManager)
    func cloudManager(manager: GoogleCloudManager, didFailIdentifyingUser error: NSError!)
    func cloudManager(manager: GoogleCloudManager, didFinishAuthentication auth: GTMOAuth2Authentication, viewController: GTMOAuth2ViewControllerTouch)
    func cloudManager(manager: GoogleCloudManager, authenticationDidFailWithError error: NSError!)
}
class GoogleCloudManager : NSObject {
    weak var delegate: GoogleCloudManagerDelegate?

    init(delegate: GoogleCloudManagerDelegate? = nil) {
        self.delegate = delegate
    }

    func configureSession() {
        ASSession.sharedSession.configure() { (configError:NSError?) -> Void in
            if let error = configError  {
                self.delegate?.cloudManager(self, didFailSessionConfiguration: error)
            }else{
                self.delegate?.cloudManagerDidConfigureSession(self)
            }
        }
    }
}
func configureSession() {
    self.delegate?.cloudManagerWillConfigureSession?(self)
    ASSession.sharedSession.configure() { (configError:NSError?) -> Void in
        if let error = configError  {
            self.delegate?.cloudManager(self, didFailSessionConfiguration: error)
        }else{
            self.delegate?.cloudManagerDidConfigureSession(self)
        }
    }
}
class ViewController : UIViewController, GoogleCloudManagerDelegate {
    let cloudManager = GoogleCloudManager(delegate: self)

    override func viewDidLoad() {
        if let auth: GTMOAuth2Authentication = GoogleEndpointAssistant.rebuildGTMOAuth2AuthenticationFromUserDefaults(){
            self.cloudManager.setAuthentication(auth)
            self.cloudManager.configureSession()
        }
    }

    func cloudManager(manager: GoogleCloudManager, didFailSessionConfiguration error: NSError!) {
        // code to do when session config fails
    }

    func cloudManagerDidConfigureSession(manager: GoogleCloudManager) {
        // code to do when session config succeeds
    }

    // etc.
}

Context

StackExchange Code Review Q#85748, answer score: 4

Revisions (0)

No revisions yet.