SwiftUI 表单对齐 macOS

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

我正在尝试在 SwiftUI

HStack
中包含自定义
Form
行,如下所示:

var body: some View {
    Form {
        TextField("Text", text: .constant("test"))
        Toggle("Toggle", isOn: .constant(true))
            .toggleStyle(SwitchToggleStyle())
        HStack {
            Text("Label")
            MenuButton("Menu") {
                Button(action: {
                    print("Clicked Pizza")
                }) { Text("Pizza") }
                Button(action: {
                    print("Clicked Pasta")
                }) { Text("Pasta") }
            }
            TextField("Topping", text: .constant("Cheese"))
                .labelsHidden()
        }
        
    }
    .padding()
}

导致

但是,我希望

Label
Toggle
垂直对齐,并且
Menu
与切换按钮垂直对齐。

是否有为自定义

HStack
行选择对齐模式的标准方法?

swift swiftui
3个回答
5
投票

仅限 macOS 13

SwiftUI 中新的

LabeledContent
视图将为您提供帮助:

LabeledContent("Label") {
  MenuButton("Menu") {
    Button(action: {
      print("Clicked Pizza")
    }) { Text("Pizza") }
    Button(action: {
      print("Clicked Pasta")
    }) { Text("Pasta") }
  }
  TextField("Topping", text: .constant("Cheese"))
    .labelsHidden()
  }
}

但是,此视图尚未向后移植到早期版本的 macOS,因此如果您需要支持早期版本,则需要另一种方法。


早期版本的 macOS

基于@Nhat Nguyen Duc 的首选项键代码,关键是使用对齐指南而不是填充。创建自定义视图,并使用仅测量宽度的自定义首选项:

struct LabeledHStack<Content: View>: View {
    var label: String
    var content: () -> Content
    @State var labelWidth: CGFloat = 0

    init(_ label: String, @ViewBuilder content: @escaping () -> Content) {
        self.label = label
        self.content = content
    }

    var body: some View {
        HStack {
            Text(label)
                .readWidth { self.labelWidth = $0 }
            content()
        }
        .alignmentGuide(.leading) { _ in labelWidth + 10 } // see note
    }
}

struct WidthPreferenceKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { }
}

extension View {
    func readWidth(onChange: @escaping (CGFloat) -> Void) -> some View {
        background(
            GeometryReader { geometryProxy in
                Color.clear
                    .preference(key: WidthPreferenceKey.self, value: geometryProxy.size.width)
            }
        )
        .onPreferenceChange(WidthPreferenceKey.self, perform: onChange)
    }
}

请注意,在自定义视图中,我添加了 10 个像素来快速模拟标签与其表单元素之间的间距。可能有更好的方法来使其适用于可访问性大小等(例如,使用

@ScaledMetric
值)。您可能还希望将其应用为填充,而不是在对齐指南计算中。

下面有一行 macOS13 的

LabeledContent
,后面是
LabeledHStack


2
投票

macOS 13

LabeledContent {
    HStack { 
        // ...
    }
} label: {
    Text("Count")
}
  • 阅读更多关于
    LabeledContent
    这里

以前的版本

想法:使用GeometryReader计算标签的大小,并按其宽度偏移视图。

@State private var textSize = CGSize.zero

var body: some View {
    Form {
        TextField("Text", text: .constant("test"))
            .padding(.leading, -textSize.width)
        Toggle("Toggle", isOn: .constant(true))
            .toggleStyle(SwitchToggleStyle())
            .padding(.leading, -textSize.width)
        HStack {
            Text("Label")
                .readSize { textSize in
                    self.textSize = textSize
                }
            MenuButton("Menu") {
                Button("Pizza") {
                    print("Clicked Pizza")
                }
                Button("Pasta") {
                    print("Clicked Pasta")
                }
            }
            TextField("Topping", text: .constant("Cheese"))
                .labelsHidden()
        }
        .padding(.leading, -textSize.width - 10)
        .frame(maxWidth: .infinity)
    }
    .padding(.leading, textSize.width + 10)
    .padding()
}
  • extension View
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)
    }
}
  • struct SizePreferenceKey
struct SizePreferenceKey: PreferenceKey {
    static var defaultValue: CGSize = .zero
    static func reduce(value: inout CGSize, nextValue: () -> CGSize) { }
}
  • 奖励:对于仅包含标签的按钮,您可以使用
Button(<#String#>) { <#Action#> }

而不是

Button(action: { <#Action#> }) { Text(<#String#>) }

1
投票

您可以将内容包装在 VStack 中,并使用其

alignment
修饰符将所有内容与前导对齐,例如:

VStack(对齐:.leading)

像这样:

var body: some View {
    Form {
        VStack (alignment: .leading){
            TextField("Text", text: .constant("test"))
            Toggle("Toggle", isOn: .constant(true))
                .toggleStyle(SwitchToggleStyle())
            HStack {
                Text("Label")
                MenuButton("Menu") {
                    Button(action: {
                        print("Clicked Pizza")
                    }) { Text("Pizza") }
                    Button(action: {
                        print("Clicked Pasta")
                    }) { Text("Pasta") }
                }
                TextField("Topping", text: .constant("Cheese"))
                    .labelsHidden()
            }
            .frame(width: .infinity, height: .infinity)
        }
        
    }
    .padding()
}

请参阅此处的工作示例:
See working example here

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