Search code examples
macoslayoutswiftui

SwiftUI MacOS Form Custom Layout


I had an earlier question that I got awesome help to but there's something not quite right with the layout still. Figured I'd create a new question rather than continue that one.

I'm making a custom picker using a button and want it laid out like the other pickers, textfields, etc on my form. In the previous question I learned to use the alignmentGuide. However that isn't working as the field isn't quite lined up with the others AND I can only make the window a bit smaller and then it locks into place. I want it to line up with above and be dynamic to window size adjustments when running.

Here's what it looks like right now enter image description here

This is as small as I can make it: enter image description here

And here's the current 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 var showingPopover:Bool = false
@State var selectedItems = [String]()
@State var allItems:[String] = ["more items",
                     "another item",
                     "and more",
                     "still more",
                     "yet still more",
                     "and the final item"]
@State private var commonSize = CGSize()
@State private var commonTextSize = CGSize()

var body: some View {
    Form {
        
        TextField("My Name:", text: $myName, prompt: Text("What's your name?"))
            .foregroundColor(.white)
            .background(Color(red: 0.4192, green: 0.2358, blue: 0.3450))
        
        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(Color(red: 0.2645, green: 0.3347, blue: 0.4008))
        
        
        HStack() {
            Text("Select Items:")
                .foregroundColor(.white)
                .readSize { textSize in
                    commonTextSize = textSize
                }
            Button(action: {
                showingPopover.toggle()
            }) {
                HStack {
                    Spacer()
                    Image(systemName: "\($selectedItems.count).circle")
                        .foregroundColor(.secondary)
                        .font(.title2)
                    Image(systemName: "chevron.right")
                        .foregroundColor(.secondary)
                        .font(.caption)
                }
            }
            .readSize { textSize in
                commonSize = textSize
            }
            .popover(isPresented: $showingPopover) {
                EmptyView()
            }
        }
        .alignmentGuide(.leading, computeValue: { d in (d.width - commonSize.width) })
        .background(Color(red: 0.4192, green: 0.2358, blue: 0.3450))
    }
    .padding()
}
}

// FROM https://stackoverflow.com/questions/57577462/get-width-of-a-view-using-in-swiftui
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) {}
}

Solution

  • I've since upgraded to Ventura 13.0 beta on my Mac and upgraded Xcode too. With the upgrade I still have this issue; however, the SwiftUI upgrade includes the Grid control which fixes this layout bug. I figured with this current upgrade Grid I'd answer this question and mark as solved.

    With the upgrade the controls still didn't line up, although I was able to make it as small as I wanted.

    Form controls are still not lined up: Form controls are still not lined up.

    But I can make the form as small as I want however the controls' start and end locations jump around as I do. But I can make the form as small as I want however the controls' start and end locations jump around as I do.

    With the update I was able to use the new Grid control (documentation link) to layout my controls making them look so much better.

    Screenshot of the apple documentation: https://developer.apple.com/documentation/swiftui/grid

    Here it is using Grid. I can drag the edges of the window to make it as small or large as I want without any weirdness. The new layout using Grid. Can make as small and large as I want.

    To do this I replaced my VStack with Grid and enclosed each control section with GridRow. My two bottom controls were too skinny so I combined the grid cells together so they'd take up the whole space by using the modifier .gridCellColumns(3). Code is:

        Grid {
            GridRow {
                TextField("My Name:", text: $myName, prompt: Text("What's your name?"))
                    .foregroundColor(.white)
                    .background(Color(red: 0.4192, green: 0.2358, blue: 0.3450))
            }
            .gridCellColumns(3)
            
            GridRow {
                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(Color(red: 0.2645, green: 0.3347, blue: 0.4008))
            }
            .gridCellColumns(3)
            
            GridRow {
                HStack() {
                    // Rather than a picker we're using Text for the label and a button for the picker itself
                    Text("Select Items:")
                        .foregroundColor(.white)
                    Button(action: {
                        // The only job of the button is to toggle the showing popover boolean so it pops up and we can select our items
                        showingPopover.toggle()
                    }) {
                        HStack {
                            Spacer()
                            Image(systemName: "\($selectedItems.count).circle")
                                .font(.title2)
                            Image(systemName: "chevron.right")
                                .font(.caption)
                        }
                    }
                    .popover(isPresented: $showingPopover) {
                        MultiSelectPickerView(allItems: allItems, selectedItems: $selectedItems)
                        // If you have issues with it being too skinny you can hardcode the width
                            .frame(width: 300)
                    }
                }
                .background(Color(red: 0.4192, green: 0.2358, blue: 0.3450))
            }
            .gridCellColumns(3)
        }
        .padding()
        
        
        // Made a quick text section so we can see what we selected
        Section("My selected items are:", content: {
            Text(selectedItems.joined(separator: "\n"))
        })
    

    Hope this helps you out if you come across a similar issue.