07 April

Solving a data flow diagram using PromiseKit

Before they start coding something, most programmers begin by creating a data flow diagram to know exactly how each piece of code is supposed to work (how it should behave). If the behavior of the application is not complicated, you do not have to sketch it (although it is a good habit, even with simple things), but with more complicated behavior, it is hard to properly code such flow.

Ok, so we have a diagram, what's next?

(this is a simple diagram for retrieving login type information for a logged-in user)

... now we sit down and code, all is good... but how to write it down so that it makes sense, and what to do when we need to call something asynchronously, e.g. download some data from the API? It is certainly a good idea to divide it into items and save them each in a separate method. This way we will create code blocks that are responsible for specific functionalities, which will make the whole thing cleaner. We already have code blocks, so now, how do we put them together? We have conditional blocks here so I will wrap the whole thing in if / else and it will be ok... yes and no :) because packaging our blocks in a conditional expression will work, but it will not look good, it will simply be not very readable. Example

class HelloDiagramFlow {
    func flow() {
        if isSessionValid {
            userInfoBlock {
                
            }
        } else {
            refreshSession { sessionKey in
                userInfoBlock {
                    
                }
            }
        }
    }
    
    func userInfoBlock(closure:()->()) {
        getUserInfoAsync { userInfo in
            if isPremiumUser {
                if isPremiumPlanExpired {
                    
                } else {
                    
                }
            } else {
                
            }
        }
    }
    
    func getUserInfoAsync(closure:(UserInfo)->()) {
        
    }
    
    func refreshSession(closure:(String)->()) {
        
    }
}

As you can see, already with the first few blocks, the code becomes quite unreadable, and our diagram is not particularly complicated. How can we fix it? We can use the so-called "promise" (I recommend to look here). Ok, but how do we go about it? Example! We will begin by packing our blocks into our "promises" using PromiseKit.

    // -- 1
    //
    fileprivate func sessionValid() -> Promise {
        return Promise{fulfill, reject in
            fulfill(helper.sessionExpired)
        }
    }
    
    // -- 2
    //
    fileprivate func refreshToken() -> Promise {
        return firstly {
            Promise(value: self.helper.sessionToken)
            }.then { token -> Promise in
                debugPrint("- Token refreshed: \(token)")
                return self.getUserInfo()
        }
    }
    
    // -- 3
    //
    fileprivate func getUserInfo() -> Promise {
        return Promise{fulfill, reject in
            self.helper.queue.async {
                for _ in 0...1000000000 {}
                DispatchQueue.main.sync {
                    fulfill(self.helper.userInfo)
                }
            }
        }
    }
    
    // -- 4
    //
    fileprivate func checkAccountType() -> Promise {
        if let userInfo = userInfo {
            debugPrint("- Account type checked: \(userInfo.accountType)")
            switch userInfo.accountType {
            case .premium:
                return self.premiumAccountFlow()
            case .regular:
                return self.regularAccountFlow()
            }
        } else {
            return Promise{fulfill, reject in reject(NSError.cancelledError())}
        }
    }
    
    // -- 4.1
    //
    fileprivate func premiumAccountFlow() -> Promise  {
        return firstly {
            self.isPremiumPlanExpired()
            }.then { expired -> Promise in
                if expired {
                    return self.renewPremiumAccount()
                } else {
                    return self.premiumUserAccount()
                }
        }
    }
    
    // -- A
    //
    fileprivate func regularAccountFlow() -> Promise {
        return Promise {fulfill, reject in
            if let closure = finishedClosure {
                closure(.regular)
            }
            fulfill()
        }
    }
    
    // -- B
    //
    fileprivate func premiumUserAccount() -> Promise {
        return Promise {fulfill, reject in
            if let closure = finishedClosure {
                closure(.premium)
            }
            fulfill()
        }
    }
    
    // -- C
    //
    fileprivate func renewPremiumAccount() -> Promise  {
        return Promise {fulfill, reject in
            if let closure = finishedClosure {
                closure(.premiumRenew)
            }
            fulfill()
        }
    }
    
    // -- 5
    //
    fileprivate func isPremiumPlanExpired() -> Promise {
        return Promise{fulfill, reject in
            fulfill(self.helper.premiumExpired)
        }
    }
    

As for the asynchronous data call, to make things simpler I do not connect to any API, but just start a loop with a large counter. However, instead of using a loop you can also put there a call to API, by using URLSession, Alamofire or something like this.

Okay, the code is nicely packaged in blocks, but it will not work yet, especially since it is not much different from the usual method. Well not exactly, see below an example of calling the whole flow, for better encapsulation I packaged this flow into a method that outputs the data we are interested in:

func start(closure:@escaping (ViewType)->()) {
        finishedClosure = closure
        debugPrint("Start")
        firstly {
            return self.sessionValid()
            }.then { sessionExpired -> Promise in
                debugPrint("- Is session expired: \(sessionExpired)")
                if sessionExpired {
                    return self.refreshToken()
                } else {
                    return self.getUserInfo()
                }
            }.then { userInfo -> Promise in
                debugPrint("- User info downloaded: \(userInfo.email)")
                self.userInfo = userInfo
                return self.checkAccountType()
            }.always {
                debugPrint("Promises chain finished - hide activity indicator etc.")
            }.catch { error in
                debugPrint(error)
        }
    }
    

Boom! That's it? Yes, that's it :), isn't it more readable? You can also easily add error handling anywhere in the chain of calls. So, I encourage you to get acquainted with PromiseKit or other libraries using "promises".

You can download running code here(Swift 3.0)