Skip to content

Instantly share code, notes, and snippets.

@parrots
Last active May 17, 2021 10:44
Show Gist options
  • Save parrots/f1a6ca9c9924905fd1bd12cfb640337a to your computer and use it in GitHub Desktop.
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)
//
// 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
}
}
@parrots
Copy link
Author

parrots commented Feb 18, 2020

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.

//
//  AsyncOperation.swift
//  Slopes
//
/* References:
https://github.com/richardtin/Advanced-NSOperations/blob/672023bc066eae6c3a9b2ed985b8c40a4e604a77/Earthquakes/Operations/Operation.swift
https://stackoverflow.com/a/48104095
*/
import Foundation
open class AsyncOperation: Operation {
    @objc private enum OperationState: Int {
        case ready
        case executing
        case finished
    }
    private let stateQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".rw.state", attributes: .concurrent)
    private var _state: OperationState = .ready
    @objc private dynamic var state: OperationState {
        get { return stateQueue.sync { _state } }
        set { stateQueue.async(flags: .barrier) { self._state = newValue } }
    }
    // MARK: - Various async-needed `NSOperation` 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 }
    open override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
        //iOS 10 uses ready, finished, and executing. Older versions observed the is- preixed ones
        if ["ready", "finished", "executing"].contains(key) {
            return [#keyPath(state)]
        }
        return super.keyPathsForValuesAffectingValue(forKey: key)
    }
    // MARK: - Execution-related methods
    public final override func start() {
        super.start()
        if isCancelled {
            state = .finished
            return
        }
        state = .executing
        execute()
    }
    open func execute() {
        fatalError("Subclasses must implement `execute`.")
    }
    public final func finish() {
        if !isFinished { state = .finished }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment