为 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)     {}
}