Last active
August 22, 2024 18:31
-
-
Save KaneCheshire/b13ad47c2f8cdfef7c6225306a3ba919 to your computer and use it in GitHub Desktop.
Type-erased Swift Task that cancels itself on deinit
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// A type-erased task that you can store in a collection | |
/// to allow you to cancel at a later date. | |
/// | |
/// Upon deinit of the task, the task will be cancelled | |
/// automatically. Similar to Combine's AnyCancellable. | |
final class AnyTask { | |
/// Call this cancellation block to cancel the task manually. | |
let cancel: () -> Void | |
/// Checks whether the task is cancelled. | |
var isCancelled: Bool { isCancelledBlock() } | |
private let isCancelledBlock: () -> Bool | |
deinit { | |
// On deinit, if the task is not cancelled then cancel it | |
if !isCancelled { cancel() } | |
} | |
/// Constructs an AnyTask from the provided Task. | |
/// The provided task is held strongly until AnyTask is | |
/// deinitted. | |
/// - Parameter task: The task to construct with. | |
init<S, E>(_ task: Task<S, E>) { | |
cancel = task.cancel | |
isCancelledBlock = { task.isCancelled } | |
} | |
} | |
extension Task { | |
var eraseToAnyTask: AnyTask { .init(self) } | |
} | |
extension Array where Element == Task<Void, Never> { | |
var erased: [AnyTask] { map(\.eraseToAnyTask) } | |
} |
For usage convenience…
Task {
// ...
}.store(in: &tasks)
extension Task {
func store<C>(in collection: inout C) where C: RangeReplaceableCollection, C.Element == AnyTask {
collection.append(eraseToAnyTask)
}
}
Nice! Thanks for that!
I've turned this into a Swift Package with test coverage: https://github.com/KaneCheshire/AnyTask
Perfect. I appreciate the effort @KaneCheshire. I’ll be switching to the package.
You can also take advantage of the flexibility of AnyCancellable and conform Task to the Cancellable protocol:
extension Task: Cancellable {}
This offers all of the same functionality:
let task = Task { ... }
// Wrap task in AnyCancellable:
let cancellable = AnyCancellable(task)
// Or use the store(in:) extension method, which we get for free:
var cancellables = Set<AnyCancellable>()
task.store(in: &cancellables)
This works because Task already implements a cancel method, which is the only requirement of the Cancellable protocol.
Here's some Playground code if you want to mess around with this:
import UIKit
import Combine
extension Task: Cancellable {}
let emitter = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
let task = Task {
for await value in emitter.values {
print("tick: \(value)")
}
}
var cancel: AnyCancellable? = AnyCancellable(task)
DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
// The print statements will stop after 4 seconds when the cancellable is deallocated.
cancel = nil
}
RunLoop.main.run() // Playground will run until stopped.
That’s nice! But means there’s a dependency on Combine which is a system library and not a language library right?
Yes, that’s correct!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I dunno why the indentation is so large, GitHub keeps reverting is 🤷