From 4a6f815b99f563c86abba18f65eb73a769d3d7c6 Mon Sep 17 00:00:00 2001 From: Thomas Durand Date: Sun, 12 Jan 2025 15:10:06 +0100 Subject: [PATCH] feat: added onButtonError modifier onButtonError() modifier allows to have a custom behavior for when a button throws, that can be shared across multiple instances of buttons Fixes issue #11 --- .../Buttons/ThrowableButtonDemo.swift | 4 ++ Sources/ButtonKit/Button+Async.swift | 3 + .../Modifiers/Button+AsyncError.swift | 71 +++++++++++++++++++ .../Modifiers/Button+AsyncTask.swift | 2 +- 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 Sources/ButtonKit/Modifiers/Button+AsyncError.swift diff --git a/Demo/ButtonKitDemo/Buttons/ThrowableButtonDemo.swift b/Demo/ButtonKitDemo/Buttons/ThrowableButtonDemo.swift index d6d8a26..a8d2a1b 100644 --- a/Demo/ButtonKitDemo/Buttons/ThrowableButtonDemo.swift +++ b/Demo/ButtonKitDemo/Buttons/ThrowableButtonDemo.swift @@ -49,6 +49,10 @@ struct ThrowableButtonDemo: View { .throwableButtonStyle(.none) } .buttonStyle(.borderedProminent) + .onButtonError { error in + // Do something with the error + print(error) + } } } diff --git a/Sources/ButtonKit/Button+Async.swift b/Sources/ButtonKit/Button+Async.swift index 750315e..489f235 100644 --- a/Sources/ButtonKit/Button+Async.swift +++ b/Sources/ButtonKit/Button+Async.swift @@ -78,6 +78,7 @@ public struct AsyncButton: View { @State private var state: AsyncButtonState = .idle @ObservedObject private var progress: P @State private var errorCount = 0 + @State private var lastError: Error? public var body: some View { let throwableLabelConfiguration = ThrowableButtonStyleLabelConfiguration( @@ -110,6 +111,7 @@ public struct AsyncButton: View { .allowsHitTesting(allowsHitTestingWhenLoading || !state.isLoading) .disabled(disabledWhenLoading && state.isLoading) .preference(key: AsyncButtonTaskPreferenceKey.self, value: state) + .preference(key: AsyncButtonErrorPreferenceKey.self, value: lastError.flatMap { .init(increment: errorCount, error: $0) }) .onAppear { guard let id else { return @@ -150,6 +152,7 @@ public struct AsyncButton: View { try await action(progress) } catch { errorCount += 1 + lastError = error } // Reset progress await progress.ended() diff --git a/Sources/ButtonKit/Modifiers/Button+AsyncError.swift b/Sources/ButtonKit/Modifiers/Button+AsyncError.swift new file mode 100644 index 0000000..0ddd24c --- /dev/null +++ b/Sources/ButtonKit/Modifiers/Button+AsyncError.swift @@ -0,0 +1,71 @@ +// +// Button+AsyncError.swift +// ButtonKit +// +// Created by Thomas Durand on 12/01/2025. +// + +import SwiftUI + +// MARK: Public protocol + +public typealias AsyncButtonErrorHandler = @MainActor @Sendable (Error) -> Void + +extension View { + public func onButtonError(_ handler: @escaping AsyncButtonErrorHandler) -> some View { + modifier(OnAsyncButtonErrorChangeModifier(handler: { error in + handler(error) + })) + } +} + +// MARK: - Internal implementation + +struct AsyncButtonErrorPreferenceKey: PreferenceKey { + static let defaultValue: ErrorHolder? = nil + + static func reduce(value: inout ErrorHolder?, nextValue: () -> ErrorHolder?) { + guard let newValue = nextValue() else { + return + } + value = .init(increment: (value?.increment ?? 0) + newValue.increment, error: newValue.error) + } +} + +struct ErrorHolder: Equatable { + let increment: Int + let error: Error + + static func == (lhs: ErrorHolder, rhs: ErrorHolder) -> Bool { + lhs.increment == rhs.increment + } +} + +struct OnAsyncButtonErrorChangeModifier: ViewModifier { + let handler: AsyncButtonErrorHandler + + init(handler: @escaping AsyncButtonErrorHandler) { + self.handler = handler + } + + func body(content: Content) -> some View { + content + .onPreferenceChange(AsyncButtonErrorPreferenceKey.self) { value in + guard let error = value?.error else { + return + } + #if swift(>=5.10) + MainActor.assumeIsolated { + onError(error) + } + #else + onError(error) + #endif + } + } + + @MainActor + func onError(_ error: Error) { + handler(error) + } +} diff --git a/Sources/ButtonKit/Modifiers/Button+AsyncTask.swift b/Sources/ButtonKit/Modifiers/Button+AsyncTask.swift index 3cd94f4..0987460 100644 --- a/Sources/ButtonKit/Modifiers/Button+AsyncTask.swift +++ b/Sources/ButtonKit/Modifiers/Button+AsyncTask.swift @@ -57,7 +57,7 @@ extension View { } } -// Internal implementation +// MARK: - Internal implementation struct AsyncButtonTaskPreferenceKey: PreferenceKey { static let defaultValue: AsyncButtonState = .idle