如何在 SwiftUI 中构建触发异步任务的按钮

前言

在我们的日常开发中,经常会碰到这样的需求:完成某个异步操作后,更新 UI 视图。比如下面的代码,在 SwiftUIPhotoView 中,每当用户点击视图中的按钮时,都会触发一个异步的任务 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
)
相关推荐
struggle20252 天前
Ollmao (OH-luh-毛程序包及源码) 是一款原生 SwiftUI 应用程序,它与 Ollama 集成,可在 Mac 上本地运行强大的 AI 模型
ios·swiftui·swift
货拉拉技术1 个月前
货拉拉用户端SwiftUI踩坑之旅
ios·swiftui·swift
ZacJi1 个月前
巧用 allowsHitTesting 自定义 SignInWithAppleButton
ios·swiftui·swift
刘争Stanley1 个月前
SwiftUI 是如何改变 iOS 开发游戏规则的?
ios·swiftui·swift
1024小神1 个月前
在swiftui中使用Alamofire发送请求获取github仓库里的txt文件内容并解析
ios·github·swiftui
大熊猫侯佩1 个月前
SwiftUI 撸码常见错误 2 例漫谈
swiftui·xcode·tag·tabview·preview·coredata·fetchrequest
东坡肘子2 个月前
肘子的 Swift 周报 #063|异种肾脏移植取得突破
swiftui·swift·apple
恋猫de小郭2 个月前
什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap
flutter·ios·swiftui
靴子学长2 个月前
iOS + watchOS Tourism App(含源码可简单复现)
mysql·ios·swiftui
hxx2212 个月前
iOS swift开发系列--如何给swiftui内容视图添加背景图片显示
ios·swiftui·swift