Writing Synchronous Code in Swift
By: Alex Nekrasov
Tags: Mobile App Development, Synchronicity, Asynchronous, Swift, iOS App Development

Photo by Anton Darius on Unsplash

When you write mobile apps in Swift, you usually have a lot of background work. I’ve been working as a mobile developer for almost 10 years and I can hardly remember a single project without Internet requests. Each Internet request requires time to be processed. Usually an unknown amount of time, possibly, endless.

If you do such work in the main or UI (which is the same) thread, your UI will get stuck. That’s why asynchronous tasks in Swift are designed the way they will never do so. The most common way is to avoid it is to use a callback or delegate. For example:

API.getUserInfo(userId: userId) { (userInfo, error) in

}

Here the app flow stops only for a small fraction of a second to prepare and send a request, but it doesn’t wait for the result. We get the result in a closure. userInfo contains information about a user (or nil if error happened), error is an optional error.

Let’s see a more complicated example. We need to load information about the user; if it succeeds, we also need a list of their purchases. If it fails, we can try again, but not more than 3 times.

func loadUser(attempt: Int = 0) {

API.getUserInfo(userId: userId) { (userInfo, error) in

if let error = error {

if attempt < 3 {

self.loadUser(attempt: attempt + 1)

} else {

self.showError(error)

}

} else {

API.loadPurchasesForUser(userId: userId) { (purchases, error) in

// ...

}

}

}

}

It looks much more complicated, but still readable. I call such things fir-trees or Christmas trees.

Bad-looking code. Thanks https://www.clipartmax.com/ for tree picture

Now imagine that you also need to make 3 attempts to load purchases. And you need to load payment methods, lists of friends with their statuses, lists of available restaurants and their menus. This function will split into several smaller functions.

Flutter or JavaScript programmers will say “why not to add async / await to this code?” — and they will be right. It will make the code much simpler. But how to do it in Swift?

We’d like to get something like this (note that the code below is not Swift code):

func loadUser() {

for i in 0..<3 {

(userInfo, error) = await API.getUserInfo(userId: userId)

if let error = error {

if i == 2 {

self.showError(error)

return

}

} else {

self.userInfo = userInfo

break

}

} for i in 0..<3 {

(purchases, error) = await API.getPurchases(userId: userId)

if error == nil {

self.purchases = purchases

break

}

} // ...

}

Yes, it also has a tree-looking structure, but it will never go deeper. And we don’t make any recursions. Everything is in one function, the flow is much clearer.

Short about threads

One of the purposes of modern multi-tasking operating systems is to manage processes and threads. Old systems (like DOS) didn’t do it, that’s why if one app “hung” it forced you to reboot your computer. iOS is not so fragile, it can run many processes at any given time and each of them can have one or more threads.

The first thread, also called main thread or UI thread handles the main app flow and changes in UI. You can’t change the text in UILabel or hide an UIImage in any other thread or it will lead to a crash.

If you need to do background work, you need another thread. In the case of a single-core CPU (Central Processing Unit), these threads will be executed one after another on the same code. In multi-core CPUs, threads can run on different cores — which potentially makes your app faster.

If a thread is “stuck”, it won’t hang the whole app. Even more, you can detect it and fix it from the main thread. Yet, it’s not a good practice to create many threads which just wait in the background. Each thread has its own context, uses memory and computational power of your device.

In simple cases, a thread runs a sequence of statements, programming code that you as a developer provide it with. In more complicated scenarios, like in the case of the main thread, it uses a dispatcher. The dispatcher has a queue of small functions which it runs one by one. The main thread has a loop like this:

App code

Updating UI

Listening to user events

Of course, it’s simplified, but it’s important to understand the core concept. You need to remember two important rules:

Never, by no means, “hang” the main thread. Don’t change the UI from any background thread.

To add code to a queue of the main thread, use this code:

DispatchQueue.main.async {

// ...

}

To create a new thread and run code there, do this:

DispatchQueue.global().async {

/// ...

}

Besides the async method, you can use asyncAfter , which schedules code to run after some time:

DispatchQueue.main.asyncAfter(deadline: .now() + 1) {

/// ...

}

In this example, .now() + 1 means that the code will run in one second.

Warning! Don’t use it as a timer. Due to specifics of the dispatch queue, it doesn’t guarantee that the code will run exactly as scheduled.

Converting asynchronous functions into synchronous

In most libraries and Apple APIs we get asynchronous functions as functions with callbacks (delegates). So, our first step will be turning one into another.

Let’s define a couple of asynchronous functions:

This example will output:

Optional(__lldb_expr_3.UserInfo(firstName: "John", lastName: "Doe")) ["Purchase 1", "Purchase 2"]

The first line will appear in 3 seconds, the second one in 2 more seconds. Now let’s get rid of a fir-tree.

We can wait while the function execution ends using a semaphore:

Not only will this make us wait until the function execution ends, but also make it thread-safe. The execution will end in the same function as it started.

Let’s see how it simplifies our code:

If we remove print statements, we’ll have only 3 lines of code! And we can add any logic we want. But there’s one downside and let’s discuss it in details.

What is Semaphore?

If you didn’t understand the idea of a semaphore from the previous example, let’s talk about it in more detail.

When we have more than one thread, they need synchronisation. There are many different mechanisms, but the simplest is called semaphore. It’s a thread-safe counter. Thread-safe means that you can change it in one thread and read from another without any negative consequences. It will be perfectly synchronised between threads.

You can create a semaphore, change its value and wait for it. This code creates a new instance:

let semaphore = DispatchSemaphore(value: 0)

value is an initial value of a semaphore. Zero means that there will be two threads which need synchronisation.

Passing zero for the value is useful for when two threads need to reconcile the completion of a particular event. — Apple Developer Documentation

This code starts a waiting loop. The code execution will be paused until another thread gives a signal:

semaphore.wait()

You can run it in the main thread, because this function is not going to block the thread. It checks if the semaphore has a signal — if yes, it just returns control. Otherwise, it schedules itself to a dispatcher. When a thread comes back to a scheduled function, it does the same, again and again, until it receives a signal.

And this function gives a signal:

semaphore.signal()

It’s your responsibility to make sure that semaphore.signal() will be called, otherwise semaphore.wait() will never end.

Running functions in different threads

The output of original code is the following:

Start Optional(__lldb_expr_21.UserInfo(firstName: "John", lastName: "Doe")) Middle ["Purchase 1", "Purchase 2"] End

We got synchronous functions, and they “block” the flow until the execution is finished. Running such functions from the main thread is totally unacceptable. Let’s go over two different examples:

Here’s the output:

Start Middle End Optional(__lldb_expr_23.UserInfo(firstName: "John", lastName: "Doe")) ["Purchase 1", "Purchase 2"]

When we use DispatchQueue....async , it schedules the execution of a closure in a specified thread, but it starts only when the thread is free. Each thread has a queue of code blocks to run and we add our closure to the end of this queue.

What’s interesting here is that we see userInfo first, and only later the list of purchases. Why does it happen? The list of purchases should come first, it takes only 2 seconds, and fetching userInfo takes 3 seconds.

But the second code block stays in the queue until the execution of the first one is finished. Not only does this block stay in the queue, but also all the other UI-related code blocks do so.

Let’s have a look at another example:

In this case instead of using the main thread, we create two background threads and run code there.

The output looks different:

Start Middle End ["Purchase 1", "Purchase 2"] Optional(__lldb_expr_27.UserInfo(firstName: "John", lastName: "Doe"))

It happens because we start these functions simultaneously, and as expected, getting the purchase list ends faster.

What’s the correct use

The correct use of blocking synchronous functions is this:

We create a background thread We implement all the logic inside the background thread without working with the UI If we need to update the UI, we jump into the UI thread

This code waits for 5 seconds, then outputs both lines together:

Optional(__lldb_expr_29.UserInfo(firstName: "John", lastName: "Doe")) ["Purchase 1", "Purchase 2"]

Synchronous frameworks

Turning each function into a synchronous can take a lot of time. Can it be done easier?

Well, there are people out there who have done some work on that regard before. For example, there is a synchronous version of the popular framework Alamofire:

The Parse framework, which was popular years ago (when it was officially supported by Facebook) has a synchronous versions of calls:

But such cases are more exceptional. For example, Firebase directly asks not to do it, explaining that the nature of their calls is asynchronous and it should stay that way:

Conclusion

Swift is a constantly evolving language, but it still doesn’t have Dart-like or JavaScript-like async / await constructions. The Developers community sends proposals to the Swift developer group asking to add them, but those haven’t been incorporated yet.

Still, there’s a good solution involving background threads and semaphores. It may be not so easy, but if you master it and design your API calls or another long-working processes synchronously from the beginning, your project can benefit from it a lot.

Happy coding and see you next time!