2

Problem: I would like to develop an iOS app in Swift which is perfoming an initial load right after the login. The sequence (of REST based calls via NSURLSession) would look like this:

  1. Login with user account -> asynchronous response returns userId
  2. Get countries for userId -> asynchronous response returns countryId's
  3. Get products for countryId -> ...etc...

Basically I would like to find an elegant way on how to implement such a sequence.

Approach: First I started by just calling the new (dependent) REST call in the completion handler of another. But if many calls need to be executed and the dependency levels are more than the one's described above the code looks a little bit messy...

I came accross the WWDC 2015 session on NSOperations and thought that this might be a good idea as some can define dependencies very easy. Unfortunately the sample code provided by Apple does not give an answer to the problem described above...is it (and I did not get it?)? While playing around with Operations I could not make my mind on how to solve the initialization problem at the time creating the different operations (LoginOperation, CountryOperation (dependent on LoginOperation), ProductOperation (dependent on CountryOperation), etc..)

I found these posts very helpful, but stil I'm lacking of understanding how to approch best the problem I described: How To Download Multiple Files Sequentially using NSURLSession downloadTask in Swift Chaining NSOperation : Pass result from an operation to the next one NSURLSession with NSBlockOperation and queues

Difficulties: Initializing an operation B at the time when another operation A is successfully finished and has returned a result which is going to be used by operation B.

Community
  • 1
  • 1
Hans M.
  • 21
  • 4
  • Any help/hint is really appreciated! Is the idea to create for each REST based request a NSOperation subclass a good one? This would lead to repition of code, does it? Or should I create instead one NSoperation base class for NSURLSession which is handling the GET/POST requests. For the second approach I'm not sure how to handle the initialization for the next call in the completion handler based on the targeted sequence... Please help! – Hans M. Feb 11 '16 at 08:57

1 Answers1

1

If still relevant, I may have an answer.

The problem here is that you’re trying to defer initialization until first operation is finished. That’s a dead end. Operations are meant to be created early, and order is guaranteed using dependencies.

So let’s discuss some approaches to this extremely frequent problem.

(You can note that the examples is written in WWDC-session style. This is actually because I use Operations library which is based on that talk. Take a look at that if you’re interested in Operations-based architecture.)

1. Use some external mutable shared state (Cocoa-way)

That basically means that, for your example, you have some UserController, instance of which you’re passing to both LoginOperation and CountryOperation. LoginOperation writes userId to that controller, and CountryOperation reads from that:

class UserController {
    var userID: String?
}

class LoginOperation: Operation {

    let userController: UserController

    init(userController: UserController) {
        self.userController = userController
    }

    override func execute() {
        // do your networking stuff
        userController.userID = receivedUserID
        finish()
    }

}

class CountryOperation: Operation {

    let userController: UserController

    init(userController: UserController) {
        self.userController = userController
    }

    override func execute() {
        let userID = userController.userID
        // continue
    }

}

and then:

let queue = OperationQueue()
let userController = UserController()
let login = LoginOperation(userController: userController)
let country = CountryOperation(userController: userController)
country.addDependency(login)
queue.addOperation(login)
queue.addOperation(country)

The problem with that solution is that it’s pretty complex and incredibly hard to test. And Swift also doesn’t like mutable shared state much.

2. Initialize operations with functions instead of values (Swifty-way)

Well, this is not obvious but amazingly elegant solution. As you saw, values get copied on initialization time. What we really need is to retrieve the needed value on execution time (as with previous example), but without such tight coupling. Well, this can be done easily - just pass a function to an initializer!

class LoginOperation: Operation {

    private(set) var userID: String?

    override func execute() {
        // do your networking stuff
        let receivedUserID = "1"
        userID = receivedUserID
        finish()
    }

}

class CountryOperation: Operation {

    private let getUserID: () -> String

    init(getUserID: () -> String) {
        self.getUserID = getUserID
    }

    override func execute() {
        /* Because this operation depends on `LoginOperation`,
        `getUserID()` is called after `LoginOperation` is finished. */
        let userID = self.getUserID()
        // continue
    }

}

and then:

let queue = OperationQueue()
let login = LoginOperation()
// Force-unwrap is no good, of course, but for this example it's ok
let country = CountryOperation(getUserID: { login.userID! })
country.addDependency(login)
queue.addOperation(login)
queue.addOperation(country)

As you see, no tight coupling, no shared mutable state (CountryOperation can only read, but not write — which is good). And it’s ridiculously easy to test: just pass whatever you want to CountryOperation initializer, you don’t need LoginOperation at all to test it!

With that approach, actually, the main goal of NSOperation is achieved: distinctive, abstract pieces of work. CountryOperation itself doesn’t know anything about LoginOperation, nor about any other shared instance.

I actually use that approach a lot in my projects and it works amazingly.

Please reply if you have any further questions :)