Last active
November 19, 2023 19:32
-
-
Save lukepistrol/ded9f4fe28748be3a37121fd5662dd20 to your computer and use it in GitHub Desktop.
Attach async tasks to SwiftUI views using a trigger mechanism.
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
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) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is now available as a Swift Package as well: https://github.com/lukepistrol/TaskTrigger