Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,16 @@ let package = Package(
],
targets: [
.target(
name: "AsyncSwiftUI"),
name: "AsyncSwiftUI",
dependencies: [
"Control",
]),
.target(
name: "Core"),
.target(
name: "Control",
dependencies: ["Core"]
),
.testTarget(
name: "AsyncSwiftUITests",
dependencies: ["AsyncSwiftUI"]),
Expand Down
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,62 @@
[![Twitter: @treastrain](https://img.shields.io/twitter/follow/treastrain?label=%40treastrain&style=social)](https://twitter.com/treastrain)

Wrappers for Swift Concurrency asynchronous actions on controls that enable user interaction specific to each platform and context provided by SwiftUI.

It's based on the following post: https://zenn.dev/treastrain/articles/3effccd39f4056

## Features
- ✅ Designed for Swift Concurrency and SwiftUI.
- ✅ Compatible with all Apple platforms (iPhone, iPod touch, iPad, Mac, Apple TV, Apple Watch, Apple Vision Pro).
- ✅ Automatically cancels the task and performs a new action on new user interaction.
- ✅ Automatically cancels the task after the view disappears before the action completes.
- ✅ Supports Sendable and inherits the Actor context.
- ✅ No dependencies.

## Samples
```swift
import AsyncSwiftUI

struct ContentView: View {
@State private var isRunning = false

var body: some View {
AsyncButton(isRunning ? "Running..." : "Run") {
isRunning = true
defer { isRunning = false }
do {
print("START")
try await Task.sleep(for: .seconds(2))
} catch {
print("ERROR:", error)
}
print("FINISH")
}
.disabled(isRunning)
}
}
```

## Installation
To use this library in a Swift Package Manager project, add the following line to the dependencies in your `Package.swift` file:

```swift
.package(url: "https://github.com/treastrain/AsyncSwiftUI", from: "0.1.0"),
```

Include `"AsyncSwiftUI"` as a dependency for your executable target:

```swift
.target(name: "<target>", dependencies: [
.product(name: "AsyncSwiftUI", package: "AsyncSwiftUI"),
]),
```

Finally, add `import AsyncSwiftUI` to your source code (or replace the existing `import SwiftUI` with it).

## Components

### Controls and indicators

| SwiftUI | AsyncSwiftUI |
|:---------------------------------------------------------------------|:--------------|
| [`Button`](https://developer.apple.com/documentation/swiftui/button) | `AsyncButton` |
8 changes: 0 additions & 8 deletions Sources/AsyncSwiftUI/AsyncSwiftUI.swift

This file was deleted.

9 changes: 9 additions & 0 deletions Sources/AsyncSwiftUI/_exported.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//
// _exported.swift
// AsyncSwiftUI
//
// Created by treastrain on 2023/11/03.
//

@_exported import SwiftUI
@_exported import Control
282 changes: 282 additions & 0 deletions Sources/Control/AsyncButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
//
// AsyncButton.swift
// AsyncSwiftUI
//
// Created by treastrain on 2023/11/03.
//

import Core
import SwiftUI

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
public struct AsyncButton<Label: View>: AsyncControl {
public typealias Base = Button<Label>

private let priority: TaskPriority
private let action: @Sendable () async -> Void
private let base: (_ trigger: _AsyncControlTrigger?) -> Base

@State private var trigger: _AsyncControlTrigger? = nil

private init(
priority: TaskPriority,
@_inheritActorContext action: @escaping @Sendable () async -> Void,
base: @escaping (_ trigger: _AsyncControlTrigger?) -> Base
) {
self.priority = priority
self.action = action
self.base = base
}

public var body: some View {
AsyncControlView(
priority: priority,
action: action,
base: base(trigger)
)
.onPreferenceChange(_AsyncControlTriggerPreferenceKey.self) {
trigger = $0
}
}
}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
extension AsyncButton {
public init(
priority: TaskPriority = .userInitiated,
@_inheritActorContext action: @escaping @Sendable () async -> Void,
@ViewBuilder label: @escaping () -> Label
) {
self.init(
priority: priority,
action: action,
base: { trigger in
Base(action: { trigger?() }, label: label)
}
)
}
}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
extension AsyncButton where Label == Text {
public init(
_ titleKey: LocalizedStringKey,
priority: TaskPriority = .userInitiated,
@_inheritActorContext action: @escaping @Sendable () async -> Void
) {
self.init(
priority: priority,
action: action,
base: { trigger in
Base(titleKey, action: { trigger?() })
}
)
}

@_disfavoredOverload
public init<S>(
_ title: S,
priority: TaskPriority = .userInitiated,
@_inheritActorContext action: @escaping @Sendable () async -> Void
) where S : Swift.StringProtocol {
self.init(
priority: priority,
action: action,
base: { trigger in
Base(title, action: { trigger?() })
}
)
}
}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
extension AsyncButton where Label == SwiftUI.Label<Text, Image> {
public init(
_ titleKey: LocalizedStringKey,
systemImage: String,
priority: TaskPriority = .userInitiated,
@_inheritActorContext action: @escaping @Sendable () async -> Void
) {
self.init(
priority: priority,
action: action,
base: { trigger in
Base(titleKey, systemImage: systemImage, action: { trigger?() })
}
)
}

@_disfavoredOverload
public init<S>(
_ title: S,
systemImage: String,
priority: TaskPriority = .userInitiated,
@_inheritActorContext action: @escaping @Sendable () async -> Void
) where S : StringProtocol {
self.init(
priority: priority,
action: action,
base: { trigger in
Base(title, systemImage: systemImage, action: { trigger?() })
}
)
}
}

@available(iOS 17.0, macOS 14.0, macCatalyst 17.0, tvOS 17.0, watchOS 10.0, visionOS 1.0, *)
extension AsyncButton where Label == SwiftUI.Label<Text, Image> {
public init(
_ titleKey: LocalizedStringKey,
image: ImageResource,
priority: TaskPriority = .userInitiated,
@_inheritActorContext action: @escaping @Sendable () async -> Void
) {
self.init(
priority: priority,
action: action,
base: { trigger in
Base(titleKey, image: image, action: { trigger?() })
}
)
}

@_disfavoredOverload
public init<S>(
_ title: S,
image: ImageResource,
priority: TaskPriority = .userInitiated,
@_inheritActorContext action: @escaping @Sendable () async -> Void
) where S : Swift.StringProtocol {
self.init(
priority: priority,
action: action,
base: { trigger in
Base(title, image: image, action: { trigger?() })
}
)
}
}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
extension AsyncButton {
public init(
role: ButtonRole?,
priority: TaskPriority = .userInitiated,
@_inheritActorContext action: @escaping @Sendable () async -> Void,
@ViewBuilder label: @escaping () -> Label
) {
self.init(
priority: priority,
action: action,
base: { trigger in
Base(role: role, action: { trigger?() }, label: label)
}
)
}
}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
extension AsyncButton where Label == Text {
public init(
_ titleKey: LocalizedStringKey,
role: ButtonRole?,
priority: TaskPriority = .userInitiated,
@_inheritActorContext action: @escaping @Sendable () async -> Void
) {
self.init(
priority: priority,
action: action,
base: { trigger in
Base(titleKey, role: role, action: { trigger?() })
}
)
}

@_disfavoredOverload
public init<S>(
_ title: S,
role: ButtonRole?,
priority: TaskPriority = .userInitiated,
@_inheritActorContext action: @escaping @Sendable () async -> Void
) where S : StringProtocol {
self.init(
priority: priority,
action: action,
base: { trigger in
Base(title, role: role, action: { trigger?() })
}
)
}
}

@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
extension AsyncButton where Label == SwiftUI.Label<Text, Image> {
public init(
_ titleKey: LocalizedStringKey,
systemImage: String,
role: ButtonRole?,
priority: TaskPriority = .userInitiated,
@_inheritActorContext action: @escaping @Sendable () async -> Void
) {
self.init(
priority: priority,
action: action,
base: { trigger in
Base(titleKey, systemImage: systemImage, role: role, action: { trigger?() })
}
)
}

@_disfavoredOverload
public init<S>(
_ title: S,
systemImage: String,
role: ButtonRole?,
priority: TaskPriority = .userInitiated,
@_inheritActorContext action: @escaping @Sendable () async -> Void
) where S : StringProtocol {
self.init(
priority: priority,
action: action,
base: { trigger in
Base(title, systemImage: systemImage, role: role, action: { trigger?() })
}
)
}
}

@available(iOS 17.0, macOS 14.0, macCatalyst 17.0, tvOS 17.0, watchOS 10.0, visionOS 1.0, *)
extension AsyncButton where Label == SwiftUI.Label<Text, Image> {
public init(
_ titleKey: LocalizedStringKey,
image: ImageResource,
role: ButtonRole?,
priority: TaskPriority = .userInitiated,
@_inheritActorContext action: @escaping @Sendable () async -> Void
) {
self.init(
priority: priority,
action: action,
base: { trigger in
Base(titleKey, image: image, role: role, action: { trigger?() })
}
)
}

@_disfavoredOverload
public init<S>(
_ title: S,
image: ImageResource,
role: ButtonRole?,
priority: TaskPriority = .userInitiated,
@_inheritActorContext action: @escaping @Sendable () async -> Void
) where S : Swift.StringProtocol {
self.init(
priority: priority,
action: action,
base: { trigger in
Base(title, image: image, role: role, action: { trigger?() })
}
)
}
}