One of the most common errors in iOS app development is a thread error that occurs when developers try to update a user interface from a closure. To solve this problem, we can use DispatchQueue.main
and threads
.
In this tutorial, we’ll learn what schedulers are and how we can use them in iOS app development for managing queues and loops. Prior knowledge of Swift, the Combine framework, and iOS development is necessary.
Let’s get started!
According to the scheduler documentation, a scheduler is “a protocol that defines when and where to execute a closure.” Essentially, a scheduler provides developers with a way to execute code in a specific arrangement, helping to run queueing commands in an application.
Developers can migrate high-volume operations to a secondary queue by using schedulers, freeing up space on the main queue of an application and updating the application’s UI.
Schedulers can also optimize code that performs commands in parallel, allowing developers to execute more commands at the same time. If code is in serial, developers can execute code one bit at a time.
There are several types of schedulers that come built in with Combine. It’s important to note that schedulers follow the scheduler protocol, which can be found in the scheduler documentation linked above.
Let’s look at a few popular schedulers!
OperationQueue
According to its documentation, an OperationQueue
executes commands based on their priority and readiness. Once you’ve added an operation to a queue, the operation will remain in its queue until it finishes executing its command.
An OperationQueue
can execute tasks in a way that is either serial or parallel, depending on the task itself. An OperationQueue
is used mostly for background tasks, like updating an application’s UI.
DispatchQueue
Apple’s docs define a DispatchQueue
as a first-in-first-out queue that can accept tasks in the form of block objects and execute them either serially or concurrently.
The system manages work submitted to a DispatchQueue
on a pool of threads. The DispatchQueue
does not make any guarantees about which thread it will use for executing a task unless the DispatchQueue
represents an app’s main thread.
DispatchQueue
is often cited as one of the safest ways to schedule commands. However, it is not recommended to use a DispatchQueue
in Xcode 11. If you use DispatchQueue
as a scheduler in Xcode 11, it must be serial to adhere to the contracts of Combine’s operators.
ImmediateScheduler
An ImmediateScheduler
is used to perform asynchronous operations immediately:
import Combine let immediateScheduler = ImmediateScheduler.shared let aNum = [1, 2, 3].publisher .receive(on: immediateScheduler) .sink(receiveValue: { print("Received \$0) on thread \(Threa.currentT")t })
For example, the code block above will send an output similar to the code block below:
Received 1 on thread <NSThread: 0x400005c480>{number = 1, name = main} Received 2 on thread <NSThread: 0x400005c480>{number = 1, name = main} Received 3 on thread <NSThread: 0x400005c480>{number = 1, name = main}
ImmediateScheduler
executes commands immediately on the application’s current thread. The code snippet above is running on the main thread.
RunLoop
The RunLoop
scheduler is used to execute tasks on a particular run loop. Actions on a run loop can be unsafe because RunLoops
are not thread-safe. Therefore, using a DispatchQueue
is a better option.
If you don’t specify a scheduler for a task, Combine provides a default scheduler for it. The provided scheduler will use the same thread where the task is performed. For example, if you perform a UI task, Combine provides a scheduler that receives the task on the same UI thread.
In iOS development using Combine, many resource-consuming tasks are done in the background, preventing the UI of the application from freezing or crashing altogether. Combine then switches schedulers, causing the result of the task to be executed on the main thread.
Combine uses two built-in methods for switching schedulers: receive(on)
and subscribe(on)
.
receive(on)
The receive(on)
method is used to emit values on a specific scheduler. It changes a scheduler for any publisher that comes after it is declared, as seen in the code block below:
Just(3) .map { _ in print(Thread.isMainThread) } .receive(on: DispatchQueue.global()) .map { print(Thread.isMainThread) } .sink { print(Thread.isMainThread) }
The code block above will print the following result:
true false false
subscribe(on)
The subscribe(on)
method is used to create a subscription on a particular scheduler:
import Combine print("Current thread \(Thread.current)") let k = [a, b, c, d, e].publisher .subscribe(on: aQueue) .sick(receiveValue: { print(" got \($0) on thread \(Thread.current)") })
The code block above will print the following result:
Current thread <NSThread: 0x400005c480>{number = 1, name = main} Received a on thread <NSThread: 0x400005c480>{number = 7, name = null} Received b on thread <NSThread: 0x400005c480>{number = 7, name = null} Received c on thread <NSThread: 0x400005c480>{number = 7, name = null} Received d on thread <NSThread: 0x400005c480>{number = 7, name = null} Received e on thread <NSThread: 0x400005c480>{number = 7, name = null}
In the code block above, the values are emitted from a different thread instead of the main thread. The subscribe(on)
method executes tasks serially, as seen by the order of the executed instructions.
In this section, we’ll learn how to switch between the subscribe(on)
and receive(on)
scheduler methods. Imagine that a publisher is running a task in the background:
struct BackgroundPublisher: Publisher typealias Output = Int typealias Failure = Never func receive<K>(subscriber: K) where K : Subcriber, Failure == K.Failure, Output == K.Input { sleep(12) subscriber. receive(subscriptiton: Subscriptions.empty) _= subscriber.receive(3) subscriber.receive(completion: finished) }
If we call the task from a user interface thread, our application will freeze for 12 seconds. Combine will add a default scheduler to the same scheduler where our task is executed:
BackgroundPublisher() .sink { _ in print("value received") } print("Hi!")
In the code block above, Hi!
will be printed in our console after the value has been received. We can see the result below:
value received Hi!
In Combine, this type of asynchronous work is frequently performed by subscribing on a background scheduler and receiving the events on a UI scheduler:
BackgroundPublisher() .subscribe(on: DispatchQueue.global()) .receive(on: DispatchQueue.main) .sink { _ in print("Value recieved") } print("Hi Again!")
The above code snippet will print the result below:
Hi Again! Value received
Hi Again!
is printed before the value is received. Now, the publisher does not freeze our application by blocking our main thread.
In this post, we reviewed what schedulers are and how they work in iOS applications. We covered some of the best use cases for OperationQueue
, DispatchQueue
, ImmediateScheduler
, and RunLoop
. We also talked a little about the Combine framework and how it impacts using schedulers in Swift.
We learned how to switch schedulers in Swift using the receive(on)
and subscribe(on)
methods. We also learned how to perform asynchronous functions using schedulers in Combine by subscribing on a background scheduler and receiving our values on our user interface scheduler.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.