I am trying to include a custom HStack
row in a SwiftUI Form
as follows:
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()
}
resulting in
However, I would like Label
to be vertically aligned with Toggle
and Menu
vertically aligned with the toggle.
Is there a standard way of choosing the alignment mode for the custom HStack
row?
CodePudding user response:
You can wrap your content inside a VStack and use its alignment
modifier to align all the content to the leading e.g:
VStack(alignment: .leading)
like this:
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()
}
However, this view has not been backported to earlier versions of macOS, so if you need to support earlier versions you'll need another approach.
Earlier versions of macOS
Building on the preference key code from @Nhat Nguyen Duc, the key is to use alignment guides rather than padding. Creating a custom view, and with a customised preference that only measures the width:
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)
.readSize { 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)
}
}
Note that in the custom view I've added 10 pixels to quickly emulate the spacing between a label and its form elements. There is probably a better way to make this work for accessibility sizes, etc., (e.g., the use of a @ScaledMetric
value).
Below has a line with macOS13's LabeledContent
, followed by LabeledHStack
:
CodePudding user response:
macOS 13
LabeledContent {
HStack {
// ...
}
} label: {
Text("Count")
}
- Read more about
LabeledContent
here
Previous version
Idea: Calculate the size of the label using GeometryReader, and offset the view by its width.
@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) { }
}
- Bonus: For a button that contains only label, you can use
Button(<#String#>) { <#Action#> }
instead of
Button(action: { <#Action#> }) { Text(<#String#>) }