I'm trying to prevent double taps on all buttons in my SwiftUI app and also reflect the state in the UI by disabling the button while it's blocked. Ideally, I'd like to do this through a Button extension so that I don't have to manually wrap every action or apply .disabled(...) everywhere.
Is there a clean way to extend Button (e.g., via init or a modifier) to wrap the action with a double-tap prevention logic and show the button as disabled during that time — without having to change the usage across the app?
Example of what I want to keep:
Button("Continue") {
onAction()
}
And have it automatically block rapid taps and show as disabled briefly.
The simplest solution is to override SwiftUI.Button
, You can takeover current instances by matching the initializers. the 2 that usually take over most of them are.
init(action: @escaping @Sendable () async throws -> Void, @ViewBuilder label: @escaping () -> L) {
init(_ titleKey: LocalizedStringKey, action: @escaping @Sendable () async throws -> Void) where L == Text {
Once you have a Basic View
you can create your own behaviors.
import SwiftUI
struct Button<L: View>: View {
@State private var isProcessing: Bool = false
let label: () -> L
let action: @Sendable () async throws -> Void
init(action: @escaping @Sendable () async throws -> Void, @ViewBuilder label: @escaping () -> L) {
self.label = label
self.action = action
}
init(_ titleKey: LocalizedStringKey, action: @escaping @Sendable () async throws -> Void) where L == Text {
label = {Text(titleKey)}
self.action = action
}
//Add more initializers to match SwiftUI.Button as needed.
var body: some View {
SwiftUI.Button(action: {
isProcessing.toggle() //trigger task/disable
}, label: {
label()
.overlay {
if isProcessing { //Present a visual for processing
ProgressView()
}
}
})
.disabled(isProcessing) // disable button
.task(id: isProcessing) { // actionable task
guard isProcessing else {return} // only process if true
do {
try await withThrowingTaskGroup { group in
//Make the action last at least a half second.
group.addTask {
try await Task.sleep(for: .seconds(0.5))
}
group.addTask {
try await action()
}
try await group.waitForAll()
}
} catch {
print(error)
//Present Alert
}
isProcessing = false // re-enable
}
}
}
#Preview {
VStack {
Button {
try await Task.sleep(for: .seconds(5))
} label: {
Text("Continue")
}
Button("Continue") {
try await Task.sleep(for: .seconds(5))
}
}
}