Managing CoroutineScope lifecycles from Swift on KMP

Sep 19, 2025
On Android, when using Kotlin Coroutines with Jetpack Compose, we can leverage Jetpack Navigation to provide a CoroutineScope whose lifecycle is automatically tied to the current navigation route.
composable<AppRoute.Home> { backStackEntry ->
val presenter = remember {
HomePresenter(
navigator = AppNavigatorImpl(navController)
)
}
HomeView(presenter = presenter)
}
However, on the Swift side, this tight integration doesn’t exist. Therefore, we need to write some additional code to manage the lifecycle of our coroutines manually.
Exposing Coroutine builder to Swift
Creating a CoroutineScope directly from Swift isn’t possible out of the box. To work around this, we can export a builder method from our shared Kotlin Multiplatform (KMP) module. We’ll also create a wrapper around CoroutineScope to expose its cancel() method to Swift:
public class CancellableCoroutineScope(private val scope: CoroutineScope) :
CoroutineScope by scope {
public fun cancel() {
scope.cancel()
}
}
public fun buildCoroutineScope(): CancellableCoroutineScope =
CancellableCoroutineScope(
object : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = SupervisorJob() + Dispatchers.IO
}
)
Managing Coroutine lifecycle from Swift
To link the CoroutineScope’s lifecycle to a SwiftUI screen, we’ll create a ScopeLifecycleManager class. This class will cancel the scope when its deinit method is called. We’ll also create a View wrapper that provides the scope and holds an instance of our ScopeLifecycleManager in a @State variable. This approach ensures our manager class stays alive as long as the view is present and automatically cancels the coroutine when the view is destroyed.
private class ScopeLifecycleManager {
let scope: CancellableCoroutineScope
init() {
self.scope = shared.buildCoroutineScope()
print("✅ ScopeLifecycleManager initialized.")
}
deinit {
scope.cancel()
print("🔴 ScopeLifecycleManager deinitialized, scope cancelled.")
}
}
struct CoroutineScopeProvider<Content: View>: View {
@State private var lifecycleManager = ScopeLifecycleManager()
private let content: (CancellableCoroutineScope) -> Content
init(@ViewBuilder content: @escaping (CancellableCoroutineScope) -> Content) {
self.content = content
}
var body: some View {
content(lifecycleManager.scope)
}
}
With this setup, we can use our wrapper in any SwiftUI view or navigation destination, effectively mimicking how Jetpack Navigation links a CoroutineScope to a route:
.navigationDestination(for: AppRoute.Detail1.self) { route in
CoroutineScopeProvider { scope in
Destination1(text: route.text, scope: scope)
}
}
This ensures our scope remains active as long as its corresponding view is on screen. Even if new views are pushed onto the navigation stack, the coroutines within that scope will persist (which would not be the case if we used .onDissapear modifier to manage our scopes). If you have a Presenter or ViewModel from your Kotlin module that requires a CoroutineScope, you can now easily provide it from Swift like so:
self.presenter = HomePresenter(
navigator: navigator,
scope: scope
)
Finally, when the screen is dismissed, the CoroutineScope is destroyed, and all the work scoped to it from the presenter will be automatically cancelled.