diff --git a/UI/NoorUI/Components/AppStoreDownloadButton.swift b/UI/NoorUI/Components/AppStoreDownloadButton.swift index fd5c22de..f873c932 100644 --- a/UI/NoorUI/Components/AppStoreDownloadButton.swift +++ b/UI/NoorUI/Components/AppStoreDownloadButton.swift @@ -21,7 +21,7 @@ struct AppStoreDownloadButton: View { let action: AsyncAction var body: some View { - Button(asyncAction: action) { + AsyncButton(action: action) { Group { switch type { case .pending: diff --git a/UI/NoorUI/Components/List/NoorListItem.swift b/UI/NoorUI/Components/List/NoorListItem.swift index 824e3bc6..ca295c62 100644 --- a/UI/NoorUI/Components/List/NoorListItem.swift +++ b/UI/NoorUI/Components/List/NoorListItem.swift @@ -96,9 +96,9 @@ public struct NoorListItem: View { if let accessory, accessory.actionable { // Use Tap gesture since tapping accessory button will also trigger the whole cell selection. content - .onTapGesture(asyncAction: action) + .onAsyncTapGesture(asyncAction: action) } else { - Button(asyncAction: action) { + AsyncButton(action: action) { content } } diff --git a/UI/NoorUI/Features/AdvancedAudioOptions/AdvancedAudioOptionsView.swift b/UI/NoorUI/Features/AdvancedAudioOptions/AdvancedAudioOptionsView.swift index 8b920081..4cb3641a 100644 --- a/UI/NoorUI/Features/AdvancedAudioOptions/AdvancedAudioOptionsView.swift +++ b/UI/NoorUI/Features/AdvancedAudioOptions/AdvancedAudioOptionsView.swift @@ -67,7 +67,7 @@ private struct LastVerseButton: View { let action: AsyncAction var body: some View { - Button(asyncAction: action) { + AsyncButton(action: action) { Text(label) .foregroundColor(.white) .padding(.vertical, 5) diff --git a/UI/NoorUI/Features/AyahMenu/AyahMenuView.swift b/UI/NoorUI/Features/AyahMenu/AyahMenuView.swift index 8901c4a8..eadf374a 100644 --- a/UI/NoorUI/Features/AyahMenu/AyahMenuView.swift +++ b/UI/NoorUI/Features/AyahMenu/AyahMenuView.swift @@ -225,10 +225,7 @@ private struct Row<Symbol: View>: View { @ScaledMetric var verticalPadding = 12 var body: some View { - Button { - await action() - } - label: { + AsyncButton(action: action) { HStack { ZStack { IconCircles() @@ -314,8 +311,8 @@ private struct NoteCircles: View { var body: some View { HStack { ForEach(Note.Color.sortedColors, id: \.self) { color in - Button( - asyncAction: { await tapped(color) }, + AsyncButton( + action: { await tapped(color) }, label: { NoteCircle(color: color.color, selected: color == selectedColor) } ) .shadow(color: Color.tertiarySystemGroupedBackground, radius: 1) diff --git a/UI/UIx/SwiftUI/Miscellaneous/AsyncAction.swift b/UI/UIx/SwiftUI/Miscellaneous/AsyncAction.swift index a42d9bae..4135d736 100644 --- a/UI/UIx/SwiftUI/Miscellaneous/AsyncAction.swift +++ b/UI/UIx/SwiftUI/Miscellaneous/AsyncAction.swift @@ -12,22 +12,63 @@ public typealias AsyncAction = @MainActor @Sendable () async -> Void public typealias ItemAction<Item> = @MainActor @Sendable (Item) -> Void public typealias AsyncItemAction<Item> = @MainActor @Sendable (Item) async -> Void -extension Button { - public init(asyncAction: @escaping AsyncAction, @ViewBuilder label: () -> Label) { - self.init(action: { - Task { - await asyncAction() +public struct AsyncButton<Label: View>: View { + // MARK: Lifecycle + + public init(action: @escaping AsyncAction, label: () -> Label) { + self.action = action + self.label = label() + } + + // MARK: Public + + public var body: some View { + Button { + // Cancel the previous task + currentTask?.cancel() + + // Start a new task + currentTask = Task { + await action() } - }, label: label) + } label: { + label + } } + + // MARK: Private + + private let action: AsyncAction + private let label: Label + + @State private var currentTask: Task<Void, Never>? = nil } extension View { - public func onTapGesture(count: Int = 1, asyncAction action: @escaping AsyncAction) -> some View { - onTapGesture(count: count, perform: { - Task { + public func onAsyncTapGesture(count: Int = 1, asyncAction action: @escaping AsyncAction) -> some View { + modifier(AsyncTapGestureModifier(count: count, action: action)) + } +} + +private struct AsyncTapGestureModifier: ViewModifier { + // MARK: Internal + + let count: Int + let action: AsyncAction + + func body(content: Content) -> some View { + content.onTapGesture(count: count, perform: { + // Cancel the previous task + currentTask?.cancel() + + // Start a new task + currentTask = Task { await action() } }) } + + // MARK: Private + + @State private var currentTask: Task<Void, Never>? = nil }