Home > Software design >  Customize the SwiftUI Form label layout for MacOS
Customize the SwiftUI Form label layout for MacOS

Time:02-04

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, 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, 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

CodePudding user response:

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

  •  Tags:  
  • Related