Last active
May 17, 2021 10:44
-
-
Save parrots/f1a6ca9c9924905fd1bd12cfb640337a to your computer and use it in GitHub Desktop.
A Swifty version of an AsyncOperation which solves two common problems I ran into. (see comment below for issues addressed)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// AsyncOperation.swift | |
// Slopes | |
// | |
import Foundation | |
class AsyncOperation: Operation { | |
private let stateLock = NSLock() | |
private var observers: [NSKeyValueObservation] = [NSKeyValueObservation]() | |
override func addDependency(_ op: Operation) { | |
isReady = false | |
super.addDependency(op) | |
let readyChecker: (Any, Any) -> Void = { [weak self] _, _ in | |
guard let self = self else { | |
return | |
} | |
if self.dependencies.filter({ !$0.isFinished && !$0.isCancelled }).isEmpty { | |
self.isReady = true | |
} | |
} | |
observers.append(op.observe(\.isFinished, options: [], changeHandler: readyChecker)) | |
observers.append(op.observe(\.isCancelled, options: [], changeHandler: readyChecker)) | |
} | |
deinit { | |
for observer in observers { | |
observer.invalidate() | |
} | |
} | |
private var _ready: Bool = true | |
override var isReady: Bool { | |
get { | |
return stateLock.withCriticalScope { _ready } | |
} | |
set { | |
willChangeValue(forKey: "isReady") | |
stateLock.withCriticalScope { | |
if _ready != newValue { | |
_ready = newValue | |
} | |
} | |
didChangeValue(forKey: "isReady") | |
} | |
} | |
private var _executing: Bool = false | |
override var isExecuting: Bool { | |
get { | |
return stateLock.withCriticalScope { _executing } | |
} | |
set { | |
willChangeValue(forKey: "isExecuting") | |
stateLock.withCriticalScope { | |
if _executing != newValue { | |
_executing = newValue | |
} | |
} | |
didChangeValue(forKey: "isExecuting") | |
} | |
} | |
private var _finished: Bool = false | |
override var isFinished: Bool { | |
get { | |
return stateLock.withCriticalScope { _finished } | |
} | |
set { | |
willChangeValue(forKey: "isFinished") | |
stateLock.withCriticalScope { | |
if _finished != newValue { | |
_finished = newValue | |
} | |
} | |
didChangeValue(forKey: "isFinished") | |
} | |
} | |
override func cancel() { | |
super.cancel() | |
if isExecuting { | |
isExecuting = false | |
} | |
} | |
override var isAsynchronous: Bool { | |
return true | |
} | |
} | |
extension NSLock { | |
func withCriticalScope<T>(block: () -> T) -> T { | |
lock() | |
let value = block() | |
unlock() | |
return value | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This async operation ran into issues with a new race condition in the bowls of NSOperationQueue in iOS 13 (was rock-solid on 12). Changed the recommended code to the following, which has been crash-free. It also formalizes a use of finish / execute setup.