I have the following Cocoa form:
struct Canvas: PreviewProvider {
static var previews: some View {
VStack {
HStack(alignment: .firstTextBaseline) {
Text("Endpoint:")
TextField("https://localhost:8080/api", text: .constant(""))
}
Divider()
HStack(alignment: .firstTextBaseline) {
Text("Path:")
TextField("/todos", text: .constant(""))
}
Spacer()
}
.padding()
.previewLayout(.fixed(width: 280, height: 200))
}
}
This panel looks nice but I’d like to right-align “Endpoint:” and “Path:” labels:
So I apply a custom horizontal alignment:
struct Canvas: PreviewProvider {
static var previews: some View {
VStack(alignment: .label) {
HStack(alignment: .firstTextBaseline) {
Text("Endpoint:").alignmentGuide(.label) { $0[.trailing] }
TextField("https://localhost:8080/api", text: .constant(""))
}
Divider()
HStack(alignment: .firstTextBaseline) {
Text("Path:").alignmentGuide(.label) { $0[.trailing] }
TextField("/todos", text: .constant(""))
}
Spacer()
}
.padding()
.previewLayout(.fixed(width: 280, height: 200))
}
}
extension HorizontalAlignment {
private enum Label: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[.leading]
}
}
static let label: HorizontalAlignment = .init(Label.self)
}
Results are not what I need however:
There is no documentation, please help.
I don't believe alignment guides will work here in their current implementation. After playing with them a bit, it seems that they size their children based on the container's given size and then align each child based on the guide. This leads to the weird behavior you were seeing.
Below I show 3 different techniques that will allow you to get your desired results, in order of complexity. Each has its applications outside of this specific example.
The last (label3()
) will be the most reliable for longer forms.
struct ContentView: View {
@State var sizes: [String:CGSize] = [:]
var body: some View {
VStack {
HStack(alignment: .firstTextBaseline) {
self.label3("Endpoint:")
TextField("https://localhost:8080/api", text: .constant(""))
}
Divider()
HStack(alignment: .firstTextBaseline) {
self.label3("Path:")
TextField("/todos", text: .constant(""))
}
}
.padding()
.onPreferenceChange(SizePreferenceKey.self) { preferences in
self.sizes = preferences
}
}
func label1(_ text: String) -> some View {
Text(text) // Use a minimum size based on your best guess. Look around and you'll see that many macOS apps actually lay forms out like this because it's simple to implement.
.frame(minWidth: 100, alignment: .trailing)
}
func label2(_ text: String, sizer: String = "Endpoint:") -> some View {
ZStack(alignment: .trailing) { // Use dummy content for sizing based on the largest expected item. This can be great when laying out icons and you know ahead of time which will be the biggest.
Text(sizer).opacity(0.0)
Text(text)
}
}
func label3(_ text: String) -> some View {
Text(text) // Use preferences and save the size of each label
.background(
GeometryReader { proxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: [text : proxy.size])
}
)
.frame(minWidth: self.sizes.values.map { $0.width }.max() ?? 0.0, alignment: .trailing)
}
}
struct SizePreferenceKey: PreferenceKey {
typealias Value = [String:CGSize]
static var defaultValue: Value = [:]
static func reduce(value: inout Value, nextValue: () -> Value) {
let next = nextValue()
for (k, v) in next {
value[k] = v
}
}
}
Here's a screenshot of the results with label2
or label3
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With