After focusing on the Application Lifecycle and View Lifecyle in iOS, our next topic is concurrency. We face handling more than one operations at the same time in today’s world. We want to shorten the user’s waiting time even though it’s not always necessary. Displaying the loading for a long time or freezing the UI moves user’s focus away and decreases the app usage from time to time. At the same time, keeping the attention in the app becomes really hard. Therefore, instead of executing operations synchronously one after another, we have to run them in parallel to have a better user experience.
Why do we need to know Concurrency really well?
In these days, almost every app works with online data transfer (network requests and responses). Handling network operations properly is really important. Besides the app functionality, we don’t want to interrupt or block the user while using the app. Interruptions always result in a bad user experience. So, while we’re displaying any kind of UI, we mostly deal with the network requests concurrently. We can send a network request and wait for the response while displaying a loading animation. Concurrency is handled by different APIs in iOS. We’ll focus on basic Grand Central Dispatch (GCD) to understand concurrency.
In the programming world, concurrency is a big topic and has a vital place. If you want to learn more about concurrency, this answer on Quora explains well.
Before talking about
DispatchQueue, we should understand the queues first. Because
DispatchQueuecan run operations on different queues.
What exactly do we need to know about Queues?
There are two types of queues in iOS, the main queue, and the others. We have only one main queue but we can have more than one background queues which can run different operations on it. Background queues run on thread pool according to their priorities. But the main queue runs on the main thread. All UI (user interface) updates have to run on the main thread. Therefore, we use the main queue for the UI.
The most important thing is in here is that we have only one main queue. The main queue should be used wisely. Since every UI update has to be done in the main queue, if we use the main queue for every other operation, we can freeze the UI operations. For example, if we send a network request on the main queue and update the UI according to the response, we will see the UI is not responsive until we get back the data. The hint is that we should use background queues as much as we can, and try to leave the main queue empty for non-UI operations.
How do we use queues with a simple network request and GCD?
Sending a network request is pretty straight-forward.
URLSession is the most common way for a simple network request. We can send a request via
dataTask and get the response data with error (if any) in a closure.
Again, one of the most important things in here is that the thread handling. We talked this one under the queues. In this example, we want to display an alert when we get an error from the server. But how we can solve the queue problem here? The lightweight and easiest way for running the UI operation method on the main queue is using Grand Central Dispatch (GCD).
So, while running this code, if an error happens, the app will switch to the main queue and execute the
showErrorAlert(error) line. In parallel, the code will continue on the background queue and print
Completed to the debug console.
Finally, we can see our example code with better user experience handling. The code below starts on the background queue and when it needs to show the loading indicator in the screen, we switch to the main queue. The execution continues and starts the task. When we got the response, we’re still on the background queue. If there is an error, now we are switching to the main queue and display the alert.
It’s possible to be confused by the main thread vs the main queue and why they are not the same. There are one main thread and one main queue. If you’re curious about more details, you can read this post.
What if we have a bit more complicated problem?
We have talked about a simple network request and GCD. Whilst it’s important for many apps to handle more complicated tasks concurrently, GCD provides only an easy and pretty straight-forward solution. We may need to cancel a concurrent operation or follow its state. GCD doesn’t provide a solution to these problems. That’s why we will get help from
Operations are chained by
OperationQueue and they can also be dependent on each other. For example, if we want to download food images after fetching the restaurant food inventory, we should add the fetch operation as a dependency to download operation and put them in the same
OperationQueue. So, download operation will wait for the fetch operation to finish.
Let’s have a look at the basic
Instead of getting into too many details about
OperationQueue, let’s take a look at the important things which we should know:
- An operation object is a single-shot object. It’s not reusable. When we use the operation object, it’s basically done. If we want to repeat the same operation, we need to create another instance from our custom operation class.
Operationis an abstract class which is associated with a single task. So, we cannot create new instances of
Operation. Instead, we can subclass it like in the example or use the system-wide defined ones (e.g.
BlockOperation). If an operation has a dependency, it is not considered ready until all of the dependencies are finished executing. When the last dependency finished, the operation starts executing.
Operationis KVC and KVO compliant (this will come up later in this series). So, we can watch the changes in operations by attaching observers. But we shouldn’t bind the operation to the UI elements. Because UI elements must execute on the main thread.
- While it’s safe to call the methods in
Operationfrom multiple threads, we should take care the thread safety in overridden methods and custom implementation.
- If we are creating a non-concurrent operation, only overriding the
main()method is enough. But for concurrent operation, we need to override
isFinishedat minimum. In the example, we have a concurrent operation. That’s why we have all the overridden methods and properties commented with number
2. But we didn’t override
start()method. The reason is
start()method automatically calls the
main()method when invoked. While overriding the
isFinishedmethods, we have to generate KVO notifications when the operation is finished or cancelled. In the example, we can see these parts with the comment with number
- In case if we override
start()method, we should never call
superin the custom implementation.
If we want to keep our apps responsive and run multiple processes in parallel easily, we have to understand concurrency and use our tools according to our needs. GCD gives us simplicity. But when we want to have advanced and custom operations like canceling the execution or reusing some parts of the code for concurrent operations, it is simply not enough. Using
OperationQueues would be less painful in these cases. GCD and Operation are not the same things. Convenient use of both is key to create robust and user-friendly iOS apps.
Which strategies do you follow while handling concurrent processes? What do you think about the simple solution with GCD and how do you use it? Did you ever face with a complex problem where you solved really quickly with the help from
Operation? Let me know your thoughts, comments or feedback on Twitter @candostEN or comments below.
All posts of the NS for iOS Devs Series:
Apple Dispatch Documentation
Apple Operation Documentation
Learn more about Concurrency, Asynchronous vs. Synchronous
Deep dive in Grand Central Dispatch
NSHipster – NSOperation
Operation and OperationQueue Tutorial in Swift
Understanding Operation and Operation Queue in Swift
Edit 1 (28.11.2018): Language improvements, thanks to shiggie on Reddit.