Skip to content

Instantly share code, notes, and snippets.

@ole
Created August 19, 2018 16:47
Show Gist options
  • Save ole/5034ce19c62d248018581b1db0eabb2b to your computer and use it in GitHub Desktop.
Save ole/5034ce19c62d248018581b1db0eabb2b to your computer and use it in GitHub Desktop.
An (NS)Operation subclass for async operations
import Foundation
/// An abstract class that makes building simple asynchronous operations easy.
/// Subclasses must override `main()` to perform any work and call `finish()`
/// when they are done. All `NSOperation` work will be handled automatically.
///
/// Source/Inspiration: https://stackoverflow.com/a/48104095/116862 and https://gist.github.com/calebd/93fa347397cec5f88233
open class AsyncOperation: Operation {
public init(name: String? = nil) {
super.init()
self.name = name
}
/// Serial queue for making state changes atomic under the constraint
/// of having to send KVO willChange/didChange notifications.
private let stateChangeQueue = DispatchQueue(label: "com.olebegemann.AsyncOperation.stateChange")
/// Private backing store for `state`
private var _state: Atomic<State> = Atomic(.ready)
/// The state of the operation
private var state: State {
get {
return _state.value
}
set {
// A state mutation should be a single atomic transaction. We can't simply perform
// everything on the isolation queue for `_state` because the KVO willChange/didChange
// notifications have to be sent from outside the isolation queue. Otherwise we would
// deadlock because KVO observers will in turn try to read `state` (by calling
// `isReady`, `isExecuting`, `isFinished`. Use a second queue to wrap the entire
// transaction.
stateChangeQueue.sync {
// Retrieve the existing value first. Necessary for sending fine-grained KVO
// willChange/didChange notifications only for the key paths that actually change.
let oldValue = _state.value
guard newValue != oldValue else {
return
}
willChangeValue(forKey: oldValue.objcKeyPath)
willChangeValue(forKey: newValue.objcKeyPath)
_state.mutate {
$0 = newValue
}
didChangeValue(forKey: oldValue.objcKeyPath)
didChangeValue(forKey: newValue.objcKeyPath)
}
}
}
/// Mirror of the possible states an (NS)Operation can be in
private enum State: Int, CustomStringConvertible {
case ready
case executing
case finished
/// The `#keyPath` for the `Operation` property that's associated with this value.
var objcKeyPath: String {
switch self {
case .ready: return #keyPath(isReady)
case .executing: return #keyPath(isExecuting)
case .finished: return #keyPath(isFinished)
}
}
var description: String {
switch self {
case .ready: return "ready"
case .executing: return "executing"
case .finished: return "finished"
}
}
}
public final override var isAsynchronous: Bool { return true }
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
}
// MARK: - Foundation.Operation
public final override func start() {
guard !isCancelled else {
finish()
return
}
state = .executing
main()
}
// MARK: - Public
/// Subclasses must implement this to perform their work and they must not call `super`.
/// The default implementation of this function traps.
open override func main() {
preconditionFailure("Subclasses must implement `main`.")
}
/// Call this function to finish an operation that is currently executing.
/// State can also be "ready" here if the operation was cancelled before it started.
public final func finish() {
if isExecuting || isReady {
state = .finished
}
}
open override var description: String {
return debugDescription
}
open override var debugDescription: String {
return "\(type(of: self)) — \(name ?? "nil") – \(isCancelled ? "cancelled" : String(describing: state))"
}
}
import Dispatch
/// A wrapper for atomic read/write access to a value.
/// The value is protected by a serial `DispatchQueue`.
public final class Atomic<A> {
private var _value: A
private let queue: DispatchQueue
/// Creates an instance of `Atomic` with the specified value.
///
/// - Paramater value: The object's initial value.
/// - Parameter targetQueue: The target dispatch queue for the "lock queue".
/// Use this to place the atomic value into an existing queue hierarchy
/// (e.g. for the subsystem that uses this object).
/// See Apple's WWDC 2017 session 706, Modernizing Grand Central Dispatch
/// Usage (https://developer.apple.com/videos/play/wwdc2017/706/), for
/// more information on how to use target queues effectively.
///
/// The default value is `nil`, which means no target queue will be set.
public init(_ value: A, targetQueue: DispatchQueue? = nil) {
_value = value
queue = DispatchQueue(label: "com.olebegemann.Atomic", target: targetQueue)
}
/// Read access to the wrapped value.
public var value: A {
return queue.sync { _value }
}
/// Mutations of `value` must be performed via this method.
///
/// If `Atomic` exposed a setter for `value`, constructs that used the getter
/// and setter inside the same statement would not be atomic.
///
/// Examples that would not actually be atomic:
///
/// let atomicInt = Atomic(42)
/// // Calls getter and setter, but value may have been mutated in between
/// atomicInt.value += 1
///
/// let atomicArray = Atomic([1,2,3])
/// // Mutating the array through a subscript causes both a get and a set,
/// // acquiring and releasing the lock twice.
/// atomicArray[1] = 42
///
/// See also: https://github.com/ReactiveCocoa/ReactiveSwift/issues/269
public func mutate(_ transform: (inout A) -> Void) {
queue.sync {
transform(&_value)
}
}
}
extension Atomic: Equatable where A: Equatable {
public static func ==(lhs: Atomic, rhs: Atomic) -> Bool {
return lhs.value == rhs.value
}
}
extension Atomic: Hashable where A: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(value)
}
}
@ole
Copy link
Author

ole commented Jan 6, 2022

@LeonidKokhnovich If I remember correctly (it's been a while), I was referring specifically to this comment: swiftlang/swift#20103 (comment)

The only safe work around is to create a main.swift file, like suggested above #20103 (comment).

E.g.

  1. Remove the @UIApplicationMain decorator from your AppDelegate

  2. Create a main.swift file:

import UIKit
import Foundation

private class CrashWorkaround: NSObject {
    @objc dynamic var test: String = ""

    /// More info: https://github.com/apple/swift/pull/20103
    fileprivate static func workaroundSwiftKVOCrash() {
        assert(Thread.isMainThread)
        let obj = CrashWorkaround()
        let observer = obj.observe(\.test, options: [.new]) { (_, _) in
            fatalError("Shouldn't be called, value doesn't change")
        }
        observer.invalidate()
    }
}

/// This needs to execute before anything can register KVO in the background
/// so the best way to do this is before the application ever runs
CrashWorkaround.workaroundSwiftKVOCrash()
UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, NSStringFromClass(AppDelegate.self))

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