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

前言

在我们的日常开发中,经常会碰到这样的需求:完成某个异步操作后,更新 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
)
相关推荐
OliverQiu4 小时前
开源一个 macOS 小工具:集中管理 ChatGPT 的网络启动配置
macos·chatgpt·swiftui·mac·proxy·codex·dmg
传奇开心果编程4 天前
【SwiftUI提高练中学】第9课 安全与隐私
swiftui·移动开发·swift·编程语言理论·开发范式·编程框架·声明式ui
传奇开心果编程5 天前
【声明式UI开发实用技术学与练】第8课 状态提升与下放
学习·flutter·react native·ui·swiftui·composer
东坡肘子5 天前
Swift Server,又多了一个 Google -- 肘子的 Swift 周报 #156
人工智能·swiftui·swift
传奇开心果编程5 天前
【现代声明式UI学与练】第4课 列表渲染与 key——如何高效渲染列表、key 的作用、列表重排时的状态保持
学习·flutter·react native·ui·swiftui·android jetpack
传奇开心果编程6 天前
【SwiftUI提高练中学】第8课 网络层架构:拦截器、重试、缓存与离线优先
学习·macos·ui·ios·swiftui
传奇开心果编程6 天前
【现代声明式UI学与练】第7课 组件通信——父传子、子传父、跨层级通信、状态提升与 Context
学习·flutter·react native·ui·swiftui·android jetpack
tink6 天前
告别 Xcode IDE:用 VS Code + SweetPad + XcodeGen 开发 iOS 应用的完整指南
ios·swiftui·xcode
传奇开心果编程6 天前
【现代声明式UI学与练】第9课 性能优化——渲染优化、列表优化、内存优化、启动优化
学习·flutter·react native·ui·性能优化·swiftui·android jetpack
传奇开心果编程9 天前
【现代声明式UI学与练】第1课 从命令式UI到声明式UI
学习·flutter·ui·swiftui·react·android jetpack