CKOperationGroup specify an order and chain operations - ios

I am asking this question due to the lack of documentation on Apple's website. Is there any way to chain to CKOperation inside a CKOperationGroup to execute in order one after the other? In particular, I am interested in two CKModifyRecordsOperation that I need to execute one after the other.
Many Thanks

Instead of using CKOperation.completionBlock, how about using an OperationQueue? Here is what I came up with. It's completely untested, so let me know if it's useless.
class CKOperationQueue: NSObject {
private var completionHandler: (() -> Void)?
private var isExecuting = false
private let operations: [CKOperation]
private let queue: OperationQueue
init(operations: [CKOperation]) {
self.operations = operations
queue = OperationQueue()
queue.name = "CKOperationQueue"
queue.maxConcurrentOperationCount = 1
}
#discardableResult func execute() -> CKOperationQueue {
guard !isExecuting else { return self }
isExecuting = true
queue.addOperations(operations, waitUntilFinished: false)
queue.addOperation {
self.completionHandler?()
}
return self
}
#discardableResult func onCompletion(_ handler: #escaping () -> Void) -> CKOperationQueue {
self.completionHandler = handler
return self
}
}

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.

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()
}
}
}
}
}

OperationQueue / DispatchGroup and recursion

I have a problem understanding how to use GCD when using asynchronous, recursive calls to the API.
Below is one of the three similar methods that contains the same logic, just for different data and API endpoint. If there is no next page request the method should finish and next method should start.
How would I make sure that fetchItems2 gets called after fetchItems1 finishes, and fetchItems3 after fetchItems2?
private func fetchItems1(completion: #escaping (Error?) -> Void) {
var _items = [Item]()
func handleReceivedItemsPage(_ page: PagingObject<Item>, _completion: ((Error?) -> Void)?) {
let newItems = page.items!
_tracks.append(contentsOf: newTracks)
if page.canMakeNextRequest {
page.getNext(success: { nextPage in
handleReceivedItemsPage(nextPage)
}) { nextError in
_completion?(nextError)
}
} else {
// Finished, next method can now start
self.items = _items
_completion?(nil)
}
}
API.getSavedItems(success: { page in
handleReceivedItemsPage(page, _completion: completion)
}, failure: completion)
}
private func fetchItems2(completion: #escaping (Error?) -> Void)) { ... }
private func fetchItems3(completion: #escaping (Error?) -> Void)) { ... }
You can keep an extra variable that keeps track of when the API calls are complete. In the completion block, increment this variable. Then, when the variable reaches the amount of API calls complete, perform your task.
I would use DispatchGroup:
public void FetchItems(completion: #escaping (Error?) -> Void) {
let group = DispatchGroup()
group.enter()
fetchItems1() { error in
completion(error)
group.leave()
}
group.wait()
group.enter()
fetchItems2() { error in
completion(error)
group.leave()
}
// 3rd function call
}
Code after group.wait() is not called until the number of group.enter() and group.leave() invocations is equal.

How can I update a view's progress bar using the AppSync S3ObjectManager?

I'm using AWSAppSyncClient to upload files but I'm struggling to connect the upload progress hook with the view.
AWSAppSyncClient is a property of the the application delegate initialized with an S3ObjectManager. The object manager method upload has access to the upload progress via the AWSTransferUtilityUplaodExpression:
expression.progressBlock = {(task, progress) in
DispatchQueue.main.async(execute: {
// Can we update the controller's progress bar here?
print("Progress: \(Float(progress.fractionCompleted))")
})
}
My controller invokes the upload by calling perform:
var appSyncClient: AWSAppSyncClient? // retrieved from the app delegate singleton
appSyncClient?.perform(mutation: CreatePostMutation(input: input)) { (result, error) in ...
What I am struggling with: how do I provide the S3ObjectManager a reference to the controller? I thought of instantiating the AWSAppSyncClient in each controller, and maybe using some sort of delegate pattern?
It's probably overkill to instantiate a new client on each view controller. Setup & teardown take a bit of time & system resources to perform, and you'd probably prefer to keep those activities separate from the view controller in any case, just for separation of responsibilities.
There isn't really a good way of registering a per-object listener, since mutations are queued for eventual, asynchronous delivery. Your delegate idea seems like the best approach at this point.
NOTE: Code below is untested, and not thread-safe.
For example, you could declare a singleton delegate that manages watchers for individual views that need to report progress:
class AppSyncS3ObjectManagerProgressWatcher {
typealias ProgressSubscription = UUID
static let shared = AppSyncS3ObjectManagerProgressWatcher()
private var watchers = [UUID: AppSyncS3ObjectManagerProgressDelegate?]()
func add(_ watcher: AppSyncS3ObjectManagerProgressDelegate) -> ProgressSubscription {
let subscription = UUID()
weak var weakWatcher = watcher
watchers[subscription] = weakWatcher
return subscription
}
func remove(_ subscription: ProgressSubscription?) {
guard let subscription = subscription else {
return
}
watchers[subscription] = nil
}
}
extension AppSyncS3ObjectManagerProgressWatcher: AppSyncS3ObjectManagerProgressDelegate {
func progressReportingExpression(forDownloadingObject object: AWSS3ObjectProtocol) -> AWSS3TransferUtilityDownloadExpression {
let expression = AWSS3TransferUtilityDownloadExpression()
expression.progressBlock = { _, progress in
self.didReportProgress(forDownloadingObject: object, progress: progress)
}
return expression
}
func progressReportingExpression(forUploadingObject object: AWSS3ObjectProtocol & AWSS3InputObjectProtocol) -> AWSS3TransferUtilityUploadExpression {
let expression = AWSS3TransferUtilityUploadExpression()
expression.progressBlock = { _, progress in
self.didReportProgress(forUploadingObject: object, progress: progress)
}
return expression
}
func didReportProgress(forDownloadingObject object: AWSS3ObjectProtocol, progress: Progress) {
for watcher in watchers.values {
watcher?.didReportProgress(forDownloadingObject: object, progress: progress)
}
}
func didReportProgress(forUploadingObject object: AWSS3ObjectProtocol & AWSS3InputObjectProtocol, progress: Progress) {
for watcher in watchers.values {
watcher?.didReportProgress(forUploadingObject: object, progress: progress)
}
}
}
Wherever you conform S3TransferUtility to S3ObjectManager, you would do something like:
extension AWSS3TransferUtility: AWSS3ObjectManager {
public func download(s3Object: AWSS3ObjectProtocol, toURL: URL, completion: #escaping ((Bool, Error?) -> Void)) {
let completionBlock: AWSS3TransferUtilityDownloadCompletionHandlerBlock = { task, url, data, error -> Void in
if let _ = error {
completion(false, error)
} else {
completion(true, nil)
}
}
let progressReportingExpression = AppSyncS3ObjectManagerProgressWatcher
.shared
.progressReportingExpression(forDownloadingObject: s3Object)
let _ = self.download(
to: toURL,
bucket: s3Object.getBucketName(),
key: s3Object.getKeyName(),
expression: progressReportingExpression,
completionHandler: completionBlock)
}
public func upload(s3Object: AWSS3ObjectProtocol & AWSS3InputObjectProtocol, completion: #escaping ((_ success: Bool, _ error: Error?) -> Void)) {
let completionBlock : AWSS3TransferUtilityUploadCompletionHandlerBlock = { task, error -> Void in
if let _ = error {
completion(false, error)
} else {
completion(true, nil)
}
}
let progressReportingExpression = AppSyncS3ObjectManagerProgressWatcher
.shared
.progressReportingExpression(forUploadingObject: s3Object)
let _ = self.uploadFile(
s3Object.getLocalSourceFileURL()!,
bucket: s3Object.getBucketName(),
key: s3Object.getKeyName(),
contentType: s3Object.getMimeType(),
expression: progressReportingExpression,
completionHandler: completionBlock
).continueWith { (task) -> Any? in
if let err = task.error {
completion(false, err)
}
return nil
}
}
}
And then in the progress reporting view:
override func awakeFromNib() {
super.awakeFromNib()
progressSubscription = AppSyncS3ObjectManagerProgressWatcher.shared.add(self)
}
func didReportProgress(forUploadingObject object: AWSS3InputObjectProtocol & AWSS3ObjectProtocol, progress: Progress) {
// TODO: Filter by object local URI/key/etc to ensure we're updating the correct progress
print("Progress received for \(object.getKeyName()): \(progress.fractionCompleted)")
self.progress = progress
}
As I noted, this code is untested, but it should outline a general approach for you to start from. I'd welcome your feedback and would like to hear what approach you eventually settle on.
Finally, please feel free to open a feature request on our issues page: https://github.com/awslabs/aws-mobile-appsync-sdk-ios/issues

How to achieve DispatchGroup functionality using OperationQueue? [duplicate]

I have an Operation subclass and Operation queue with maxConcurrentOperationCount = 1.
This performs my operations in a sequential order that i add them which is good but now i need to wait until all operations have finished before running another process.
i was trying to use notification group but as this is run in a for loop as soon as the operations have been added to the queue the notification group fires.. How do i wait for all operations to leave the queue before running another process?
for (index, _) in self.packArray.enumerated() {
myGroup.enter()
let myArrayOperation = ArrayOperation(collection: self.outerCollectionView, id: self.packArray[index].id, count: index)
myArrayOperation.name = self.packArray[index].id
downloadQueue.addOperation(myArrayOperation)
myGroup.leave()
}
myGroup.notify(queue: .main) {
// do stuff here
}
You can use operation dependencies to initiate some operation upon the completion of a series of other operations:
let queue = OperationQueue()
let completionOperation = BlockOperation {
// all done
}
for object in objects {
let operation = ...
completionOperation.addDependency(operation)
queue.addOperation(operation)
}
OperationQueue.main.addOperation(completionOperation) // or, if you don't need it on main queue, just `queue.addOperation(completionOperation)`
Or, in iOS 13 and later, you can use barriers:
let queue = OperationQueue()
for object in objects {
queue.addOperation(...)
}
queue.addBarrierBlock {
DispatchQueue.main.async {
// all done
}
}
A suitable solution is KVO
First before the loop add the observer (assuming queue is the OperationQueue instance)
queue.addObserver(self, forKeyPath:"operations", options:.new, context:nil)
Then implement
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if object as? OperationQueue == queue && keyPath == "operations" {
if queue.operations.isEmpty {
// Do something here when your queue has completed
self.queue.removeObserver(self, forKeyPath:"operations")
}
} else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
Edit:
In Swift 4 it's much easier
Declare a property:
var observation : NSKeyValueObservation?
and create the observer
observation = queue.observe(\.operationCount, options: [.new]) { [unowned self] (queue, change) in
if change.newValue! == 0 {
// Do something here when your queue has completed
self.observation = nil
}
}
Since iOS13 and macOS15 operationCount is deprecated. The replacement is to observe progress.completedUnitCount.
Another modern way is to use the KVO publisher of Combine
var cancellable: AnyCancellable?
cancellable = queue.publisher(for: \.progress.completedUnitCount)
.filter{$0 == queue.progress.totalUnitCount}
.sink() { _ in
print("queue finished")
self.cancellable = nil
}
I use the next solution:
private let queue = OperationQueue()
private func addOperations(_ operations: [Operation], completionHandler: #escaping () -> ()) {
DispatchQueue.global().async { [unowned self] in
self.queue.addOperations(operations, waitUntilFinished: true)
DispatchQueue.main.async(execute: completionHandler)
}
}
Set the maximum number of concurrent operations to 1
operationQueue.maxConcurrentOperationCount = 1
then each operation will be executed in order (as if each was dependent on the previous one) and your completion operation will execute at the end.
Code at the end of the queue
refer to this link
NSOperation and NSOperationQueue are great and useful Foundation framework tools for asynchronous tasks. One thing puzzled me though: How can I run code after all my queue operations finish? The simple answer is: use dependencies between operations in the queue (unique feature of NSOperation). It's just 5 lines of code solution.
NSOperation dependency trick
with Swift it is just easy to implement as this:
extension Array where Element: NSOperation {
/// Execute block after all operations from the array.
func onFinish(block: () -> Void) {
let doneOperation = NSBlockOperation(block: block)
self.forEach { [unowned doneOperation] in doneOperation.addDependency($0) }
NSOperationQueue().addOperation(doneOperation)
}}
My solution is similar to that of https://stackoverflow.com/a/42496559/452115, but I don't add the completionOperation in the main OperationQueue but into the queue itself. This works for me:
var a = [Int](repeating: 0, count: 10)
let queue = OperationQueue()
let completionOperation = BlockOperation {
print(a)
}
queue.maxConcurrentOperationCount = 2
for i in 0...9 {
let operation = BlockOperation {
a[i] = 1
}
completionOperation.addDependency(operation)
queue.addOperation(operation)
}
queue.addOperation(completionOperation)
print("Done 🎉")

Resources