我正在尝试在 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()
}
正如评论中所建议的,您可以考虑使用
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
)
}
}