The solution I came to use is somewhat a hybrid of what you've been discussing and an approach that @mbrandonw used in a TCA sample code.
Every composable feature alongside the reducer defines something like:
enum PermissionsTeardownToken: CaseIterable, Hashable {
case locationManagerId
}
Which simply defines tokens for all long-running Effects for that reducer. So when parent to Permissions reducer decides to nil it's state out:
case .stop:
state.permissions = nil
return .cancel(token: PermissionsTeardownToken.self)
Where cancel(token:)
is a tiny extension on Effect:
extension Effect {
static func cancel<T>(token: T.Type) -> Effect where T: CaseIterable, T: Hashable {
.merge(token.allCases.map(Effect.cancel(id:)))
}
}