Async Operation for Core Data imports
Old-school way of handling async tasks using Operation
I wrote about my Essentials snippers 4 years ago. I am still using them, slightly updated and modernised but they are still one of the first stops to choose what I need when I start a particular UIKit project.
One advanced feature in there is AsyncOperation
which a subclass of Operation which will not set itself to finished until the asynchronous task you give it is done. OperationQueue and Operation were a staple of my code for a very long time, whenever I needed to be sure things are being done in proper order. It’s super useful and simple to use API in Foundation framework to handle Task management.
Concurrency of Swift itself has been greatly expanded in recent years, with
Task
/TaskGroup
and especially with async / await pattern. Thus I’m not sure how usefulAsyncOperation
will really be, going forward. I advise all to read-up on modern concurrency patterns and API and choose what’s most appropriate.
Apple’s BlockOperation handles one or more blocks of synchronous code. But what if you have chained tasks where code inside each task is asynchronous? AsyncOperation
overrides its internal state and prevents Operation
from setting isFinished=true
until you do it yourself, manually.
So to create true async Operation
- we provide our desired block of code and
- we must make sure that we call
markFinished
at the true end of the task
Developer is the only one that knows what “task is finished” means. I previously gave an example with networking code where markFinished
is called inside the dataTask
’s closure. I have since changed the internal working of the AsyncOperation but that example usage code still works.
Here’s another example.
When importing data from various web API endpoints into Core Data, I need to make sure Core Data store operations are done 1-by-1 to avoid data duplications. Doing insert/update/delete from multiples concurrent threads is super dangerous as you could end up in situation where you insert the same object multiple times. Cleaning that up from production devices is not fun at all.
Thus I have this simple implementation of SaveOperation
:
final class SaveOperation: AsyncOperation {
typealias ProcessingBlock = () -> Void
override func workItem() {
block()
markFinished()
}
required init() {
fatalError("Use the `init(name:block:)`")
}
private var block: ProcessingBlock
init(name: String = "", block: @escaping ProcessingBlock) {
self.block = block
super.init()
self.name = name
}
}
Basically, you give it a block of code that must complete before the Operation is marked as finished. This code, for Core Data, is always synchronous since all fetches, inserts, deletes are done on same thread, including the saving of the ManagedObjectContext
changes at the end of it all.
With this, this is how processing of received JSON is being done:
private(set) var processingQueue = OperationQueue()
...
processingQueue.maxConcurrentOperationCount = 1
...
processingQueue.addOperation( SaveOperation(name: endpoint.logName) {
[unowned self] in
let moc = self.vendManagedObjectContextForImporting()
moc.performAndWait {
do {
[PROCESSING HAPPENS HERE]
// eventually save into core data
try moc.save()
} catch let error {
self.log(level: .warning, error)
}
}
})
It really is that simple. performAndWait
make sure that everything is sync inside the block()
and thus calling markFinished
like this is OK:
override func workItem() {
block()
markFinished()
}