Drop me an email if you'd like to hire me (part time) or learn more about what I do. You can read about my experience and references on LinkedIn.
iOS: Second try to NSOperation and long running tasks
Yesterday I wrote about using NSOperation for background tasks.
It was more about long running tasks. When I finished writing the code I wasn’t
so happy about how it looked. There were few bad things that I didn’t like.
If you didn’t read the first version give it a try and next continue this one,
I’ll wait :)
I found few bad things in operations I implemented:
1. Few operations were using semaphores! I’m not a big fun of using them,
I know how to use them, but when I see them in the code I think the code gets complicated. I want to avoid using them in operations.
2. Not all cases were handled. It worked in ideal world but it must improve.
3. DownloadOperation was mistake and NetworkOperation is better idea.
I wrote it in the previous post’s update, but want to mention this here too:
I decided to keep decoding downloaded data out of NetworkOperation to have
ability to decode it depending on what I need in other tasks that depends on received data.
Let’s improve it
Spent a while reading docs for NSOperation and subclassing, and found few
points that changed my thinking about implementing custom operations.
main() method should be overridden for not-concurrent operations. Task starts, main() is called by start() method, main() ends and task finishes.
If operation is concurrent one, following methods should be overridden: start(), asynchronous(), executing(), finished(). And this is what I wanted from
the beginning. My operations are long running that hit backend and I wanted to
code them in this manner.
In concurrent operation start() method is responsible for starting operation. Asynchronous methods should be called from this one. When subclassing you are
also responsible for changing executing and finished properties of the class.
What is important you must generate appropriate KVO notifications to let objects
that observe this operation react correctly. When KVO notification is not send
task will not be checked again after it started - so… yeah, it will never finish.
The next important thing is that main() may be called from start() or may
not be called at all. When overriding start() you’re responsible for calling
Task also will never finish itself. You must change finished property (and accordingly executing property) and generate KVO notifications.
Important functionality that should be supported is cancellation. In subclass
you’re responsible for handling cancellation. When cancel() is called on the
operation or cancelAllOperations() on a queue the cancelled property changes
and operation should correctly react when task is cancelled. Operation may not
be cancelled immediately but you should stop doing things as fast as possible.
You’re responsible for checking cancellation:
In start() method before doing anything. If task is cancelled just break
what was planned to do.
In main() method during operation is in executing state. Stop doing all
When operation is cancelled you’re responsible to change executing and finished property accordingly to false and true and generate KVO notifications.
Few things to do and to check, but not a nightmare, right?
Building custom operation
I thought about improvement of my previously implemented operations and decided
to subclass NSOperation to create a bit more intelligent Operation class.
Main goals of this class are:
keeping its completion block and pass it in init optionally.
starting on the same thread as queue is working on or starting on main thread.
This is useful for operations that uses e.g. NSURLConnection and such operations
must call start() method on the main thread.
finishing in main() method optionally. I don’t want every time to stop
operation in main(), especially when this is asynchronous operation.
Implementation is presented below. I think that the class is pretty
self-explanatory. It contains functionality listed above and is still simple.
Let’s improve operations presented in the previous post.
Since operation has custom finish() method implemented and we’re responsible
for finishing it, it never stops in main() if specified to not finish there.
This provides simplicity to the code. This is what I wanted to achieve. Cool :)
NetworkOperation and ParseQuotesOperation stopped using semaphores. Cool, we’re done with those ugly semaphores in operations.
GetAllQuotesOperation has been simplified a bit and has added one more operation
for finishing itself because we have to stop it manually now. Happy path for
the operation is like this: Download data > Parse Data > Call completion block > Finish operation.
And here is the UpdateReadCountOperation not presented in the previous post
which shows how nice is to keep data decoding out of NetworkOperation.
It works the same way as GetAllQuotesOperation but sends request for increasing
read count of quote that user is currently reading. NetworkOperation returns
data with most recent read count and quote object is updated (someone might read
the same quote before request is send).
I am fine to say “It looks a lot better now and I like operations!”.
Operations are powerful and makes code cleaner and more loosely coupled.
The Operation class is ready to re-use and easy to use which is great.
No semaphores anymore :)
I think the second try is much better, what do you think?
Update 15 Jul 2015 16:17
I’ve used the code in real project and improved it. I realized that I don’t
need this startOnMainThread property because I always need to override start()
method to do some extra things anyway. I removed the property and Operation
class is even simpler.