Skip to content

Instantly share code, notes, and snippets.

@lukepistrol
Last active November 19, 2023 19:32
Show Gist options
  • Save lukepistrol/ded9f4fe28748be3a37121fd5662dd20 to your computer and use it in GitHub Desktop.
Save lukepistrol/ded9f4fe28748be3a37121fd5662dd20 to your computer and use it in GitHub Desktop.
Attach async tasks to SwiftUI views using a trigger mechanism.
import SwiftUI
struct TaskTrigger<T: Equatable>: Equatable {
fileprivate enum TaskState<S: Equatable>: Equatable {
case inactive
case active(value: S, uniqueId: UUID? = nil)
}
fileprivate var state: TaskState<T> = .inactive
mutating func trigger(value: T, id: UUID? = UUID()) {
self.state = .active(value: value, uniqueId: id)
}
mutating func cancel() {
self.state = .inactive
}
}
extension TaskTrigger where T == Bool {
mutating func trigger() {
self.state = .active(value: true)
}
}
struct TaskViewModifier<T: Equatable>: ViewModifier {
@Binding var trigger: TaskTrigger<T>
let action: @Sendable (_ arg: T) async -> Void
func body(content: Content) -> some View {
content
.task(id: trigger.state) {
guard case .active(let arg, _) = trigger.state else {
return
}
await action(arg)
if !Task.isCancelled {
self.trigger.cancel()
}
}
}
}
extension View {
func task<T: Equatable>(
_ trigger: Binding<TaskTrigger<T>>,
_ action: @escaping @Sendable @MainActor (_ arg: T) async -> Void
) -> some View {
modifier(TaskViewModifier(trigger: trigger, action: action))
}
func task(
_ trigger: Binding<TaskTrigger<Bool>>,
_ action: @escaping @Sendable () async -> Void
) -> some View {
modifier(TaskViewModifier(trigger: trigger, action: { _ in await action() }))
}
}
/************************
******* Examples *******
************************/
/*
This first example simply acts as a boolean trigger without any
attached value. Once you call `trigger()`, the async task will execute.
By being attached to the view it will also get cancelled once the
view gets dismissed.
*/
struct ContentView: View {
@State private var asyncTrigger: TaskTrigger<Bool> = TaskTrigger()
var body: some View {
Button("Do async stuff") {
asyncTrigger.trigger()
}
.task($asyncTrigger) {
await someAsyncFunction()
}
}
}
/*
This example attaches any equatable value (e.g. an integer) to the trigger.
Once you call `trigger(value:)`, the async task will execute and the attached
value is passed to the task. This might be useful when fetching some API based
on variable parameters.
*/
struct ContentView: View {
@State private var asyncTrigger: TaskTrigger<Int> = TaskTrigger()
var body: some View {
Button("Do async stuff") {
asyncTrigger.trigger(value: 42)
}
.task($asyncTrigger) { value in
await someAsyncFunctionWithInteger(value)
}
}
}
@lukepistrol
Copy link
Author

This is now available as a Swift Package as well: https://github.com/lukepistrol/TaskTrigger

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment