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 :)