June 29, 2021
Understanding Swift schedulers

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!

What is a scheduler?

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.

Types of schedulers

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.

Default schedulers

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.

Switching schedulers

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.

Performing asynchronous tasks with schedulers

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.

Conclusion

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.

The post Understanding Swift schedulers appeared first on LogRocket Blog.

Leave a Reply

Your email address will not be published. Required fields are marked *

Send