前言
在我们的日常开发中,经常会碰到这样的需求:完成某个异步操作后,更新 UI 视图。比如下面的代码,在 SwiftUI
的 PhotoView
中,每当用户点击视图中的按钮时,都会触发一个异步的任务 onLike() :
css
struct PhotoView: View {
var photo: Photo
var onLike: () async -> Void
var body: some View {
VStack {
Image(uiImage: photo.image)
Text(photo.description)
Button(action: {
Task {
await onLike()
}
}, label: {
Image(systemName: "hand.thumbsup.fill")
})
.disabled(photo.isLiked)
}
}
}
上述的代码可以完成基本的需求,但它存在一个问题。如果用户多次快速点击按钮且异步任务没有完成的情况下,代码会多次调用 onLike() 函数,因为按钮只有在异步任务完成才会将状态置为不可用。
封装 AsyncButton 来解决上述问题
我们可以自己封装一个 AsyncButton 来解决上述问题。首先,引入一个布尔类型的变量用来表示异步任务是否已经在执行:isPerformingTask。如果异步任务已经在执行过程中,我们就将按钮的状态置为不可用,下面是代码示例:
swift
struct AsyncButton<Label: View>: View {
var action: () async -> Void
@ViewBuilder var label: () -> Label
@State private var isPerformingTask = false
var body: some View {
Button(
action: {
isPerformingTask = true
Task {
await action()
isPerformingTask = false
}
},
label: {
ZStack {
label().opacity(isPerformingTask ? 0 : 1)
if isPerformingTask {
ProgressView()
}
}
}
)
.disabled(isPerformingTask)
}
}
因为它是继承自 View 的,所以我们可以像使用 Button 那样去使用它。并且封装的代码中,在 action 中我们已经使用了 Task 对异步任务进行包装,所以外层使用的使用直接编写异步任务即可:
css
AsyncButton(action: {
await onLike()
}, label: {
Image(systemName: "hand.thumbsup.fill")
})
目前为止,封装的 AsyncButton 可以完成基本的需求。但我们还可以进一步改进它,以便支持更多的功能。比如可以支持是否显示 ProgressView,或者是否需要在异步任务执行的时候禁用按钮。
支持多样性
为了支持上述功能,我们可以定义一个枚举:
swift
extension AsyncButton {
enum ActionOption: CaseIterable {
case disableButton
case showProgressView
}
}
因为我们要遍历枚举的所有 case,所以需要枚举遵守 CaseIterable 协议,以便系统自动生成 allCases 来供我们使用。
下面是改良版的 AsyncButton 代码:
typescript
struct AsyncButton<Label: View>: View {
var action: () async -> Void
var actionOptions = Set(ActionOption.allCases)
@ViewBuilder var label: () -> Label
@State private var isDisabled = false
@State private var showProgressView = false
var body: some View {
Button(
action: {
if actionOptions.contains(.disableButton) {
isDisabled = true
}
if actionOptions.contains(.showProgressView) {
showProgressView = true
}
Task {
await action()
isDisabled = false
showProgressView = false
}
},
label: {
ZStack {
label().opacity(showProgressView ? 0 : 1)
if showProgressView {
ProgressView()
}
}
}
)
.disabled(isDisabled)
}
}
现在,封装的 AsyncButton
已经可以灵活的去适应不同的功能,但它仍有一些优化的空间。我们可以提供便利的构造器来使它的构建更加的方便。
便利的构造器
我们可以提供两个构造器,一个用于显示纯文本按钮的构建;一个用来使用系统图片资源按钮的构建。
- 纯文本构造器:
less
extension AsyncButton where Label == Text {
init(_ label: String,
actionOptions: Set<ActionOption> = Set(ActionOption.allCases),
action: @escaping () async -> Void) {
self.init(action: action) {
Text(label)
}
}
}
- 系统图片构造器:
less
extension AsyncButton where Label == Image {
init(systemImageName: String,
actionOptions: Set<ActionOption> = Set(ActionOption.allCases),
action: @escaping () async -> Void) {
self.init(action: action) {
Image(systemName: systemImageName)
}
}
}
下面是系统图片构造器的使用示例:
less
AsyncButton(
systemImageName: "hand.thumbsup.fill",
action: onLike
)