(NS)Operations can definitely seem scary at first, they run on queues there is asynchronisity, threading and state involved. As good Swift developers two of these cause us headaches and the third is something we are taught is bad and must avoid. We don't like dealing with these things so we shy away from (NS)Operation and usually end up dealing with async, threading and state in other ways. This is silly, (NS)Operation is actually built to deal with these things, yes there is a barrier to entry but once you take the plunge and learn how to use operations effectively the problems mentioned above become quick and easy to deal with.
TLDR; Ditching service classes and embracing reusable tasks (or operations) allows us to free up our architecture and make our (insert your favourite here; Presenter's, Model's, Controller's...) less complex and even easier to test.
A little backstory
I have been writing iOS applications since 2012, before that I had barely written a line of code in my life. Objective C was my first programming language. In 2014 when Swift was released I was lucky enough to begin writing code in Swift straight away. This meant that whilst I was writing Objective C I was still very much in that early learning stage getting to grips with Software Engineering as a whole. It speaks alot to my education that I knew about method swizzling and associated objects before (NS)Operation. The point being I never played with (NS)Operation before I became a full time Swift developer, at which point it was just archaic legacy Objective C stuff that Swift developers didn't need to concern themselves with. I was an idiot. On joining my current company under the mentorship of a senior developer at the time I learnt just how much I needed to learn.
There is plenty of information about (NS)Operation, how it works and the ins and outs which will give you a far better introduction than I can here. If you are new to the topic I recommend watching this WWDC Video and check out the accompanying sample code (It's written in Swift 2 but you can get the gist).
What is important to understand for this article however is that (NS)Operation is subclassed by you to create an individual task that can be run at a later time. The task is only carried out when the (NS)Operation is added to a (NS)OperationQueue. This queue is usually not on the main thread which means the task will actually be executed in the background for you by default.
Two things make (NS)Operation powerful IMO. Dependencies and state, two scary words. If I have two (NS)Operation's A and B, I can add B as a dependency of A. This means B will never start until A is finished, even if they are running in different queues. This allows us to manage asynchronisty very effectively. The fact that (NS)Operations are inherintly stateful, it is actually a great place to encapsulate the state of your application.
As we write code we often talk about writing reusable components and the same goes when utilising (NS)Operation. I recomend breaking things down into as many unique (think microservice) individually testable components as possible.
Lets imagine the scenario where we need to display our users favourite artists top 5 songs on a certain screen of our app. The users favourite artist is exposed via one api and the top 5 songs for an artist can be retrieved by a second api supplying an artist id returned in the previous api.
We have two options here, we can create one operation that calls both apis and then returns the reult to the caller (via block or delegation). This has a lot of benefits, whatever we are using to retrieveing this data just needs to add one (NS)Operation to the queue. However the downside is if in another area of our application we need to retrieve a differnt artists top 5 songs we need to write something new. I would propose writing the following two operations.
- RetrieveFavouriteArtist - Calls the first endpoint and returns a users favourite artist.
- TopFiveSongsForArtist - Given an artist returns the favourite song for that artist.
We now have two nice independent (NS)Operation's that can be independantly tested and easily reused. The downside is the caller needs to build two operations and manage some state.
It's time for the secret third option. Now we have our two operations we create an aggregation operation. This operation has it's own queue. It holds onto the state and builds the two operations and adds them to it's own internal queue. Once everything has completed it can return to the caller. This wraps the state up in a place that is built to handle it.
The logic for our caller is now simple again as we just have to add one operation to our queue. The whole way up the stack everything is reusable and testable. This actually looks like a great example of encapsulation and composition. We make small services and compose them into feture specific components. This gets powerful fast.
Putting them to use
So our (presenter, model, controller...) can now pick and choose some (NS)Operations and add them to it's queue to get the information it needs.
So far there isn't any obvious advantages over a normal service class/struct. There are a few subtle ones already, these tasks being background thread by default does help stop those pesky dropped frames because some heavy task is running on the main thread. If your caller needs to call a lot of different services to get it's data there are a lot of services that need to be injected into the class/struct. The beauty of using (NS)Operation is that just the queue needs to be injected into the class/struct. The (NS)Operations just need to be created as and when they are needed and added to the queue. There is no issue with coupling when this approach is taken because the (NS)Operation and the queue is removing the dependancy for you. This will be discussed more in the next section.
Now lets really ramp up the complexity, lets imagine a situation where three different unrelated service calls nees to happen, only after they all complete do we want to aggregate the data and inform our view layer. Our caller can create a
BlockOperation which has the completion logic and make each
(NS)Operation a dependancy of the
BlockOperation. This means the
BlockOperation will only run once the other
(NS)Operations's have completed. The behaviour is that simple to implement. If there is too much state involved in the completion of these
(NS)Operation's then of course this logic can be wrapped up in it's own
(NS)Operation I usually leave this up to the discrepency of the implementer to decide when is the write time to create an aggregation. That is the beauty of small services, they are easy to compose in any way you like.
This is just scratching the surface of what we can do with
(NS)Operation. Some great further examples involve things like presenting modal views using
(NS)Operation and leveraging a global queue mixed with the state management capabilities of
(NS)Operation to ensure that multiple modals arent presented to the user at the same time, but the user will see all of the content they are supposed to on their own schedule. It's also quite interesting to create workflows using
(NS)Operation where entire flows of your application can be presented just by adding an
(NS)Operation to a queue, with all the state being handled inside the
(NS)Operation. The trick to these view based operations is to remember to execute the view displaying and view updating parts on the main thread!
Testing is nice and easy. The
(NS)Operation can be tested in the exact same way as any other service class. We just call the
main() method directly.
The classes/structs that utilise these operations have an even easier time. All that needs to be tested when creating the operation is that the right type of operation is added to an injected
MockQueue with the right dependancies.
The completion of the operations are tested in a couple of different ways depending on the communication pattern you choose block's or delegation. Testing these patterns is very well talked about so I won't go into it here!
So Async/await looks very much like it's coming to Swift. How does the affect my use of
(NS)Operation I hear you ask. It will, lot's! Hopefully it will replace my use of
(NS)Operation. So why should you begin utilising operations now. Well the main reason is Async/await isn't ready yet! Another more pressing reason is that most of this post has been about composing services than
(NS)Operation. The operations have been a tool that allows us to acheive this for now. If we use operations and implement this way of thinking now it will be a nice easy switch to async await, as many of the interfaces and the lines in our architecture will already be drawn for us, it will mainly be a syntax change.
(NS)Operation is still important today. Breaking up our architecture into reusable components is becoming more and more important as we build bigger and more complex applications. Composition is the way forward, it always has been.