Running code when App is Ready

Every now and then, you find yourself in a situation where you need to run specific code when certain conditions are met. My recent encounter with this involved the following flow:

• A VoIP notification wakes up my app

• I need to handle internal call logic and start a call

The problem was that the second part of this flow couldn't start until the UI was ready. In my case, I had to ensure that the Coordinators had executed and the UI was in a state ready to handle the call logic. When developing new features, I often explore open-source codebases for inspiration, and this instance was no exception. In the source code of the popular messaging app Signal, I found a class named AppReadiness that allows you to queue some code and execute it once the conditions are met. Although the class is somewhat complex and robust, I extracted the main concepts and created a lightweight variant suitable for my use case.

Let's explore my lightweight version of AppReadiness.

final class AppReadiness: @unchecked Sendable {
    typealias Task = () -> Void
    private let queue = DispatchQueue(label: "com.supersonicbyte.AppReadiness")
    private var taskQueue: [Task] = []
    private var _isAppReady = false

    static let shared = AppReadiness()

    private init() {}

    func setAppIsReady() {
        queue.sync {
            guard !_isAppReady else { return }
            runQueuedTasks()
            _isAppReady = true
        }
    }

    func isAppReady() -> Bool {
        return queue.sync {
            return _isAppReady
        }
    }

    func runSyncNowOrWhenAppBecomesReady(_ task: @escaping Task) {
        queue.sync {
            if _isAppReady {
                task()
            } else {
                taskQueue.append(task)
            }
        }
    }

    private func runQueuedTasks() {
        for task in taskQueue {
            task()
        }
        taskQueue.removeAll()
    }
}

First, we see that AppReadiness is a singleton, interacting only through the shared instance. This makes sense since the app itself is a single entity, and we need a single source of truth to determine if the app is ready.

Next, we have three public methods: setAppIsReady(), isAppReady(), and runSyncNowOrWhenAppBecomesReady(_ task: @escaping Task).

The first method, setAppIsReady(), allows us to mark the app as ready. The goal is to call this method when all the necessary conditions have been met. In my case, I call setAppIsReady() upon the initialization of the last coordinator during app startup.

The isAppReady() method allows us to query the AppReadiness status in a thread-safe manner.

Finally, the runSyncNowOrWhenAppBecomesReady method allows us to queue code to run when the app is ready. If the app is already ready when this method is called, the task will execute immediately. If the app is not ready, the task will get queued and executed once the setAppIsReady() method is called. Internally, the class uses a DispatchQueue to synchronize access to the shared mutable state, making the class thread-safe.

As I mentioned, this is a lightweight solution, leaving plenty of room for potential improvements and additional features based on your needs. For example, if we needed a way to set dependencies between tasks (e.g., running task C only after tasks A and B are completed), we could utilize the NSOperations API.

At this repo you can find a demo of the AppReadiness.

Until next time, M.

Tagged with: