ButtonKit 提供了一个新的 SwiftUI Button 替代方案,用于处理可抛出异常和异步操作。默认情况下,SwiftUI Button 仅接受闭包。
使用 ButtonKit,你将可以使用 AsyncButton
视图,它接受一个 () async throws -> Void
闭包。
使用 Swift Package Manager 安装
dependencies: [
.package(url: "https://github.com/Dean151/ButtonKit.git", from: "0.3.0"),
],
targets: [
.target(name: "MyTarget", dependencies: [
.product(name: "ButtonKit", package: "ButtonKit"),
]),
]
并导入它
import ButtonKit
像使用任何 SwiftUI button 一样使用它,但如果需要在闭包中抛出异常。
AsyncButton {
try doSomethingThatCanFail()
} label {
Text("Do something")
}
默认情况下,当 button 的闭包抛出异常时,button 会抖动。
目前,仅内置了这种抖动行为。
![]() |
.throwableButtonStyle(.shake) |
你仍然可以通过将 .none
传递给 throwableButtonStyle 来禁用它。
AsyncButton {
try doSomethingThatCanFail()
} label {
Text("Do something")
}
.throwableButtonStyle(.none)
你也可以使用 ThrowableButtonStyle
协议来实现你自己的行为。
在 ThrowableButtonStyle 中,你可以实现 makeLabel
、makeButton
或两者来改变 button 的外观和行为。
public struct TryAgainThrowableButtonStyle: ThrowableButtonStyle {
public init() {}
public func makeLabel(configuration: LabelConfiguration) -> some View {
if configuration.errorCount > 0 {
Text("Try again!")
} else {
configuration.label
}
}
}
extension ThrowableButtonStyle where Self == TryAgainThrowableButtonStyle {
public static var tryAgain: TryAgainThrowableButtonStyle {
TryAgainThrowableButtonStyle()
}
}
然后,使用它
AsyncButton {
try doSomethingThatCanFail()
} label {
Text("Do something")
}
.throwableButtonStyle(.tryAgain)
像使用任何 SwiftUI button 一样使用它,但闭包将支持 try 和 await。
AsyncButton {
try await doSomethingThatTakeTime()
} label {
Text("Do something")
}
当进程正在进行时,另一次按钮按下不会导致发出新的 Task。但按钮仍然启用且可点击。你可以使用 disabledWhenLoading
修饰符在加载时禁用按钮。
AsyncButton {
...
}
.disabledWhenLoading()
你还可以使用 allowsHitTestingWhenLoading
修饰符在加载时禁用点击测试。
AsyncButton {
...
}
.allowsHitTestingWhenLoading(false)
使用 asyncButtonTaskStarted
或 asyncButtonTaskEnded
修饰符访问和响应底层任务。
AsyncButton {
...
}
.asyncButtonTaskStarted { task in
// Task started
}
.asyncButtonTaskEnded {
// Task ended or was cancelled
}
你可以使用 asyncButtonTaskChanged
修饰符来总结两者。
AsyncButton {
...
}
.asyncButtonTaskChanged { task in
if let task {
// Task started
} else {
// Task ended or was cancelled
}
}
在加载过程中,按钮将进行动画,默认情况下会将按钮的标签替换为 ProgressView
。内置了各种样式
![]() |
![]() |
.asyncButtonStyle(.overlay) |
.asyncButtonStyle(.pulse) |
![]() |
![]() |
.asyncButtonStyle(.leading) |
.asyncButtonStyle(.trailing) |
你可以通过将 .none
传递给 asyncButtonStyle
来禁用此行为。
AsyncButton {
try await doSomethingThatTakeTime()
} label {
Text("Do something")
}
.asyncButtonStyle(.none)
你还可以通过实现 AsyncButtonStyle
协议来构建你自己的自定义。
就像 ThrowableButtonStyle
一样,AsyncButtonStyle
允许你实现 makeLabel
、makeButton
或两者来改变加载过程中按钮的外观和行为。
你可能需要通过特定的用户操作来触发 button 后面的行为,例如按下虚拟键盘上的“发送”键。
因此,要在你的 button 上获得免费的动画进度和错误行为,你不能只自己启动 button 的动作。你需要 button 来启动它。
为此,你需要为你的 button 设置一个唯一的标识符
enum LoginViewButton: Hashable {
case login
}
struct ContentView: View {
var body: some View {
AsyncButton(id: LoginViewButton.login) {
try await login()
} label: {
Text("Login")
}
}
}
并从任何视图中访问 triggerButton 环境
struct ContentView: View {
@Environment(\.triggerButton)
private var triggerButton
...
func performLogin() {
triggerButton(LoginViewButton.login)
}
}
请注意
AsyncButton 支持进度报告
AsyncButton(progress: .discrete(totalUnitCount: files.count)) { progress in
for file in files {
try await file.doExpensiveComputation()
progress.completedUnitCount += 1
}
} label: {
Text("Process")
}
.buttonStyle(.borderedProminent)
.buttonBorderShape(.roundedRectangle)
AsyncButtonStyle
现在也支持确定性进度,响应 configuration.fractionCompleted: Double?
属性
AsyncButton(progress: .discrete(totalUnitCount: files.count)) { progress in
for file in files {
try await file.doExpensiveComputation()
progress.completedUnitCount += 1
}
} label: {
Text("Process")
}
.buttonStyle(.borderedProminent)
.buttonBorderShape(.roundedRectangle)
.asyncButtonStyle(.trailing)
![]() |
![]() |
.asyncButtonStyle(.overlay) |
.asyncButtonStyle(.overlay(style: .percent)) |
![]() |
![]() |
.asyncButtonStyle(.leading) |
.asyncButtonStyle(.trailing) |
你还可以通过实现 TaskProgress
协议来创建你自己的进度逻辑。这将允许你构建基于对数的进度,或者在移动到确定性状态之前(如 App Store 下载按钮)的第一个不确定步骤。
可用的 TaskProgress 实现包括
.indeterminate
的默认非确定性进度.discrete(totalUnitsCount: Int)
.estimated(for: Duration)
.progress
鼓励你通过提出 issue 或 pull request 来为这个存储库做出贡献,以修复错误、改进请求或提供支持。 贡献建议