ButtonKit

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 中,你可以实现 makeLabelmakeButton 或两者来改变 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)

使用 asyncButtonTaskStartedasyncButtonTaskEnded 修饰符访问和响应底层任务。

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 允许你实现 makeLabelmakeButton 或两者来改变加载过程中按钮的外观和行为。

外部触发

你可能需要通过特定的用户操作来触发 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 实现包括

贡献

鼓励你通过提出 issue 或 pull request 来为这个存储库做出贡献,以修复错误、改进请求或提供支持。 贡献建议