Created
February 14, 2024 12:27
-
-
Save groue/c17af67a285ea441e4b16de4e5a35530 to your computer and use it in GitHub Desktop.
A Trigger type that helps SwiftUI views control when cancellable async jobs are run.
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 | |
extension View { | |
/// Adds a task to perform before this view appears or when the trigger | |
/// is fired. | |
/// | |
/// This method behaves like `View.task(id:priority:_:)`, except that it | |
/// cancels and recreates the task when the `fire` method of the | |
/// trigger is called. | |
/// | |
/// For example: | |
/// | |
/// ```swift | |
/// struct MyView: View { | |
/// @State var myTrigger = Trigger() | |
/// | |
/// var body: some View { | |
/// Group { | |
/// Button("Fire") { | |
/// myTrigger.fire() | |
/// } | |
/// } | |
/// .task(trigger: myTrigger) { fired in | |
/// // Run before the view appears, or when the trigger is | |
/// // fired. Test the `fired` argument if you want to | |
/// // distinguish between the two situations. | |
/// await performJob() | |
/// } | |
/// } | |
/// } | |
/// ``` | |
/// | |
/// - Parameters: | |
/// - trigger: The trigger to observe for changes. | |
/// - priority: The task priority to use when creating the asynchronous | |
/// task. | |
/// - action: A closure that SwiftUI calls as an asynchronous task | |
/// before the view appears. SwiftUI can automatically cancel the | |
/// task after the view disappears before the action completes. If | |
/// the `trigger` is fired, SwiftUI cancels and restarts the task. | |
/// The argument is true if the task is started because the trigger | |
/// was fired, and it is false if the task is started before the | |
/// view appears. | |
/// | |
/// - Returns: A view that runs the specified action asynchronously before | |
/// the view appears, or restarts the task with the `trigger` value | |
/// is fired. | |
public func task( | |
trigger: Trigger, | |
priority: TaskPriority = .userInitiated, | |
@_inheritActorContext _ action: @escaping @Sendable (_ fired: Bool) async -> Void) | |
-> some View | |
{ | |
modifier(TriggerModifier( | |
triggerId: trigger.id, | |
priority: priority, | |
action: action)) | |
} | |
} | |
private struct TriggerModifier: ViewModifier { | |
@State private var lastTriggerId = Trigger.ID() | |
var triggerId: Trigger.ID | |
var priority: TaskPriority | |
var action: @Sendable (_ fired: Bool) async -> Void | |
func body(content: Content) -> some View { | |
content.task(id: triggerId, priority: priority) { | |
let fired = (lastTriggerId != triggerId) | |
lastTriggerId = triggerId | |
await action(fired) | |
} | |
} | |
} | |
public struct Trigger: Identifiable, Sendable { | |
public struct ID: Hashable, Sendable { | |
private var rawValue: Int = 0 | |
mutating func touch() { | |
rawValue += 1 | |
} | |
} | |
public var id = ID() | |
public init() { } | |
public mutating func fire() { | |
self.id.touch() | |
} | |
} | |
#if DEBUG | |
#Preview { | |
struct Preview: View { | |
@State var trigger = Trigger() | |
@State var isRunning = false | |
@State var wasFired = false | |
@State var cancelledTaskCount = 0 | |
@State var completedTaskCount = 0 | |
var body: some View { | |
VStack { | |
Button { | |
trigger.fire() | |
} label: { | |
Text(verbatim: "Fire Task") | |
} | |
Text(verbatim: "Number of completed tasks: \(completedTaskCount)") | |
Text(verbatim: "Number of cancelled tasks: \(cancelledTaskCount)") | |
if isRunning { | |
HStack(spacing: 10) { | |
Text(verbatim: "Task is running (fired: \(wasFired))") | |
ProgressView() | |
} | |
} | |
} | |
.task(trigger: trigger) { fired in | |
do { | |
wasFired = fired | |
isRunning = true | |
try await Task.sleep(for: .seconds(2)) | |
completedTaskCount += 1 | |
isRunning = false | |
} catch { | |
cancelledTaskCount += 1 | |
isRunning = false | |
} | |
} | |
} | |
} | |
return TabView { | |
Preview() | |
.tabItem { | |
Label("A", systemImage: "circle") | |
} | |
Preview() | |
.tabItem { | |
Label("B", systemImage: "square") | |
} | |
} | |
} | |
#endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment