Class
InteractorLifecycle
Holds the active-state, lifecycle-bound tasks, and AsyncStream continuations for a single Interactable actor.
final class InteractorLifecycle
Mentioned In
Overview
Swift actors cannot be subclassed, so napkin distributes interactor lifecycle behaviour through a protocol extension on Interactable. InteractorLifecycle is the one shared, mutable, thread-safe object that every conforming actor delegates to. It owns three pieces of state:
isActive: the current lifecycle phase.
tasks: the set of unstructured Tasks spawned via task(priority:_:) that should be cancelled on deactivate().
continuations: the AsyncStream<Bool>.Continuations handed out by isActiveStream, kept around so the lifecycle can yield transitions and finish them on deinit.
All three live behind a single Mutex<State> from the Synchronization module. The class is the only type in the framework that is @unchecked Sendable: the @unchecked is justified because every public operation reads or mutates state exclusively via withLock, never re-entering the lock from within itself, and never holding the lock across an await.
Mutex is non-recursive on its supported platforms — the implementation is careful to release the lock before invoking client closures (activate(invoking:), deactivate(invoking:)) and before cancelling drained tasks.
Usage
Every conforming Interactable declares a single stored property:
final actor HomeInteractor: PresentableInteractable {
nonisolated let lifecycle = InteractorLifecycle()
nonisolated let presenter: HomePresentable
init(presenter: HomePresentable) {
self.presenter = presenter
}
func didBecomeActive() async {
task {
for await user in userService.userStream {
await presenter.presentUser(user)
}
}
}
}
You generally do not call InteractorLifecycle’s methods directly. Interactable’s default-implementation extension forwards activate(), deactivate(), task(priority:_:), isActive, and isActiveStream here.
Teardown
InteractorLifecycle also cleans up when it is simply released. Its deinit cancels every still-registered task and calls finish() on every isActiveStream continuation, so an interactor dropped without an explicit deactivate(invoking:) still cancels its bound work and closes its streams. This release-time backstop, paired with deactivate(invoking:), is why napkin needs no runtime leak detector.
See Also
Interactable
See Also
InteractorScope
Topics
Creating a Lifecycle
Reading State
Driving Transitions
Spawning Bound Tasks