├── .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