SwiftUI 中的组合框

问题描述 投票:0回答:1

我正在尝试在 SwiftUI 中为 macOS 应用程序构建一个组合框,但遇到了困难。我在这里查看了其他问题,但没有真正找到任何答案。 主要问题是我希望下拉列表出现在下面的视图顶部,以及顶部和父视图上,类似于 AppKit 中的 ComboBox 的作用。 在下面的代码中,您会看到下拉列表没有甚至没有出现,可能是因为空间不足。我尝试使用 z 索引和叠加层,但没有结果。

import Foundation
import SwiftUI

struct ComboBox: View {
    let label: String

    @Binding var text: String
    @State private var isExpanded = false

    var items: [String]

    var body: some View {
        VStack(spacing: 0) {
            TextField(label, text: $text)
                .overlay(alignment: .trailing) {
                    Button {
                        print("Clicked chyron")

                        withAnimation {
                            isExpanded.toggle()
                        }

                    } label: {
                        if isExpanded {
                            Image(systemName: "chevron.up")
                        } else {
                            Image(systemName: "chevron.down")
                        }
                    }.buttonStyle(.borderedProminent)
                }
                .onChange(of: text) { _, newValue in
                    if newValue.isEmpty {
                        isExpanded = false
                    } else {
                        isExpanded = true
                    }
                }

            if isExpanded {
                ScrollView {
                    ForEach(items, id: \.self) { item in
                        Text(item)
                    }
                }
            }
        }
        .padding()
    }
}

#Preview {
    @Previewable @State var text = ""
    let items = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"]
    Form {
        ComboBox(label: "Label", text: $text, items: items)
        HStack {
            Spacer()

            Button("Cancel") {
                print("Cancel")
            }

            Button("Submit") {
                print("Submit")
            }.buttonStyle(.borderedProminent)
        }
    }.padding()
}
swift macos swiftui
1个回答
0
投票

正如评论中所建议的,您可以考虑使用

sheet
fullScreenCover
。但是,在 macOS 中,您在使用工作表时无法过多控制位置,并且需要启用 Mac Catalyst 才能使用全屏覆盖。

或者,您可以考虑将菜单与组合字段分开,并将菜单显示为

ZStack
中的顶层。可以使用
.matchedGeometryEffect
来完成定位。这本质上与 iOS SwiftUI Need to Display Popover Without "Arrow" 的答案中使用的技术相同,用于显示自定义弹出框(这是我的答案)。

一些注意事项:

  • 布尔标志也需要作为绑定传递到

    ComboBox

  • .fixedSize()
    应用于
    ScrollView
    中的
    ComboMenu
    ,将其大小限制为所需的最小值。

  • 可以将一个包罗万象的点击手势附加到

    ZStack
    ,以清除弹出窗口(如果显示)。

  • 我发现弹出动画很难顺利工作,简单的不透明过渡可能更安全。

struct ContentView: View {
    let items = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"]
    @State private var text = ""
    @State private var isComboExpanded = false
    @Namespace private var ns

    var body: some View {
        ZStack {
            Form {
                ComboBox(
                    label: "Label",
                    ns: ns,
                    text: $text,
                    isExpanded: $isComboExpanded
                )
                HStack {
                    Spacer()

                    Button("Cancel") {
                        text = ""
                        print("Cancel")
                    }

                    Button("Submit") {
                        print("Submit")
                    }
                    .buttonStyle(.borderedProminent)
                }
            }
            .padding()
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .contentShape(Rectangle())
        .onTapGesture {
            isComboExpanded = false
        }
        .overlay {
            if isComboExpanded {
                ComboMenu(label: "Label", ns: ns, items: items, text: $text)
            }
        }
        .animation(.spring, value: isComboExpanded)
    }
}

struct ComboBox: View {
    let label: String
    let ns: Namespace.ID

    @Binding var text: String
    @Binding var isExpanded: Bool

    var body: some View {
        TextField(label, text: $text)
            .padding(.trailing, 40)
            .overlay(alignment: .trailing) {
                Button {
                    print("Clicked chevron")
                    isExpanded.toggle()
                } label: {
                    Image(systemName: "chevron.down")
                        .rotation3DEffect(
                            .degrees(isExpanded ? 180 : 0),
                            axis: (x: 1, y: 0, z: 0),
                            perspective: 0.1
                        )
                }
                .buttonStyle(.borderedProminent)
                .matchedGeometryEffect(
                    id: label,
                    in: ns,
                    anchor: .bottomTrailing,
                    isSource: true
                )
            }
            .onChange(of: text) { _, newValue in
                isExpanded = newValue.isEmpty
            }
            .padding(.vertical, 10)
    }
}

struct ComboMenu: View {
    let label: String
    let ns: Namespace.ID
    let items: [String]
    @Binding var text: String

    var body: some View {
        ScrollView {
            VStack(alignment: .leading) {
                ForEach(items, id: \.self) { item in
                    Text(item)
                        .padding(.vertical, 4)
                        .frame(maxWidth: .infinity)
                        .contentShape(Rectangle())
                        .onTapGesture {
                            text = item
                        }
                }
            }
            .padding()
        }
        .fixedSize()
        .background {
            RoundedRectangle(cornerRadius: 6)
                .fill(.background)
                .shadow(radius: 6)
        }
        .padding(.top, 6)
        .matchedGeometryEffect(
            id: label,
            in: ns,
            properties: .position,
            anchor: .topTrailing,
            isSource: false
        )
    }
}

Animation

© www.soinside.com 2019 - 2024. All rights reserved.