使用 SwiftUI 为 macOS 创建类似于 App Store Connect 的选择器

2,899 阅读3分钟

前言

最近,我一直在为我的应用开发一个全新的界面,它可以让你查看 TestFlight 上所有可用的构建,并允许你将它们添加到测试群组中。

作为这项工作的一部分,我需要创建一个组件,允许用户从特定构建中添加和删除测试群组。我希望构建类似于 App Store Connect 中的选择器组件,使用户体验尽可能熟悉,并在本文中,将展示如何使用 SwiftUI 为 macOS 构建了这个组件。

创建选择器组件

让我们分析一下,我们有一组想要在 SwiftUI 列表中显示的构建。每个构建都包含一组属性,其中之一是 betaGroups,它是一个表示构建所属测试群组的结构体数组。

struct VersionBuild: Identifiable, Equatable {
    let number: String
    let date: Date
    let hasAppClip: Bool
    let iconURL: URL?
    let id: String
    let isProcessing: Bool
    var betaGroups: [BetaGroup]
}

struct BetaGroup: Identifiable, Equatable {
    let id: String
    let name: String
}

列表使用一个名为 TestFlightBuildCell 的简单组件来显示构建信息:

struct TestFlightBuildCell: View {
    let build: VersionBuild
    
    var body: some View {
        
        HStack(spacing: 12) {
            if let appIcon = build.iconURL {
                KFImage(appIcon)
                    .retry(maxCount: 3, interval: .seconds(5))
                    .cacheOriginalImage()
                    .resizable()
                    .appIconImage(size: .small)
                
            }
            
            VStack(alignment: .leading) {
                
                HStack(alignment: .center) {
                    
                    VStack(alignment:.leading){
                        
                        Text("Build \(build.number)")
                            .font(.HELheadline)
                            .foregroundStyle(.primary)
                            .place(.leading)
                        
                        
                        Text(build.date.fullText)
                            .font(.HELfootnote)
                            .foregroundStyle(.secondary)
                        
                        if build.hasAppClip {
                            Label("Includes App Clip", systemImage: "appclip")
                                .font(.HELfootnote)
                                .foregroundStyle(.secondary)
                        }
                    }
                    
                    Spacer()
                    
                    if build.isProcessing {
                        BuildTag(
                            title: "PROCESSING",
                            background: build.isProcessing ? Color.warning.opacity(0.3) : Color.gray.opacity(0.1)
                        )
                    }
                }
            }
        }
    }
}

在应用程序的上下文中,列表如下所示:

虽然上面的组件可以很好地传达所需的构建信息,但它在应用程序的这个部分仍然缺少一些关键功能。我们需要能够决定构建属于哪些测试群组,并根据需要添加或删除它们。

让我们看看 SwiftUI 中测试群组选择器组件的代码:

struct BetaGroupPicker: View {
    // 1
    @Binding var betaGroups: [BetaGroup]
    // 2
    let availableBetaGroups: [BetaGroup]
    // 3
    @State var hoveringGroup: BetaGroup?
    
    var body: some View {
        HStack(spacing: 4) {
            // 4
            ForEach(betaGroups) { betaGroup in
                Text(betaGroup.displayName)
                    .padding(4)
                    .background(Color.gray.opacity(0.2))
                    .bold()
                    .clipShape(Circle())
                    // 5
                    .onHover { hovering in
                        withAnimation {
                            hoveringGroup = hovering ? betaGroup : nil
                        }
                    }
                    // 6
                    .overlay(alignment: .topTrailing) {
                        if hoveringGroup == betaGroup {
                            Button {
                                withAnimation {
                                    betaGroups.removeAll(where: { $0 == betaGroup })
                                }
                            } label: {
                                Image(systemName: "minus.circle.fill")
                                    .foregroundStyle(Color.red)
                            }
                            .buttonStyle(.plain)
                            .offset(x: 2, y: -4)
                        }
                    }
            }
            
            // 7
            if !availableBetaGroups.isEmpty {
                Menu {
                    ForEach(availableBetaGroups) { betaGroup in
                        Button {
                            withAnimation(.snappy) {
                                betaGroups.append(betaGroup)
                            }
                        } label: {
                            Text(betaGroup.name)
                        }
                    }
                } label: {
                    Text(Image(systemName: "plus"))
                        .padding(4)
                        .background(Color.blue.opacity(0.2))
                        .bold()
                        .clipShape(Circle())
                }
                .menuStyle(.button)
                .buttonStyle(.plain)
            }
        }
    }
}

以上代码片段中涉及了很多内容,让我们来逐步分解:

  1. 绑定到构建中可用的测试群组数组。这是一个绑定,因为我们希望能够从内部视图修改它。
  2. 所有可用于添加到构建中的测试群组的数组。父视图负责提供这些信息,正如我们将在下一节中看到的那样。
  3. 一个状态属性,用于跟踪用户悬停的测试群组。此属性的值用于在用户悬停在上面时显示一个移除按钮。
  4. 遍历构建所属的测试群组,并使用 BetaGroup 结构体上的 displayName 属性将它们显示为圆形文本视图。
  5. 当用户悬停在特定测试群组组件上时,修改 hoveringGroup 状态属性。
  6. 使用 .overlay 修改器在用户悬停在测试群组组件上时显示一个移除按钮。该按钮从构建所属的测试群组列表中移除测试群组。
  7. 如果有任何可用的测试群组可以添加到构建中,则显示一个加号按钮,让用户选择要添加的测试群组。

以上代码片段使用了 BetaGroup 结构体上的一个名为 displayName 的属性来显示测试群组的名称,类似于在 App Store Connect 中的显示方式,显示名称中的前两个单词的首字母大写:

extension BetaGroup {
    var displayName: String {
        let output = name
            .components(separatedBy: .whitespaces)
            .filter { $0.lowercased() != "and" && $0.lowercased() != "&" }
            .prefix(2)
            .map { $0.first?.uppercased() ?? "" }
            .joined()
        
        return output.isEmpty ? "TF" : output
    }
}

使用选择器组件

现在我们有了 BetaGroupPicker 视图,我们可以开始在 TestFlightBuildCell 组件中使用它,让用户可以从特定构建中添加和删除测试群组:

struct TestFlightBuildCell: View {
    @Binding var build: VersionBuild
    let availableBetaGroups: [BetaGroup]

    init(
        build: Binding<VersionBuild>,
        availableBetaGroups: [BetaGroup]
    ) {
        self._build = build
        self.availableBetaGroups = availableBetaGroups.filter { !build.wrappedValue.betaGroups.contains($0) }
    }
    
    var body: some View {
        
        HStack(spacing: 12) {
            if let appIcon = build.iconURL {
                KFImage(appIcon)
                    .retry(maxCount: 3, interval: .seconds(5))
                    .cacheOriginalImage()
                    .resizable()
                    .appIconImage(size: .small)
                
            }
            
            VStack(alignment: .leading) {
                
                HStack(alignment: .center) {
                    
                    VStack(alignment:.leading){
                        
                        Text("Build \(build.number)")
                            .font(.HELheadline)
                            .foregroundStyle(.primary)
                            .place(.leading)
                        
                        
                        Text(build.date.fullText)
                            .font(.HELfootnote)
                            .foregroundStyle(.secondary)
                        
                        if build.hasAppClip {
                            Label("Includes App Clip", systemImage: "appclip")
                                .font(.HELfootnote)
                                .foregroundStyle(.secondary)
                        }
                    }
                    
                    Spacer()
                    
                    if build.isProcessing {
                        BuildTag(
                            title: "PROCESSING",
                            background: build.isProcessing ? Color.warning.opacity(0.3) : Color.gray.opacity(0.1)
                        )
                    }
                }
            }

            BetaGroupPicker(
                betaGroups: $build.betaGroups,
                availableBetaGroups: availableBetaGroups
            )
        }
    }
}

正如你所看到的,使用该组件非常简单。你只需要将父视图上的 build 属性修改为一个绑定,并将可用的测试群组传递给组件。

正如你所看到的,我们编写了一个自定义的初始化方法来过滤出任何已经属于构建的测试群组。

总结

文章介绍了如何使用 SwiftUI为macOS 创建类似于 App Store Connect 的选择器组件。作者在应用程序中添加了一个新的界面,允许用户查看 TestFlight 上所有可用的构建,并将它们添加到测试群组中。为了实现这一功能,作者创建了一个名为 BetaGroupPicker 的组件,该组件允许用户从特定构建中添加和删除测试群组。

BetaGroupPicker 中,用户可以看到构建所属的测试群组,并有选择地将它们添加到或从构建中移除。文章还提供了 TestFlightBuildCell 组件的示例,演示了如何在构建信息中集成 BetaGroupPicker 组件,以便用户可以直接在界面上操作测试群组。通过这一步骤,用户可以更方便地管理测试群组,并为应用程序的测试和部署提供更好的支持。