SomeOperation went isFinished=YES without being started by the queue it is in
public class SomeOperation : AsyncOperation {
//MARK: Start
public override func start() {
isExecuting = true
guard !isCancelled else {
markAsCompleted() //isExecuting = false, isFinished = true
return
}
doSomethingAsynchronously { [weak self] in
self?.markAsCompleted() //isExecuting = false, isFinished = true
}
}
//MARK: Cancel
public override func cancel() {
super.cancel()
markAsCompleted() //isExecuting = false, isFinished = true
}
}
//someOperation is a property in a class
if let someOperation = someOperation {
queue.addOperation(someOperation)
}
//Based on some condition cancelling it
someOperation?.cancel()
public override func cancel() {
isExecuting = true //Just in case the operation was cancelled before starting
super.cancel()
markAsCompleted()
}
markAsCompleted
sets isExecuting = false
and isFinished = true
isExecuting
, isFinished
are properties which are synchronised KVO
The key problem is that your markAsCompleted
is triggering isFinished
when the operation is not isExecuting
. I'd suggest you just fix that markAsCompleted
to only do this if isExecuting
is true. This reduces the burden on subclasses doing any complicated state tests to figure out whether they need to transition to isFinished
or not.
That having been said, I see three basic patterns when writing cancelable asynchronous operations:
If I'm dealing with some pattern where the canceling of the task will prevent it from transitioning executing operations to a isFinished
state.
In that case, I must have cancel
implementation manually finish the executing operations. For example:
class FiveSecondOperation: AsynchronousOperation {
var block: DispatchWorkItem?
override func main() {
block = DispatchWorkItem { [weak self] in
self?.finish()
self?.block = nil
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: block!)
}
override func cancel() {
super.cancel()
if isExecuting {
block?.cancel()
finish()
}
}
}
Focusing on the cancel
implementation, because if I cancel the DispatchWorkItem
it won't finish the operation, I therefore need to make sure that cancel
will explicitly finish the operation itself.
Sometimes, when you cancel some asynchronous task, it will call its completion handler automatically for you, in which case cancel
doesn't need to do anything other than cancel the that task and call super. For example:
class GetOperation: AsynchronousOperation {
var url: URL
weak var task: URLSessionTask?
init(url: URL) {
self.url = url
super.init()
}
override func main() {
let task = URLSession.shared.dataTask(with: url) { data, _, error in
defer { self.finish() } // make sure to finish the operation
// process `data` & `error` here
}
task.resume()
self.task = task
}
override func cancel() {
super.cancel()
task?.cancel()
}
}
Again, focusing on cancel
, in this case we don't touch the "finished" state, but just cancel dataTask
(which will call its completion handler even if you cancel the request) and call the super
implementation.
The third scenario is where you have some operation that is periodically checking isCancelled
state. In that case, you don't have to implement cancel
at all, as the default behavior is sufficient. For example:
class DisplayLinkOperation: AsynchronousOperation {
private weak var displayLink: CADisplayLink?
private var startTime: CFTimeInterval!
private let duration: CFTimeInterval = 2
override func main() {
startTime = CACurrentMediaTime()
let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
displayLink.add(to: .main, forMode: .commonModes)
self.displayLink = displayLink
}
@objc func handleDisplayLink(_ displayLink: CADisplayLink) {
let percentComplete = (CACurrentMediaTime() - startTime) / duration
if percentComplete >= 1.0 || isCancelled {
displayLink.invalidate()
finish()
}
// now do some UI update based upon `elapsed`
}
}
In this case, where I've wrapped a display link in an operation so I can manage dependencies and/or encapsulate the display link in a convenient object, I don't have to implement cancel
at all, because the default implementation will update isCancelled
for me, and I can just check for that.
Those are three basic cancel
patterns I generally see. That having been said, updating markAsCompleted
to only trigger isFinished
if isExecuting
is a good safety check to make sure you can never get the problem you described.
By the way, the AsynchronousOperation
that I used for the above examples is as follows, adapted from Trying to Understand Asynchronous Operation Subclass. BTW, what you called markAsCompleted
is called finish
, and it sounds like you're triggering the isFinished
and isExecuting
KVO via a different mechanism, but the idea is basically the same. Just check the current state before you trigger isFinished
KVO:
open class AsynchronousOperation: Operation {
/// State for this operation.
@objc private enum OperationState: Int {
case ready
case executing
case finished
}
/// Concurrent queue for synchronizing access to `state`.
private let stateQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".rw.state", attributes: .concurrent)
/// Private backing stored property for `state`.
private var rawState: OperationState = .ready
/// The state of the operation
@objc private dynamic var state: OperationState {
get { return stateQueue.sync { rawState } }
set { stateQueue.sync(flags: .barrier) { rawState = newValue } }
}
// MARK: - Various `Operation` properties
open override var isReady: Bool { return state == .ready && super.isReady }
public final override var isExecuting: Bool { return state == .executing }
public final override var isFinished: Bool { return state == .finished }
public final override var isAsynchronous: Bool { return true }
// MARK: - KVN for dependent properties
open override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
if ["isReady", "isFinished", "isExecuting"].contains(key) {
return [#keyPath(state)]
}
return super.keyPathsForValuesAffectingValue(forKey: key)
}
// MARK: - Foundation.Operation
public final override func start() {
if isCancelled {
state = .finished
return
}
state = .executing
main()
}
/// Subclasses must implement this to perform their work and they must not call `super`. The default implementation of this function throws an exception.
open override func main() {
fatalError("Subclasses must implement `main`.")
}
/// Call this function to finish an operation that is currently executing
public final func finish() {
if isExecuting { state = .finished }
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With