Operation isFinished always false - ios

I'm trying to save my data to plist file using Operation.
I want to implement an asynchronous saving, so I've override the start()
But when I try to check wether saving have finished isFinished remains false.
However data has saved successfully, as I planned
class OperationDataManager: Operation {
var user: AppUser?
override func start() {
if let plist = Plist(name: "userFile") {
if let dict = plist.getMutablePlistFile() {
dict["userName"] = user?.userName
dict["userInfo"] = user?.userDescription
dict["userColor"] = NSKeyedArchiver.archivedData(withRootObject: user?.userColor)
dict["userImage"] = UIImagePNGRepresentation((user?.userImage)!)
do {
try plist.addValuesToPlistFile(dictionary: dict)
} catch {
print(error)
}
}
}
if isFinished == true {
print("Operation: finished")
} else {
print("Operation: not finished")
}
}
}
What's wrong?

isFinished isn't set to true until after main completes. It will never be true inside start or main.
There's no need for your operation to check isFinished. It is finished when main gets to the end.

Related

EXC_BAD_ACCESS KERN_INVALID_ADDRESS crash in addOperation of OperationQueue

I have asynchronous operation implementation like this:
class AsyncOperation: Operation {
private enum State: String {
case ready, executing, finished
fileprivate var keyPath: String {
return "is\(rawValue.capitalized)"
}
}
private let stateAccessQueue = DispatchQueue(label: "com.beamo.asyncoperation.state")
private var unsafeState = State.ready
private var state: State {
get {
return stateAccessQueue.sync {
return unsafeState
}
}
set {
willChangeValue(forKey: newValue.keyPath)
stateAccessQueue.sync(flags: [.barrier]) {
self.unsafeState = newValue
}
didChangeValue(forKey: newValue.keyPath)
}
}
override var isReady: Bool {
return super.isReady && state == .ready
}
override var isExecuting: Bool {
return state == .executing
}
override var isFinished: Bool {
return state == .finished
}
override var isAsynchronous: Bool {
return true
}
override func start() {
if isCancelled {
if !isFinished {
finish()
}
return
}
state = .executing
main()
}
public final func finish() {
state = .finished
}
}
App will download multiple resources with API. Implementation of downloader is managed by operation queue and custom asynchronous operation. Here is implementation of asynchronous downloader operation:
final class DownloaderOperation: AsyncOperation {
private let downloadTaskAccessQueue = DispatchQueue(label: "com.download.task.access.queue")
private var downloadTask: URLSessionDownloadTask?
private var threadSafeDownloadTask: URLSessionDownloadTask? {
get {
return downloadTaskAccessQueue.sync {
return downloadTask
}
}
set {
downloadTaskAccessQueue.sync(flags: [.barrier]) {
self.downloadTask = newValue
}
}
}
private let session: URLSession
let download: Download
let progressHandler: DownloadProgressHandler
init(session: URLSession,
download: Download,
progressHandler: #escaping DownloadProgressHandler) {
self.session = session
self.download = download
self.progressHandler = progressHandler
}
override func main() {
let bodyModel = DownloaderRequestBody(fileUrl: download.fileUrl.absoluteString)
let bodyData = (try? JSONEncoder().encode(bodyModel)) ?? Data()
var request = URLRequest(url: URL(string: "api url here")!)
request.httpMethod = "POST"
request.httpBody = bodyData
let task = session.downloadTask(with: request)
task.countOfBytesClientExpectsToSend = 512
task.countOfBytesClientExpectsToSend = 1 * 1024 * 1024 * 1024 // 1GB
task.resume()
threadSafeDownloadTask = task
DispatchQueue.main.async {
self.download.progress = 0
self.download.status = .inprogress
self.progressHandler(self.model, 0)
}
}
override func cancel() {
super.cancel()
threadSafeDownloadTask?.cancel()
}
}
And downloader implementation is here:
final class MultipleURLSessionDownloader: NSObject {
private(set) var unsafeOperations: [Download: DownloaderOperation] = [:]
private(set) var progressHandlers: [Download: DownloadProgressHandler] = [:]
private(set) var completionHandlers: [Download: DownloadCompletionHandler] = [:]
private let operationsAccessQueue = DispatchQueue(label: "com.multiple.downloader.operations")
private(set) var operations: [Download: DownloaderOperation] {
get {
return operationsAccessQueue.sync {
return unsafeOperations
}
}
set {
operationsAccessQueue.sync(flags: [.barrier]) {
self.unsafeOperations = newValue
}
}
}
private lazy var downloadOperationQueue: OperationQueue = {
var queue = OperationQueue()
queue.name = "com.multiple.downloader.queue"
queue.maxConcurrentOperationCount = 2
return queue
}()
private(set) var urlSession = URLSession.shared
init(configuration: URLSessionConfiguration = URLSessionConfiguration.background(withIdentifier: "com.multiple.downloader.session")) {
super.init()
configuration.isDiscretionary = false
configuration.sessionSendsLaunchEvents = true
configuration.timeoutIntervalForResource = 120
configuration.timeoutIntervalForRequest = 10
self.urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}
deinit {
debugPrint("[MultipleURLSessionDownloader] deinit")
}
func startDownload(downloads: [Download],
progressHandler: #escaping DownloadProgressHandler,
completionHandler: #escaping DownloadCompletionHandler) {
for download in downloads {
startDownload(
download: download,
progressHandler: progressHandler,
completionHandler: completionHandler
)
}
}
func cancelDownload(download: Download) {
cancelOperation(download: download)
}
func cancelAllDownloads() {
for download in operations.keys {
cancelOperation(download: download)
}
urlSession.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in
dataTasks.forEach {
$0.cancel()
}
uploadTasks.forEach {
$0.cancel()
}
downloadTasks.forEach {
$0.cancel()
}
}
}
private func startDownload(download: Download,
progressHandler: #escaping DownloadProgressHandler,
completionHandler: #escaping DownloadCompletionHandler) {
download.status = .default
progressHandlers[download] = progressHandler
completionHandlers[download] = completionHandler
if let operation = operations[download] {
if operation.isExecuting,
let inProgressDownload = operations.keys.first(where: { $0.id == download.id }) {
progressHandlers[download]?(inProgressDownload, inProgressDownload.progress ?? 0)
}
} else {
let operation = DownloaderOperation(
session: urlSession,
download: download) {[weak self] (progressDownload, progress) in
self?.progressHandlers[progressDownload]?(progressDownload, progress)
}
operations[download] = operation
downloadOperationQueue.addOperation(operation)
}
}
private func cancelOperation(download: Download) {
download.status = .cancelled
operations[download]?.cancel()
callCompletion(download: download, error: nil)
}
private func callCompletion(download: Download,
error: DownloadError?) {
operations[download]?.finish()
operations[download] = nil
progressHandlers[download] = nil
download.progress = nil
let handler = completionHandlers[download]
completionHandlers[download] = nil
handler?(download, error)
}
}
There is 2 cancel method:
cancelDownload(download: Download)
cancelAllDownloads()
If download is canceled and tried to download multiple times app is crashed and crash log is here:
If download is cancelledAll and tried to download multiple times app is crashed and crash log is here:
And the strange thing here is if I open app by running from Xcode the crash is not happened.
This is happening only when if I open app from device without running by Xcode.
For now I fixed replecaing operation queue with dispatch queue like this:
downloadOperationQueue.addOperation(operation)
downloadDispatchQueue.async {
operation.start()
}
And this is working fine without any crash.
I think crash is happening on addOperation method of OperationQueue, but I don't know the reason.
It does not make much sense to use operations in conjunction with a background URLSession. The whole idea of background URLSession is network requests proceed if the user leaves the app, and even if the app has been terminated in the course of its normal lifecycle (e.g., jettisoned due to memory pressure). But an operation queue cannot survive the termination of an app. If you really want to use operations with background session, you will need to figure out how you will handle network requests that are still underway, but for which there is no longer any operation to represent that request.
Let us set that aside and look at the operation and diagnose the crash.
A critical issue is the isFinished KVO when the operation is canceled. The documentation is very clear that if the operation has started and is subsequently canceled, you must perform the isFinished KVO notifications. If the task not yet started. If you do that, you can receive a warning:
went isFinished=YES without being started by the queue it is in
That would seem to suggest that one should not issue isFinished KVO notifications if an unstarted operation is canceled. But, note you absolutely must set the underlying state to finished, or else the unstarted canceled operations will be retained by the operation queue.
But what is worse, that performing the isFinished KVO notifications for unstarted operations it can lead to NSKeyValueDidChangeWithPerThreadPendingNotifications in the stack trace, just like you showed in your question. Here is my stack trace from my crash:
So, all of that said, I discovered two work-arounds:
Use locks for synchronization rather than GCD. They are more performant, anyway, and in my experiments, avoid the crash. (This is an unsatisfying solution because it is unclear as to whether it really solved the root problem, or just moved the goal-posts sufficiently, such that the crash no longer manifests itself.)
Alternatively, when you set the state to .finished, only issue the isFinished KVO if the operation was currently running. (This is also a deeply unsatisfying solution, as it is contemplated nowhere in Apple’s documentation. But it silences the above warning and eliminates the crash.)
For example:
func finish() {
if isExecuting {
state = .finished // change state with KVO
} else {
synchronized { _state = .finished } // change state without KVO
}
}
Basically, that sets the state to finished either way, with KVO notifications if the operation is executing, and without if not yet started when it is canceled.
So, you might end up with:
open class AsynchronousOperation: Operation {
override open var isReady: Bool { super.isReady && state == .ready }
override open var isExecuting: Bool { state == .executing }
override open var isFinished: Bool { state == .finished }
override open var isAsynchronous: Bool { true }
private let lock = NSLock()
private var _state: State = .ready
private var state: State {
get { synchronized { _state } }
set {
let oldValue = state
guard oldValue != .finished else { return }
willChangeValue(forKey: oldValue.keyPath)
willChangeValue(forKey: newValue.keyPath)
synchronized { _state = newValue }
didChangeValue(forKey: newValue.keyPath)
didChangeValue(forKey: oldValue.keyPath)
}
}
override open func start() {
if isCancelled {
finish()
return
}
state = .executing
main()
}
open func finish() {
state = .finished
}
}
// MARK: - State
private extension AsynchronousOperation {
enum State: String {
case ready, executing, finished
fileprivate var keyPath: String {
return "is\(rawValue.capitalized)"
}
}
}
// MARK: - Private utility methods
private extension AsynchronousOperation {
func synchronized<T>(block: () throws -> T) rethrows -> T {
lock.lock()
defer { lock.unlock() }
return try block()
}
}
A few notes on the above:
If we refer to Listing 2-7 the old Concurrency Programming Guide: Operations, they illustrate (in Objective-C) the correct KVO notifications that must take place when we, for example, transition from “executing” to “finished”:
- (void)completeOperation {
[self willChangeValueForKey:#"isFinished"];
[self willChangeValueForKey:#"isExecuting"];
executing = NO;
finished = YES;
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
}
So, as we transition from executing to finished, we have to perform the notification for both isExecuting and isFinished. But your state setter is only performing the KVO for the newValue (thus, only isFinished, neglecting to perform the isExecuting-related KVO notifications). This is likely unrelated to your problem at hand, but is important, nonetheless.
If you were to synchronize with GCD serial queue, the barrier becomes redundant. This lingering barrier was probably a legacy of a previous rendition using a reader-writer pattern. IMHO, the transition to a serial queue was prudent (as the reader-writer just introduces more problems than its negligible performance difference warrants). But if we eliminate the concurrent queue, we should also remove the redundant barrier. Or just eliminate GCD altogether as shown above.
I guard against state changes after the operation is already finished. This is a bit of defensive-programming adapted from Apple’s “Advanced NSOperations” implementation (which, admittedly, is no longer available on Apple’s site).
I would not recommend making finish a final function. While unlikely, it is possible that a subclass might want to add functionality. So I removed final.
I moved the GCD synchronization code into its own method, synchronized, so that I could easily switch between different synchronization mechanisms.
Given that Operation is an open class, I did the same here.

Swift Local Notifications Checker Main Thread Only

I have a method with a completion that returns a bool. But I cant figure out how to get around this error.
Error
UISwitch.isOn must be used from main thread only
Button Action
#IBAction func notificationSwitch(_ sender: Any) {
LocalNotification().checkEnabled(completion: { (success) -> Void in
// When download completes,control flow goes here.
if success == false{
print("Cant Turn On")
self.notificationToggle.isOn = false
} else {
print("Can Turn On")
if self.notificationToggle.isOn == true {
self.notificationToggle.isOn = false
} else {
self.notificationToggle.isOn = true
}
}
})
}
also already tried wrapping the LocalNotifications().... in DispatchQueue.main.async but still get the same error
You're almost there. It is not the checkEnabled that needs to be wrapped in the call to get onto the main thread, but the stuff "inside" it:
LocalNotification().checkEnabled(completion: { (success) -> Void in
DispatchQueue.main.async {
if success == false {

DispatchQueue Main async reference problem. (Reference gone)

I have a function for getting user session. I successfully get the session. But inside the DispatchQueue i lose task object (task:AWSTask< AWSCognitoIdentityUserSession >).
Before DispatchQueue it is not null but in the dispatch DispatchQueue it is null. Some how i am losing the reference.
What is the best way to get and object reference from outside of the DispatchQueue. (I dont want to create a general variable in the class.)
BTW this is not happening all the time.
var pool:AWSCognitoIdentityUserPool
override init(){
pool = AWSCognitoIdentityUserPool(forKey: "UserPool")
super.init()
}
func getUserPool() -> AWSCognitoIdentityUserPool {
return pool
}
func getUserSession(completition: #escaping () -> Void)
{
let user = pool.currentUser()!
let task = user.getSession()
task.continueWith{ (task:AWSTask<AWSCognitoIdentityUserSession>) in
{
DispatchQueue.main.async
{
if(task.result != nil && task.error == nil)
{
/*
There are some calculations here
*/
completition()
}
}
}
}
}

Waiting for completion handler to complete, before continuing

I have an array of 9 images and I'd like to save them all to the user's camera roll. You can do that with UIImageWriteToSavedPhotosAlbum. I wrote a loop to save each image. The problem with this is that for some reason, it will only save the first five. Now, order is important, so if an image fails to save, I want to retry and wait until it succeeds, rather than have some unpredictable race.
So, I implement a completion handler, and thought I could use semaphores like so:
func save(){
for i in (0...(self.imagesArray.count-1)).reversed(){
print("saving image at index ", i)
semaphore.wait()
let image = imagesArray[i]
self.saveImage(image)
}
}
func saveImage(_ image: UIImage){
UIImageWriteToSavedPhotosAlbum(image, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil)
}
func image(_ image: UIImage, didFinishSavingWithError error: NSError?, contextInfo: UnsafeRawPointer) {
//due to some write limit, only 5 images get written at once.
if let error = error {
print("trying again")
self.saveImage(image)
} else {
print("successfully saved")
semaphore.signal()
}
}
The problem with my code is it gets blocked out after the first save and semaphore.signal never gets called. I'm thinking my completion handler is supposed to be called on the main thread, but is already being blocked by the semaphore.wait(). Any help appreciated. Thanks
As others have pointed out, you want to avoid waiting on the main thread, risking deadlocking. So, while you can push it off to a global queue, the other approach is to employ one of the many mechanisms for performing a series of asynchronous tasks. Options include asynchronous Operation subclass or promises (e.g. PromiseKit).
For example, to wrap the image saving task in an asynchronous Operation and add them to an OperationQueue you could define your image save operation like so:
class ImageSaveOperation: AsynchronousOperation {
let image: UIImage
let imageCompletionBlock: ((NSError?) -> Void)?
init(image: UIImage, imageCompletionBlock: ((NSError?) -> Void)? = nil) {
self.image = image
self.imageCompletionBlock = imageCompletionBlock
super.init()
}
override func main() {
UIImageWriteToSavedPhotosAlbum(image, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil)
}
func image(_ image: UIImage, didFinishSavingWithError error: NSError?, contextInfo: UnsafeRawPointer) {
imageCompletionBlock?(error)
complete()
}
}
Then, assuming that you had an array, images, i.e. that was a [UIImage], you could then do:
let queue = OperationQueue()
queue.name = Bundle.main.bundleIdentifier! + ".imagesave"
queue.maxConcurrentOperationCount = 1
let operations = images.map {
return ImageSaveOperation(image: $0) { error in
if let error = error {
print(error.localizedDescription)
queue.cancelAllOperations()
}
}
}
let completion = BlockOperation {
print("all done")
}
operations.forEach { completion.addDependency($0) }
queue.addOperations(operations, waitUntilFinished: false)
OperationQueue.main.addOperation(completion)
You can obviously customize this to add retry logic upon error, but that is likely not needed now because the root of the "too busy" problem was a result of too many concurrent save requests, which we've eliminated. That only leaves errors that are unlikely to solved by retrying, so I probably wouldn't add retry logic. (The errors are more likely to be permissions failures, out of space, etc.) But you can add retry logic if you really want. More likely, if you have an error, you might want to just cancel all of the remaining operations on the queue, like I have above.
Note, the above subclasses AsynchronousOperation, which is just an Operation subclass for which isAsynchronous returns true. For example:
/// Asynchronous Operation base class
///
/// This class performs all of the necessary KVN of `isFinished` and
/// `isExecuting` for a concurrent `NSOperation` subclass. So, to developer
/// a concurrent NSOperation subclass, you instead subclass this class which:
///
/// - must override `main()` with the tasks that initiate the asynchronous task;
///
/// - must call `completeOperation()` function when the asynchronous task is done;
///
/// - optionally, periodically check `self.cancelled` status, performing any clean-up
/// necessary and then ensuring that `completeOperation()` is called; or
/// override `cancel` method, calling `super.cancel()` and then cleaning-up
/// and ensuring `completeOperation()` is called.
public class AsynchronousOperation : Operation {
private let syncQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".opsync")
override public var isAsynchronous: Bool { return true }
private var _executing: Bool = false
override private(set) public var isExecuting: Bool {
get {
return syncQueue.sync { _executing }
}
set {
willChangeValue(forKey: "isExecuting")
syncQueue.sync { _executing = newValue }
didChangeValue(forKey: "isExecuting")
}
}
private var _finished: Bool = false
override private(set) public var isFinished: Bool {
get {
return syncQueue.sync { _finished }
}
set {
willChangeValue(forKey: "isFinished")
syncQueue.sync { _finished = newValue }
didChangeValue(forKey: "isFinished")
}
}
/// Complete the operation
///
/// This will result in the appropriate KVN of isFinished and isExecuting
public func complete() {
if isExecuting { isExecuting = false }
if !isFinished { isFinished = true }
}
override public func start() {
if isCancelled {
isFinished = true
return
}
isExecuting = true
main()
}
}
Now, I appreciate operation queues (or promises) is going to seem like overkill for your situation, but it's a useful pattern that you can employ wherever you have a series of asynchronous tasks. For more information on operation queues, feel free to refer to the Concurrency Programming Guide: Operation Queues.
Year ago I faced Same problem
You should try to put your code in Dispatach.global queue it will surely help
Reason : I really don't know about reason too , I think it what semaphore it is , May be it is required to execute in Background Thread to sync wait and signal
As Mike alter mentioned, using a Dispatch.global().async helped solve the problem. The code now looks like this:
func save(){
for i in (0...(self.imagesArray.count-1)).reversed(){
DispatchQueue.global().async { [unowned self] in
self.semaphore.wait()
let image = self.imagesArray[i]
self.saveImage(image)
}
}
}
I suspect the problem was that the completion handler gets executed in the main thread, which was already locked by the semaphore.wait() called initially. So when the completion occurs, semaphore.signal() never gets called.
This is solved by running the tasks an asynchronous queue.

How can an NSOperationQueue wait for two async operations?

How can I make an NSOperationQueue (or anything else) wait for two async network calls with callbacks? The flow needs to look like this
Block Begins {
Network call with call back/block begins {
first network call is done
}
}
Second Block Begins {
Network call with call back/block begins {
second network call is done
}
}
Only run this block once the NETWORK CALLS are done {
blah
}
Here's what I have so far.
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
__block NSString *var;
[queue addOperation:[NSBlockOperation blockOperationWithBlock:^{
[AsyncReq get:^{
code
} onError:^(NSError *error) {
code
}];
}]];
[queue addOperation:[NSBlockOperation blockOperationWithBlock:^{
[AsyncReq get:^{
code
} onError:^(NSError *error) {
code
}];
}]];
[queue waitUntilAllOperationsAreFinished];
//do something with both of the responses
Do you have to use NSOperation Queue? Here's how you can do it w/ a dispatch group:
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
[AsyncReq get:^{
code
dispatch_group_leave(group);
} onError:^(NSError *error) {
code
dispatch_group_leave(group);
}];
dispatch_group_enter(group);
[AsyncReq get:^{
code
dispatch_group_leave(group);
} onError:^(NSError *error) {
code
dispatch_group_leave(group);
}];
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(#"Both operations completed!")
});
Using Grand Central Dispatch and DispatchGroup
With Swift 3, in the simplest cases where you don't need fine grained control on tasks states, you can use Grand Central Dispatch and DispatchGroup. The following Playground code shows how it works:
import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
let group = DispatchGroup()
group.enter()
// Perform some asynchronous operation
let queue1 = DispatchQueue(label: "com.example.imagetransform")
queue1.async {
print("Task One finished")
group.leave()
}
group.enter()
// Perform some asynchronous operation
let queue2 = DispatchQueue(label: "com.example.retrievedata")
queue2.async {
print("Task Two finished")
group.leave()
}
group.notify(queue: DispatchQueue.main, execute: { print("Task Three finished") })
The previous code will print "Task Three finished" once the two asynchronous tasks are both finished.
Using OperationQueue and Operation
Using OperationQueue and Operation for your request tasks requires more boilerplate code but offers many advantages like kvoed state and dependency.
1. Create an Operation subclass that will act as an abstract class
import Foundation
/**
NSOperation documentation:
Operation objects are synchronous by default.
At no time in your start method should you ever call super.
When you add an operation to an operation queue, the queue ignores the value of the asynchronous property and always calls the start method from a separate thread.
If you are creating a concurrent operation, you need to override the following methods and properties at a minimum:
start, asynchronous, executing, finished.
*/
open class AbstractOperation: Operation {
#objc enum State: Int {
case isReady, isExecuting, isFinished
func canTransition(toState state: State) -> Bool {
switch (self, state) {
case (.isReady, .isExecuting): return true
case (.isReady, .isFinished): return true
case (.isExecuting, .isFinished): return true
default: return false
}
}
}
// use the KVO mechanism to indicate that changes to `state` affect other properties as well
class func keyPathsForValuesAffectingIsReady() -> Set<NSObject> {
return [#keyPath(state) as NSObject]
}
class func keyPathsForValuesAffectingIsExecuting() -> Set<NSObject> {
return [#keyPath(state) as NSObject]
}
class func keyPathsForValuesAffectingIsFinished() -> Set<NSObject> {
return [#keyPath(state) as NSObject]
}
// A lock to guard reads and writes to the `_state` property
private let stateLock = NSLock()
private var _state = State.isReady
var state: State {
get {
stateLock.lock()
let value = _state
stateLock.unlock()
return value
}
set (newState) {
// Note that the KVO notifications MUST NOT be called from inside the lock. If they were, the app would deadlock.
willChangeValue(forKey: #keyPath(state))
stateLock.lock()
if _state == .isFinished {
assert(_state.canTransition(toState: newState), "Performing invalid state transition from \(_state) to \(newState).")
_state = newState
}
stateLock.unlock()
didChangeValue(forKey: #keyPath(state))
}
}
override open var isExecuting: Bool {
return state == .isExecuting
}
override open var isFinished: Bool {
return state == .isFinished
}
var hasCancelledDependencies: Bool {
// Return true if this operation has any dependency (parent) operation that is cancelled
return dependencies.reduce(false) { $0 || $1.isCancelled }
}
override final public func start() {
// If any dependency (parent operation) is cancelled, we should also cancel this operation
if hasCancelledDependencies {
finish()
return
}
if isCancelled {
finish()
return
}
state = .isExecuting
main()
}
open override func main() {
fatalError("This method has to be overriden and has to call `finish()` at some point")
}
open func didCancel() {
finish()
}
open func finish() {
state = .isFinished
}
}
2. Create your operations
import Foundation
open class CustomOperation1: AbstractOperation {
override open func main() {
if isCancelled {
finish()
return
}
// Perform some asynchronous operation
let queue = DispatchQueue(label: "com.app.serialqueue1")
let delay = DispatchTime.now() + .seconds(5)
queue.asyncAfter(deadline: delay) {
self.finish()
print("\(self) finished")
}
}
}
import Foundation
open class CustomOperation2: AbstractOperation {
override open func main() {
if isCancelled {
finish()
return
}
// Perform some asynchronous operation
let queue = DispatchQueue(label: "com.app.serialqueue2")
queue.async {
self.finish()
print("\(self) finished")
}
}
}
3. Usage
import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
// Declare operations
let operation1 = CustomOperation1()
let operation2 = CustomOperation2()
let operation3 = CustomOperation1()
// Set operation3 to perform only after operation1 and operation2 have finished
operation3.addDependency(operation2)
operation3.addDependency(operation1)
// Launch operations
let queue = OperationQueue()
queue.addOperations([operation2, operation3, operation1], waitUntilFinished: false)
With this code, operation3 is guaranteed to always be performed last.
You can find this Playground on this GitHub repo.
AsyncOperation maintains its state w.r.t to Operation own state.
Since the operations are of asynchronous nature. We need to explicitly define the state of our operation.
Because the execution of async operation returns immediately after it's called.
So in your subclass of AsyncOperation you just have to set the state of the operation as finished in your completion handler
class AsyncOperation: Operation {
public enum State: String {
case ready, executing, finished
//KVC of Operation class are
// isReady, isExecuting, isFinished
var keyPath: String {
return "is" + rawValue.capitalized
}
}
//Notify KVO properties of the new/old state
public var state = State.ready {
willSet {
willChangeValue(forKey: newValue.keyPath)
willChangeValue(forKey: state.keyPath)
}
didSet{
didChangeValue(forKey: oldValue.keyPath)
didChangeValue(forKey: state.keyPath)
}
}
}
extension AsyncOperation {
//have to make sure the operation is ready to maintain dependancy with other operation
//hence check with super first
override open var isReady: Bool {
return super.isReady && state == .ready
}
override open var isExecuting: Bool {
return state == .executing
}
override open var isFinished: Bool {
return state == .finished
}
override open func start() {
if isCancelled {
state = .finished
return
}
main()
state = .executing
}
override open func cancel() {
super.cancel()
state = .finished
} }
Now to call if from your own Operation class
Class MyOperation: AsyncOperation {
override main() {
request.send() {success: { (dataModel) in
//waiting for success closure to be invoked before marking the state as completed
self.state = .finished
}
}
}

Resources