为 MacOS 自定义 SwiftUI 表单标签布局
Customize the SwiftUI Form label layout for MacOS
我是 运行 一个 macOS 应用程序,它包含一个带有一些元素的表单。我喜欢使用带有 labels 的元素时的默认布局(例如:TextField 和 Picker),但在使用自定义控件复制该布局时遇到了问题。具体来说,我想要一个 HStack 包含一个 Text/Label 字段(位于其他标签下)和一个按钮(甚至包含上面的其他字段)。相反,两个元素都在第二部分下,而不是两者都在。
我创建了一个虚拟项目来展示我的问题。就像表单是两列 table 并且标签被放在第一列,其他任何东西都在第二列。我确实看到了这个问题,,但我希望下面的示例可能有更好的解决方案。我尝试将所有内容都放在 VStack 中,但标签左对齐,文本框从任何地方开始。我也不想硬编码标签的宽度。
这是我做的例子:
使用代码:
import SwiftUI
struct ContentView: View {
@State var myName:String = "Kyra"
@State var selectedPickerItem: String?
var pickerItems = ["item 1",
"item 2",
"item 3",
"item 4",
"item 5",
"item 6"]
var body: some View {
Form {
TextField("My Name:", text: $myName, prompt: Text("What's your name?"))
.foregroundColor(.white)
.background(.black)
Picker(selection: $selectedPickerItem, label: Text("Pick Something:")) {
Text("No Chosen Item").tag(nil as String?)
ForEach(pickerItems, id: \.self) { item in
Text(item).tag(item as String?)
}
}
.foregroundColor(.white)
.background(.black)
HStack {
Label("Label:", image: "default")
.labelStyle(.titleOnly)
.foregroundColor(.white)
Button(action: {
print("Do something")
}) {
HStack {
Text("Button HERE")
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
.font(.caption)
}
}
}
.frame(minWidth: 0, maxWidth: .infinity)
.background(.black)
}
.padding()
}
}
我基本上希望表格中的第三部分看起来像前两部分。我添加了黑色背景,所以它更明显。我希望 label: 甚至在 My Name: 和 Pick Something: 下,而按钮是即使有文本字段本身和上面的选择器。
感谢任何能帮我想出一个优雅的解决方案的人
更新 1:接受了@ChrisR 的评论并尝试使用该样式。我发现 ButtonStyle
没有像 ToggleStyle
示例那样的 width
。然后我发现了这个问题,, and used the commonSize
in Paul B's answer to set the alignment。它正确设置了我的堆栈宽度,但无法正确设置对齐方式。
图片:
代码:
import SwiftUI
struct ContentView: View {
@State var myName:String = "Kyra"
@State var selectedPickerItem: String?
var pickerItems = ["item 1",
"item 2",
"item 3",
"item 4",
"item 5",
"item 6"]
@State private var commonSize = CGSize()
var body: some View {
Form {
TextField("My Name:", text: $myName, prompt: Text("What's your name?"))
.foregroundColor(.white)
.background(.black)
.readSize { textSize in
commonSize = textSize
}
Picker(selection: $selectedPickerItem, label: Text("Pick Something:")) {
Text("No Chosen Item").tag(nil as String?)
ForEach(pickerItems, id: \.self) { item in
Text(item).tag(item as String?)
}
}
.foregroundColor(.white)
.background(.black)
HStack {
Label("Label:", image: "default")
.labelStyle(.titleOnly)
.foregroundColor(.white)
Button(action: {
print("Do something")
}) {
HStack {
Text("Button HERE")
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
.font(.caption)
}
}
}
.frame(width: commonSize.width, height: commonSize.height)
.alignmentGuide(.leading, computeValue: { d in (d.width - commonSize.width) })
.background(.black)
}
.padding()
}
}
func readSize(onChange: @escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
}
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}
更新 2:我尝试了 ChrisP 的回答“你想从你的 Button 标签中获取 .readSize,并去掉 .frame”,结果在右列中左对齐:
如果我不设置 commonSize 或使用 0,它会将两个元素移到第一列:
如果我通过从 HStack 中删除标签来拆分元素,我可以在第一列上得到一个,在第二列上得到一个,但是它们在不同的两行上。
您想要按钮标签中的 .readSize,
并摆脱 .frame,然后它就可以工作了:)
struct ContentView: View {
@State var myName:String = "Kyra"
@State var selectedPickerItem: String?
var pickerItems = ["item 1",
"item 2",
"item 3",
"item 4",
"item 5",
"item 6"]
@State private var commonSize = CGSize()
var body: some View {
Form {
TextField("My Name:", text: $myName, prompt: Text("What's your name?"))
.foregroundColor(.white)
.background(.black)
Picker(selection: $selectedPickerItem, label: Text("Pick Something:")) {
Text("No Chosen Item").tag(nil as String?)
ForEach(pickerItems, id: \.self) { item in
Text(item).tag(item as String?)
}
}
.foregroundColor(.white)
.background(.black)
HStack {
Label("Label:", image: "default")
.labelStyle(.titleOnly)
.foregroundColor(.white)
Button(action: {
print("Do something")
}) {
HStack {
Text("Button HERE")
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
.font(.caption)
}
}
.readSize { textSize in
commonSize = textSize
}
}
// .frame(width: commonSize.width, height: commonSize.height)
.alignmentGuide(.leading, computeValue: { d in (d.width - commonSize.width) })
.background(.black)
}
.padding()
}
}
extension View {
func readSize(onChange: @escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
}
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}
我是 运行 一个 macOS 应用程序,它包含一个带有一些元素的表单。我喜欢使用带有 labels 的元素时的默认布局(例如:TextField 和 Picker),但在使用自定义控件复制该布局时遇到了问题。具体来说,我想要一个 HStack 包含一个 Text/Label 字段(位于其他标签下)和一个按钮(甚至包含上面的其他字段)。相反,两个元素都在第二部分下,而不是两者都在。
我创建了一个虚拟项目来展示我的问题。就像表单是两列 table 并且标签被放在第一列,其他任何东西都在第二列。我确实看到了这个问题,
这是我做的例子:
import SwiftUI
struct ContentView: View {
@State var myName:String = "Kyra"
@State var selectedPickerItem: String?
var pickerItems = ["item 1",
"item 2",
"item 3",
"item 4",
"item 5",
"item 6"]
var body: some View {
Form {
TextField("My Name:", text: $myName, prompt: Text("What's your name?"))
.foregroundColor(.white)
.background(.black)
Picker(selection: $selectedPickerItem, label: Text("Pick Something:")) {
Text("No Chosen Item").tag(nil as String?)
ForEach(pickerItems, id: \.self) { item in
Text(item).tag(item as String?)
}
}
.foregroundColor(.white)
.background(.black)
HStack {
Label("Label:", image: "default")
.labelStyle(.titleOnly)
.foregroundColor(.white)
Button(action: {
print("Do something")
}) {
HStack {
Text("Button HERE")
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
.font(.caption)
}
}
}
.frame(minWidth: 0, maxWidth: .infinity)
.background(.black)
}
.padding()
}
}
我基本上希望表格中的第三部分看起来像前两部分。我添加了黑色背景,所以它更明显。我希望 label: 甚至在 My Name: 和 Pick Something: 下,而按钮是即使有文本字段本身和上面的选择器。
感谢任何能帮我想出一个优雅的解决方案的人
更新 1:接受了@ChrisR 的评论并尝试使用该样式。我发现 ButtonStyle
没有像 ToggleStyle
示例那样的 width
。然后我发现了这个问题,commonSize
in Paul B's answer to set the alignment。它正确设置了我的堆栈宽度,但无法正确设置对齐方式。
图片:
代码:
import SwiftUI
struct ContentView: View {
@State var myName:String = "Kyra"
@State var selectedPickerItem: String?
var pickerItems = ["item 1",
"item 2",
"item 3",
"item 4",
"item 5",
"item 6"]
@State private var commonSize = CGSize()
var body: some View {
Form {
TextField("My Name:", text: $myName, prompt: Text("What's your name?"))
.foregroundColor(.white)
.background(.black)
.readSize { textSize in
commonSize = textSize
}
Picker(selection: $selectedPickerItem, label: Text("Pick Something:")) {
Text("No Chosen Item").tag(nil as String?)
ForEach(pickerItems, id: \.self) { item in
Text(item).tag(item as String?)
}
}
.foregroundColor(.white)
.background(.black)
HStack {
Label("Label:", image: "default")
.labelStyle(.titleOnly)
.foregroundColor(.white)
Button(action: {
print("Do something")
}) {
HStack {
Text("Button HERE")
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
.font(.caption)
}
}
}
.frame(width: commonSize.width, height: commonSize.height)
.alignmentGuide(.leading, computeValue: { d in (d.width - commonSize.width) })
.background(.black)
}
.padding()
}
}
func readSize(onChange: @escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
}
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}
更新 2:我尝试了 ChrisP 的回答“你想从你的 Button 标签中获取 .readSize,并去掉 .frame”,结果在右列中左对齐:
如果我不设置 commonSize 或使用 0,它会将两个元素移到第一列:
如果我通过从 HStack 中删除标签来拆分元素,我可以在第一列上得到一个,在第二列上得到一个,但是它们在不同的两行上。
您想要按钮标签中的 .readSize, 并摆脱 .frame,然后它就可以工作了:)
struct ContentView: View {
@State var myName:String = "Kyra"
@State var selectedPickerItem: String?
var pickerItems = ["item 1",
"item 2",
"item 3",
"item 4",
"item 5",
"item 6"]
@State private var commonSize = CGSize()
var body: some View {
Form {
TextField("My Name:", text: $myName, prompt: Text("What's your name?"))
.foregroundColor(.white)
.background(.black)
Picker(selection: $selectedPickerItem, label: Text("Pick Something:")) {
Text("No Chosen Item").tag(nil as String?)
ForEach(pickerItems, id: \.self) { item in
Text(item).tag(item as String?)
}
}
.foregroundColor(.white)
.background(.black)
HStack {
Label("Label:", image: "default")
.labelStyle(.titleOnly)
.foregroundColor(.white)
Button(action: {
print("Do something")
}) {
HStack {
Text("Button HERE")
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
.font(.caption)
}
}
.readSize { textSize in
commonSize = textSize
}
}
// .frame(width: commonSize.width, height: commonSize.height)
.alignmentGuide(.leading, computeValue: { d in (d.width - commonSize.width) })
.background(.black)
}
.padding()
}
}
extension View {
func readSize(onChange: @escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
}
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}