iosdev

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 useful AsyncOperation 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

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