如何在 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
)
相关推荐
今天也想MK代码2 天前
在Swift开发中简化应用程序发布与权限管理的解决方案——SparkleEasy
前端·javascript·chrome·macos·electron·swiftui
東三城7 天前
【ios】---SwiftUI开发从入门到放弃
ios·swiftui·swift·1024程序员节
今天也想MK代码10 天前
基于swiftui 实现3D loading 动画效果
ios·swiftui·swift
胖虎111 天前
SwiftUI(五)- ForEach循环创建视图&尺寸类&安全区域
ios·swiftui·swift·foreach·安全区域
zyosasa17 天前
SwiftUI 精通之路 11: 栅格布局
前端·swiftui·swift
小溪彼岸21 天前
【iOS小组件实战】灵动岛实时进度通知
swiftui·swift
提笔忘字的帝国25 天前
【ios】SwiftUI 混用 UIKit 的 Bug 解决:UITableView 无法滚动到底部
swiftui·bug·xcode
zyosasa25 天前
SwiftUI 精通之路 09:ForEach 视图构造器的基础应用
swiftui·swift
提笔忘字的帝国25 天前
【ios】在 SwiftUI 中实现可随时调用的加载框
ios·swiftui·xcode·swift
小溪彼岸25 天前
【iOS小组件】小组件App ID、Group ID、描述文件
swiftui·swift