How to Use RunLoop in IOS Applications

Written by alekseimarinin | Published 2024/03/04
Tech Story Tags: swift | ios-app-development | ios-development | runloop | what-is-runloop | why-use-runloop | hackernoon-top-story | ios-programming

TLDRRunloop is a loop that coordinates the receipt and processing of incoming events in a specific thread. By default, the main RunLoop is always running in the application; it processes messages from the system and transmits them to the application. Auxiliary threads require self-determination of the need for a RunLoop, and you will have to configure and run it yourself.via the TL;DR App

What Is a Runloop?

A RunLoop is a loop that coordinates the receipt and processing of incoming events in a specific thread.

The RunLoop is present in every thread, but by default, it is in standby mode and does not do any work.

The developer can run it if necessary, but it will not work automatically. To do this, you will need to write code.

What Problem Does Runloop Solve?

First of all, RunLoop is designed to manage the flow of incoming tasks and execute them at the right time.

This is most noticeable when working with the UI, for example, when using UIScrollView.

By default, the main RunLoop is always running in the application; it processes messages from the system and transmits them to the application. An example of such messages may be, for example, an event when a user clicks on the screen.

Auxiliary threads require self-determination of the need for a RunLoop. If you need it, you will have to configure and run it yourself. Running RunLoop by default is not recommended, it is required only in cases where we need active interaction with threads.

Also, all timers in the application are executed on the runloop, so if you need to interact with them in your application, you definitely need to study the features of the runloop.

How Does It Work?

RunLoop is a loop, and it has several modes of operation that help the developer understand when to run a particular task.

So, RunLoop can be in the following modes:

  1. Default - The default mode, the stream is free, and large operations can safely be performed in it.

  2. Tracking - The thread is busy doing some important work. At this point, it is better not to run any tasks, or at least to run some small tasks.

  3. Initialization - This mode is executed once during the initialization of the stream.

  4. EventReceive - This is an internal mode for receiving system events, which is usually not used.

  5. Common - Is a placeholder mode that has no practical significance.

    On the main RunLoop, these modes are switched automatically; the developer can use them to perform time-consuming tasks so that the user does not notice the interface hanging. Let's look at an example.

Execution cycle management in other RunLoop is not fully automatic. You need to write code for a thread that will start the execution cycle at an appropriate time. Also, you need to respond appropriately to events and use endless loops to ensure that the execution cycle doesn't stop.

We have a UIScrollView, and we need to perform a large task on the main thread so that the user doesn't notice anything.

We can complete the task in the usual way:

DispatchQueue.main.async {
   sleep(2)
   self.tableView.refreshControl?.endRefreshing()
}

But the result is going to be pretty bad. The user will notice significant delays in the application.

https://youtu.be/7JIzvsX_vc4?embedable=true

This negative effect occurs due to the fact that we run a task on the main thread without paying any attention to what is happening on it at the moment.

Because of this, we begin to perform our big task at the moment when the user interacts with the interface. This, of course, leads to the fact that the user sees the interface hanging.

This can be avoided by using the RunLoop mechanism. Let's implement the same logic using this:

CFRunLoopPerformBlock(CFRunLoopGetMain(), CFRunLoopMode.defaultMode.rawValue) {
    sleep(2)
    self.tableView.refreshControl?.endRefreshing()
}

Let me explain what happens here. The CFRunLoopPerformBlock function adds code for execution through the RunLoop. In addition to the code block itself, this function has 2 important parameters.

The first one is responsible for selecting which RunLoop should execute the function. In this example, "main" is used.

The second one is responsible for the mode in which the task will be completed.

There are three possible modes in total:

  • Tracking - when the user interacts with the interface, such as scrolling through a UIScrollView.

  • Default - the user does not interact with the interface. At this time, it is possible to safely complete a resource-intensive task.

  • Common - combines the default mode and tracking mode.

    The result of running the program with the code above will be:

https://youtu.be/2Rd7Z13RhJA?si=u3r5rhFKixbDK6e6&embedable=true

When the user begins interacting with the user interface (UI), the main run loop switches to "tracking" mode and temporarily suspends the processing of all other events in order to ensure the smoothness of the interface. Once the user stops interacting with the interface, the run loop returns to its "default" mode and resumes performing our task.

Timers

In addition to the user interface, the loop is also closely linked to the functioning of timers.

Any timer in the application runs in a loop, and you need to be extra careful not to make mistakes while working with them, especially if they are responsible for important functionality such as payment processing.

Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
    // makeSomething
}

The timer starts in default mode by default, so it may stop working if the user is scrolling through the table at the moment. This is because the loop is currently in tracking mode. This is why the code in the example may not work properly. You can fix this issue by adding a timer in common mode.

let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
    // makeSomething
}

RunLoop.main.add(timer, forMode: .common)

Also, the timers may not fire at the expected time or not at all. This is because the RunLoop only checks the timers at the beginning of each cycle. If a timer triggers after the RunLoop has passed this stage, we will not know about it until the beginning of the next iteration. At the same time, the longer the task runs on the RunLoop, the longer the delay will be.

To solve this issue, you can create a new thread, start a RunLoop in that thread, and then add a timer to the thread without adding any other tasks - in this way, the timer will function correctly.

let thread = Thread {
    let timer = Timer(timeInterval: 1.0, repeats: true) { timer in
        // makeSomething
    }
    RunLoop.current.add(timer, forMode: .default)
    RunLoop.current.run()
}
thread.start()

The Result

In this article, we have looked at what RunLoop is and what problems it solves in iOS applications. A RunLoop is a loop that coordinates the reception and processing of incoming events within a specific thread, and it has several modes of operation.

It is especially useful when working with the user interface (UI) and timers because it has the ability to execute tasks at the right time, which allows you to avoid "hanging up" the interface and ensure correct operation of the timers.

Despite the fact that working with Run Loop requires additional coding, it is a worthwhile investment that improves the efficiency and stability of your application.


Written by alekseimarinin | Ios developer
Published by HackerNoon on 2024/03/04