├── .spi.yml ├── .gitignore ├── Sources └── AsyncButton │ ├── UnlocalizedError.swift │ ├── AnyLocalizedError.swift │ ├── AsyncButtonOperations.swift │ ├── AsyncButtonOptions.swift │ └── AsyncButton.swift ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Package.swift └── README.md /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: ["AsyncButton"] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /Sources/AsyncButton/UnlocalizedError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct UnlocalizedError: LocalizedError { 4 | 5 | let errorDescription: String? 6 | 7 | init(error: Error) { 8 | self.errorDescription = error.localizedDescription 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/AsyncButton/AnyLocalizedError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct AnyLocalizedError: LocalizedError { 4 | 5 | let errorDescription: String? 6 | 7 | let failureReason: String? 8 | 9 | let recoverySuggestion: String? 10 | 11 | let helpAnchor: String? 12 | 13 | init(erasing localizedError: LocalizedError) { 14 | errorDescription = localizedError.errorDescription 15 | failureReason = localizedError.failureReason 16 | recoverySuggestion = localizedError.recoverySuggestion 17 | helpAnchor = localizedError.helpAnchor 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "swiftui-async-button", 7 | platforms: [ 8 | .iOS(.v16), 9 | .macOS(.v13), 10 | .tvOS(.v16), 11 | .watchOS(.v9) 12 | ], 13 | products: [ 14 | .library( 15 | name: "AsyncButton", 16 | targets: ["AsyncButton"]), 17 | ], 18 | targets: [ 19 | .target( 20 | name: "AsyncButton", 21 | dependencies: [], 22 | path: "Sources") 23 | ] 24 | ) 25 | 26 | #if swift(>=5.6) 27 | // Add the documentation compiler plugin if possible 28 | package.dependencies.append( 29 | .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0") 30 | ) 31 | #endif 32 | -------------------------------------------------------------------------------- /Sources/AsyncButton/AsyncButtonOperations.swift: -------------------------------------------------------------------------------- 1 | public enum AsyncButtonOperation { 2 | 3 | case loading(Task) 4 | case completed(Task, Result) 5 | 6 | var task: Task { 7 | switch self { 8 | case .loading(let task): 9 | return task 10 | case .completed(let task, _): 11 | return task 12 | } 13 | } 14 | } 15 | 16 | extension AsyncButtonOperation: Equatable { 17 | 18 | public static func == (lhs: AsyncButtonOperation, rhs: AsyncButtonOperation) -> Bool { 19 | if case .loading(let lhsTask) = lhs, case .loading(let rhsTask) = rhs { 20 | return lhsTask == rhsTask 21 | } else if case .completed(let lhsTask, _) = lhs, case .completed(let rhsTask, _) = rhs { 22 | return lhsTask == rhsTask 23 | } else { 24 | return false 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/AsyncButton/AsyncButtonOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Lorenzo Fiamingo on 27/06/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct AsyncButtonOptions: OptionSet { 11 | 12 | public let rawValue: Int 13 | 14 | public static let disableButtonOnLoading = AsyncButtonOptions(rawValue: 1 << 0) 15 | public static let showProgressViewOnLoading = AsyncButtonOptions(rawValue: 1 << 1) 16 | public static let showAlertOnError = AsyncButtonOptions(rawValue: 1 << 2) 17 | public static let disallowParallelOperations = AsyncButtonOptions(rawValue: 1 << 3) 18 | public static let enableNotificationFeedback = AsyncButtonOptions(rawValue: 1 << 4) 19 | public static let enableTintFeedback = AsyncButtonOptions(rawValue: 1 << 5) 20 | 21 | public static let all: AsyncButtonOptions = [.disableButtonOnLoading, .showProgressViewOnLoading, .showAlertOnError, .disallowParallelOperations, .enableNotificationFeedback, .enableTintFeedback] 22 | public static let automatic: AsyncButtonOptions = [.disableButtonOnLoading, .showProgressViewOnLoading, .showAlertOnError, .disallowParallelOperations, .enableNotificationFeedback] 23 | 24 | public init(rawValue: Int) { 25 | self.rawValue = rawValue 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftUI AsyncButton 🖲️ 2 | 3 | `AsyncButton` is a `Button` capable of running concurrent code. 4 | 5 | 6 | ## Usage 7 | 8 | `AsyncButton` has the exact same API as `Button`, so you just have to change this: 9 | ```swift 10 | Button("Run") { run() } 11 | ``` 12 | to this: 13 | ```swift 14 | AsyncButton("Run") { try await run() } 15 | ``` 16 | 17 | In addition to `Button` initializers, you have the possibilities to specify special behaviours via `AsyncButtonOptions`: 18 | ```swift 19 | AsyncButton("Ciao", options: [.showProgressViewOnLoading, .showAlertOnError], transaction: Transaction(animation: .default)) { 20 | try await run() 21 | } 22 | ``` 23 | 24 | For heavy customizations you can have access to the `AsyncButtonOperation`s: 25 | 26 | ```swift 27 | AsyncButton { 28 | try await run() 29 | } label: { operations in 30 | if operations.contains { operation in 31 | if case .loading = operation { 32 | return true 33 | } else { 34 | return false 35 | } 36 | } { 37 | Text("Loading") 38 | } else if 39 | let last = operations.last, 40 | case .completed(_, let result) = last 41 | { 42 | switch result { 43 | case .failure: 44 | Text("Try again") 45 | case .success: 46 | Text("Run again") 47 | } 48 | } else { 49 | Text("Run") 50 | } 51 | } 52 | ``` 53 | 54 | ## Installation 55 | 56 | 1. In Xcode, open your project and navigate to **File** → **Swift Packages** → **Add Package Dependency...** 57 | 2. Paste the repository URL (`https://github.com/lorenzofiamingo/swiftui-async-button`) and click **Next**. 58 | 3. Click **Finish**. 59 | 60 | 61 | ## Other projects 62 | 63 | [SwiftUI VariadicViews 🥞](https://github.com/lorenzofiamingo/swiftui-variadic-views) 64 | 65 | [SwiftUI CachedAsyncImage 🗃️](https://github.com/lorenzofiamingo/swiftui-cached-async-image) 66 | 67 | [SwiftUI MapItemPicker 🗺️](https://github.com/lorenzofiamingo/swiftui-map-item-picker) 68 | 69 | [SwiftUI PhotosPicker 🌇](https://github.com/lorenzofiamingo/swiftui-photos-picker) 70 | 71 | [SwiftUI VerticalTabView 🔝](https://github.com/lorenzofiamingo/swiftui-vertical-tab-view) 72 | 73 | [SwiftUI SharedObject 🍱](https://github.com/lorenzofiamingo/swiftui-shared-object) 74 | -------------------------------------------------------------------------------- /Sources/AsyncButton/AsyncButton.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct AsyncButton