1

I am running a macOS application that contains a Form with some elements on it. I love the default layout when using elements with labels (example: TextField and Picker) but am having trouble replicating that layout with a custom control. Specifically I want to have an HStack containing a Text/Label field (laid out under the other labels) and a button (even with the other fields above). Instead both elements are under the second part and not both.

I created a dummy project showing my issue. It's like the Form is a two column table and the labels are getting put in the first column and anything else goes in the second column. I did see this question, Swiftui Form Label alignment on macOS, but I was hoping with my below example there might be a better solution. I tried putting everything in a VStack but then the labels are left aligned and the text boxes start wherever. I also don't want to hardcode the width of the labels.

This is what I made as an example: enter image description here With the code:

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()
}
}

I basically want the third section in the form to look like the previous two. I added a black background so it's more apparent. I want the label: to be even under My Name: and Pick Something: while the button is even with the text field itself and the picker above.

Thanks to anyone that can help me come up with an elegant solution

Update 1: Took @ChrisR's comment and attempted to use the style. I found that the ButtonStyle doesn't have a width like the ToggleStyle example. I then found this question, Get width of a view using in SwiftUI, and used the commonSize in Paul B's answer to set the alignment. It's set the width of my stack properly but can't set the alignment properly.

Image: enter image description here

Code:

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

Update 2: I attempted ChrisP's answer "You want .readSize from your Button label, and get rid of the .frame" and ended up with both left aligned in the right column: enter image description here

If I don't set commonSize or use 0 instead it moves both elements to the first column: enter image description here

If I split up the elements by removing the label from the HStack I can get one on the first column and one on the second BUT then they're on two different lines.

enter image description here

Kyra
  • 5,129
  • 5
  • 35
  • 55
  • 1
    this will help: https://swiftui-lab.com/custom-styling/ --- chapter "Special Consideration #3: Form (macOS)" – ChrisR Feb 02 '22 at 22:19
  • That looks stupendous; however, when I work with ButtonStyle the `self.width` doesn't work as it has "no member width" so I'm not sure how I should try to replicate that with a button and text? – Kyra Feb 02 '22 at 23:51
  • 1
    You almost made it. Just .getSize at the wrong element, look at my answer :) – ChrisR Feb 03 '22 at 06:58
  • it does work ! put the .readSize on the BUTTON, not the HStack as shown below – ChrisR Feb 03 '22 at 15:19
  • Thank you so much. Can't believe I made that mistake lol – Kyra Feb 03 '22 at 15:26

2 Answers2

4

With macOS 13 (Ventura), you can use LabeledContent to create a label with any view easily.

LabeledContent("Label:") {
    Button("Button HERE") { ... }
}
samwize
  • 25,675
  • 15
  • 141
  • 186
2

You want .readSize from your Button label, and get rid of the .frame, then it works :)

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

enter image description here

ChrisR
  • 9,523
  • 1
  • 8
  • 26
  • Thanks for your help. Still having a little bit of an issue but understand if you don't want to help. Threw it on a new question as this was great. https://stackoverflow.com/questions/70977168/swiftui-macos-form-custom-layout – Kyra Feb 03 '22 at 19:28