Skip to content

Commit 34e8b33

Browse files
authored
Merge pull request #2 from treastrain/async-button
Add `AsyncButton`
2 parents da1a539 + fb63400 commit 34e8b33

File tree

5 files changed

+358
-9
lines changed

5 files changed

+358
-9
lines changed

Package.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,16 @@ let package = Package(
1212
],
1313
targets: [
1414
.target(
15-
name: "AsyncSwiftUI"),
15+
name: "AsyncSwiftUI",
16+
dependencies: [
17+
"Control",
18+
]),
1619
.target(
1720
name: "Core"),
21+
.target(
22+
name: "Control",
23+
dependencies: ["Core"]
24+
),
1825
.testTarget(
1926
name: "AsyncSwiftUITests",
2027
dependencies: ["AsyncSwiftUI"]),

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,62 @@
77
[![Twitter: @treastrain](https://img.shields.io/twitter/follow/treastrain?label=%40treastrain&style=social)](https://twitter.com/treastrain)
88

99
Wrappers for Swift Concurrency asynchronous actions on controls that enable user interaction specific to each platform and context provided by SwiftUI.
10+
11+
It's based on the following post: https://zenn.dev/treastrain/articles/3effccd39f4056
12+
13+
## Features
14+
- ✅ Designed for Swift Concurrency and SwiftUI.
15+
- ✅ Compatible with all Apple platforms (iPhone, iPod touch, iPad, Mac, Apple TV, Apple Watch, Apple Vision Pro).
16+
- ✅ Automatically cancels the task and performs a new action on new user interaction.
17+
- ✅ Automatically cancels the task after the view disappears before the action completes.
18+
- ✅ Supports Sendable and inherits the Actor context.
19+
- ✅ No dependencies.
20+
21+
## Samples
22+
```swift
23+
import AsyncSwiftUI
24+
25+
struct ContentView: View {
26+
@State private var isRunning = false
27+
28+
var body: some View {
29+
AsyncButton(isRunning ? "Running..." : "Run") {
30+
isRunning = true
31+
defer { isRunning = false }
32+
do {
33+
print("START")
34+
try await Task.sleep(for: .seconds(2))
35+
} catch {
36+
print("ERROR:", error)
37+
}
38+
print("FINISH")
39+
}
40+
.disabled(isRunning)
41+
}
42+
}
43+
```
44+
45+
## Installation
46+
To use this library in a Swift Package Manager project, add the following line to the dependencies in your `Package.swift` file:
47+
48+
```swift
49+
.package(url: "https://github.com/treastrain/AsyncSwiftUI", from: "0.1.0"),
50+
```
51+
52+
Include `"AsyncSwiftUI"` as a dependency for your executable target:
53+
54+
```swift
55+
.target(name: "<target>", dependencies: [
56+
.product(name: "AsyncSwiftUI", package: "AsyncSwiftUI"),
57+
]),
58+
```
59+
60+
Finally, add `import AsyncSwiftUI` to your source code (or replace the existing `import SwiftUI` with it).
61+
62+
## Components
63+
64+
### Controls and indicators
65+
66+
| SwiftUI | AsyncSwiftUI |
67+
|:---------------------------------------------------------------------|:--------------|
68+
| [`Button`](https://developer.apple.com/documentation/swiftui/button) | `AsyncButton` |

Sources/AsyncSwiftUI/AsyncSwiftUI.swift

Lines changed: 0 additions & 8 deletions
This file was deleted.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//
2+
// _exported.swift
3+
// AsyncSwiftUI
4+
//
5+
// Created by treastrain on 2023/11/03.
6+
//
7+
8+
@_exported import SwiftUI
9+
@_exported import Control

Sources/Control/AsyncButton.swift

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
//
2+
// AsyncButton.swift
3+
// AsyncSwiftUI
4+
//
5+
// Created by treastrain on 2023/11/03.
6+
//
7+
8+
import Core
9+
import SwiftUI
10+
11+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
12+
public struct AsyncButton<Label: View>: AsyncControl {
13+
public typealias Base = Button<Label>
14+
15+
private let priority: TaskPriority
16+
private let action: @Sendable () async -> Void
17+
private let base: (_ trigger: _AsyncControlTrigger?) -> Base
18+
19+
@State private var trigger: _AsyncControlTrigger? = nil
20+
21+
private init(
22+
priority: TaskPriority,
23+
@_inheritActorContext action: @escaping @Sendable () async -> Void,
24+
base: @escaping (_ trigger: _AsyncControlTrigger?) -> Base
25+
) {
26+
self.priority = priority
27+
self.action = action
28+
self.base = base
29+
}
30+
31+
public var body: some View {
32+
AsyncControlView(
33+
priority: priority,
34+
action: action,
35+
base: base(trigger)
36+
)
37+
.onPreferenceChange(_AsyncControlTriggerPreferenceKey.self) {
38+
trigger = $0
39+
}
40+
}
41+
}
42+
43+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
44+
extension AsyncButton {
45+
public init(
46+
priority: TaskPriority = .userInitiated,
47+
@_inheritActorContext action: @escaping @Sendable () async -> Void,
48+
@ViewBuilder label: @escaping () -> Label
49+
) {
50+
self.init(
51+
priority: priority,
52+
action: action,
53+
base: { trigger in
54+
Base(action: { trigger?() }, label: label)
55+
}
56+
)
57+
}
58+
}
59+
60+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
61+
extension AsyncButton where Label == Text {
62+
public init(
63+
_ titleKey: LocalizedStringKey,
64+
priority: TaskPriority = .userInitiated,
65+
@_inheritActorContext action: @escaping @Sendable () async -> Void
66+
) {
67+
self.init(
68+
priority: priority,
69+
action: action,
70+
base: { trigger in
71+
Base(titleKey, action: { trigger?() })
72+
}
73+
)
74+
}
75+
76+
@_disfavoredOverload
77+
public init<S>(
78+
_ title: S,
79+
priority: TaskPriority = .userInitiated,
80+
@_inheritActorContext action: @escaping @Sendable () async -> Void
81+
) where S : Swift.StringProtocol {
82+
self.init(
83+
priority: priority,
84+
action: action,
85+
base: { trigger in
86+
Base(title, action: { trigger?() })
87+
}
88+
)
89+
}
90+
}
91+
92+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
93+
extension AsyncButton where Label == SwiftUI.Label<Text, Image> {
94+
public init(
95+
_ titleKey: LocalizedStringKey,
96+
systemImage: String,
97+
priority: TaskPriority = .userInitiated,
98+
@_inheritActorContext action: @escaping @Sendable () async -> Void
99+
) {
100+
self.init(
101+
priority: priority,
102+
action: action,
103+
base: { trigger in
104+
Base(titleKey, systemImage: systemImage, action: { trigger?() })
105+
}
106+
)
107+
}
108+
109+
@_disfavoredOverload
110+
public init<S>(
111+
_ title: S,
112+
systemImage: String,
113+
priority: TaskPriority = .userInitiated,
114+
@_inheritActorContext action: @escaping @Sendable () async -> Void
115+
) where S : StringProtocol {
116+
self.init(
117+
priority: priority,
118+
action: action,
119+
base: { trigger in
120+
Base(title, systemImage: systemImage, action: { trigger?() })
121+
}
122+
)
123+
}
124+
}
125+
126+
@available(iOS 17.0, macOS 14.0, macCatalyst 17.0, tvOS 17.0, watchOS 10.0, visionOS 1.0, *)
127+
extension AsyncButton where Label == SwiftUI.Label<Text, Image> {
128+
public init(
129+
_ titleKey: LocalizedStringKey,
130+
image: ImageResource,
131+
priority: TaskPriority = .userInitiated,
132+
@_inheritActorContext action: @escaping @Sendable () async -> Void
133+
) {
134+
self.init(
135+
priority: priority,
136+
action: action,
137+
base: { trigger in
138+
Base(titleKey, image: image, action: { trigger?() })
139+
}
140+
)
141+
}
142+
143+
@_disfavoredOverload
144+
public init<S>(
145+
_ title: S,
146+
image: ImageResource,
147+
priority: TaskPriority = .userInitiated,
148+
@_inheritActorContext action: @escaping @Sendable () async -> Void
149+
) where S : Swift.StringProtocol {
150+
self.init(
151+
priority: priority,
152+
action: action,
153+
base: { trigger in
154+
Base(title, image: image, action: { trigger?() })
155+
}
156+
)
157+
}
158+
}
159+
160+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
161+
extension AsyncButton {
162+
public init(
163+
role: ButtonRole?,
164+
priority: TaskPriority = .userInitiated,
165+
@_inheritActorContext action: @escaping @Sendable () async -> Void,
166+
@ViewBuilder label: @escaping () -> Label
167+
) {
168+
self.init(
169+
priority: priority,
170+
action: action,
171+
base: { trigger in
172+
Base(role: role, action: { trigger?() }, label: label)
173+
}
174+
)
175+
}
176+
}
177+
178+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
179+
extension AsyncButton where Label == Text {
180+
public init(
181+
_ titleKey: LocalizedStringKey,
182+
role: ButtonRole?,
183+
priority: TaskPriority = .userInitiated,
184+
@_inheritActorContext action: @escaping @Sendable () async -> Void
185+
) {
186+
self.init(
187+
priority: priority,
188+
action: action,
189+
base: { trigger in
190+
Base(titleKey, role: role, action: { trigger?() })
191+
}
192+
)
193+
}
194+
195+
@_disfavoredOverload
196+
public init<S>(
197+
_ title: S,
198+
role: ButtonRole?,
199+
priority: TaskPriority = .userInitiated,
200+
@_inheritActorContext action: @escaping @Sendable () async -> Void
201+
) where S : StringProtocol {
202+
self.init(
203+
priority: priority,
204+
action: action,
205+
base: { trigger in
206+
Base(title, role: role, action: { trigger?() })
207+
}
208+
)
209+
}
210+
}
211+
212+
@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
213+
extension AsyncButton where Label == SwiftUI.Label<Text, Image> {
214+
public init(
215+
_ titleKey: LocalizedStringKey,
216+
systemImage: String,
217+
role: ButtonRole?,
218+
priority: TaskPriority = .userInitiated,
219+
@_inheritActorContext action: @escaping @Sendable () async -> Void
220+
) {
221+
self.init(
222+
priority: priority,
223+
action: action,
224+
base: { trigger in
225+
Base(titleKey, systemImage: systemImage, role: role, action: { trigger?() })
226+
}
227+
)
228+
}
229+
230+
@_disfavoredOverload
231+
public init<S>(
232+
_ title: S,
233+
systemImage: String,
234+
role: ButtonRole?,
235+
priority: TaskPriority = .userInitiated,
236+
@_inheritActorContext action: @escaping @Sendable () async -> Void
237+
) where S : StringProtocol {
238+
self.init(
239+
priority: priority,
240+
action: action,
241+
base: { trigger in
242+
Base(title, systemImage: systemImage, role: role, action: { trigger?() })
243+
}
244+
)
245+
}
246+
}
247+
248+
@available(iOS 17.0, macOS 14.0, macCatalyst 17.0, tvOS 17.0, watchOS 10.0, visionOS 1.0, *)
249+
extension AsyncButton where Label == SwiftUI.Label<Text, Image> {
250+
public init(
251+
_ titleKey: LocalizedStringKey,
252+
image: ImageResource,
253+
role: ButtonRole?,
254+
priority: TaskPriority = .userInitiated,
255+
@_inheritActorContext action: @escaping @Sendable () async -> Void
256+
) {
257+
self.init(
258+
priority: priority,
259+
action: action,
260+
base: { trigger in
261+
Base(titleKey, image: image, role: role, action: { trigger?() })
262+
}
263+
)
264+
}
265+
266+
@_disfavoredOverload
267+
public init<S>(
268+
_ title: S,
269+
image: ImageResource,
270+
role: ButtonRole?,
271+
priority: TaskPriority = .userInitiated,
272+
@_inheritActorContext action: @escaping @Sendable () async -> Void
273+
) where S : Swift.StringProtocol {
274+
self.init(
275+
priority: priority,
276+
action: action,
277+
base: { trigger in
278+
Base(title, image: image, role: role, action: { trigger?() })
279+
}
280+
)
281+
}
282+
}

0 commit comments

Comments
 (0)