How to align baselines across multiple VStacks? - macos

I'm working on an "Inspector" style view in SwiftUI for macOS. I'm having trouble figuring out a solution that doesn't involve hard coded dimensions.
You can see in the example that when I use fixed width labels, I have put each row in an HStack and everything lines up.
Alternatively, using VStacks for the columns, the baselines don't line up of course. How can I get the content of the adjacent VStacks to align by baseline to look more like the fixed width label example? Or is there another way to achieve this without hardcoded values?
struct ContentView: View {
#State var labelWidth: CGFloat = 100
var body: some View {
VStack {
GroupBox.init(label: Text("Fixed Width Labels:"), content: {
fixedWithHStacks
})
GroupBox.init(label: Text("Would prefer this:"), content: {
vStackColumns
})
}
}
/// This one works but I don't like the fixed label size
var fixedWithHStacks: some View {
VStack {
HStack {
Text("Label")
.frame(width: labelWidth, alignment: .trailing)
TextField("Label", text: .constant("Value 1"))
}
HStack {
Text("Longer Label 2")
.frame(width: labelWidth, alignment: .trailing)
TextField("Label 2", text: .constant("Value 2"))
}
}
}
/// This would be ideal but how to I get the "rows" to align by baseline?
var vStackColumns: some View {
HStack {
VStack(alignment: .trailing) {
Text("Label")
Text("Longer Label 2")
}
VStack {
TextField("Label", text: .constant("Value 1"))
TextField("Label 2", text: .constant("Value 2"))
}
}
}
}

Related

The TextFields on my SwiftUI macOS project are making my window height too tall

I have a multiplatform SwiftUI project and I noticed on the macOS side that some of the views make my window too tall and I, the user, can't make it shorter. I noticed this with two particular views and so created a sample project containing a simplified version to duplicate this issue and figure it out. I was able to duplicate the issue and, through commenting out lines and running it, found the source seems to be the TextField itself. Is there anyway to make the window shorter as is? Is there something I can use instead of a TextField to fix this? Or am I stuck with this until the framework is updated? Figured I'd ask here in case anyone has any insight.
For your ease at replicating I'll include the code and then, below, screenshots explaining my issue. Thanks
Main ContentView:
struct ContentView: View {
var body: some View {
TabView {
ExampleViewOne()
.tabItem {
Label("Example One", image: "one")
}
.tag("one")
ExampleViewTwo()
.tabItem {
Label("Example Two", image: "two")
}
.tag("two")
}
.background(Color.green)
}
}
Example One struct for the first tab:
struct ExampleViewOne: View {
// Editable Fields
#State var aString:String = "name"
#State var aBoolean:Bool = false
var body: some View {
VStack(alignment: .center) {
Form {
Text("Hello Everyone!")
.italic()
.frame(maxWidth: .infinity)
.multilineTextAlignment(.center)
Button(action: {
print("A Button")
}, label: {
HStack(spacing: 10) {
Text("A Button")
}
.frame(maxWidth: .infinity)
})
TextField("Name:", text: $aString, prompt: Text("Nickname"))
// TextField("Username:", text: $aString, prompt: Text("root"))
// TextField("Password:", text: $aString, prompt: Text("********"))
// TextField("I.P. Address:", text: $aString, prompt: Text("XXX.XXX.X.XXX"))
Button(action: {
print("Another button")
}) {
HStack(spacing: 10) {
Image(systemName: "questionmark")
Text("Another button")
Image(systemName: "questionmark")
}
.frame(maxWidth: .infinity)
}
Divider()
Toggle(isOn: $aBoolean, label: {
Text("This is the label of this switch")
})
}
.background(Color.purple)
Spacer()
.background(Color.red)
HStack(spacing:10) {
Button(action: {
print("Delete")
}) {
HStack(spacing: 10) {
Image(systemName: "trash.fill")
Text("Delete (⌘ d)")
}
.frame(maxWidth: .infinity)
}
.keyboardShortcut("d", modifiers: .command)
Button(action: {
print("Save")
}) {
HStack(spacing: 10) {
Image(systemName: "checkmark")
Text("Save")
}
.frame(maxWidth: .infinity)
}
.keyboardShortcut("s", modifiers: .command)
}
.background(Color.blue)
}
.padding()
.background(Color.black)
}
}
Example Two struct for the second tab
struct ExampleViewTwo: View {
// Editable Fields
#State var aString:String = "name"
#State var aBoolean:Bool = false
let theExamples = ["example one", "example two", "example another"]
var body: some View {
VStack(alignment: .center) {
Form {
TextField("Display Name:", text: $aString, prompt: Text("Name"))
// TextField("Directory (Folder) Name:", text: $aString, prompt: Text("Directory Name"))
ScrollView {
Text("These are the examples.")
ForEach(theExamples, id:\.self) { number in
Text(number)
}
}
}
.background(Color.purple)
Spacer()
.background(Color.red)
HStack(spacing:10) {
Button(action: {
print("Delete")
}) {
HStack(spacing: 10) {
Image(systemName: "trash.fill")
Text("Delete (⌘ d)")
}
.frame(maxWidth: .infinity)
}
.keyboardShortcut("d", modifiers: .command)
Button(action: {
print("Save")
}) {
HStack(spacing: 10) {
Image(systemName: "checkmark")
Text("Save")
}
.frame(maxWidth: .infinity)
}
.keyboardShortcut("s", modifiers: .command)
}
.background(Color.blue)
}
.padding()
.background(Color.black)
}
}
Explanation:
I set different background colors to each view, to tell them apart, and then played with the spacers before commenting out the code in the top (purple) view and slowly bringing it back until the height was restricted again.
The spacer background color isn't shown on either tab but it is used on Example One. For Example One the views are centered vertically if the spacer isn't there yet the height isn't affected. On Example Two the height also stays the same regardless of the spacer's presence but the space itself is taken up by the purple view instead.
The first tab with the spacer included is laid out properly but I want the black space between the two smaller:
Without the spacer the views are centered but the height is still unable to be shortened by the user:
The second tab looks the same regardless whether there's a spacer or not with the purple view taking up the space that is somehow required:
After commenting out the code and bringing it back again I realized the culprit was the TextField as without them the window could be as small as I wanted it. With one TextField uncommented the height was restricted. With each additional TextField uncommented the height became taller and taller.
With no TextField in the view I could make the Window however small I wanted:
As soon as I added a TextField back the height was restricted:
With each additional TextField added the height needed became larger and thus the empty black space was larger. I can't make the Window smaller than this:
The same could be said for the second example window. No TextFields means I could make the Window really short. The uncommenting of a TextField made the height increase without me making it smaller. The additional TextFields makes it taller and the purple space larger:
Is there anyway to allow me to make this Window smaller in the presence of the TextFields?
My other views don't seem to have this problem but they're contained within a ScrollView. I could embed these too but now I'm really curious what's causing it.
Thanks for any help.

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
This is as small as I can make it:
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) {}
}
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:
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.
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.
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.

SwiftUI: Double click on white space in a list row in macOS?

I'm trying to get the entirety of a row to be double clickable.
Here is my code:
import SwiftUI
let colourArray = ["red","blue","green"]
struct ContentView: View
{
var body: some View
{
List
{
ForEach (colourArray, id: \.self)
{ colour in
ArrayRow(colour: colour)
.gesture(TapGesture(count: 2).onEnded
{
print("double clicked")
})
.padding(.bottom, 15)
}
}
}
}
struct ArrayRow: View
{
let colour: String
var body: some View
{
HStack(alignment: .top)
{
Text(colour)
.frame(width: 150, alignment: .leading)
Text(colour)
.frame(width: 150, alignment: .leading)
Text(colour)
.frame(width: 150, alignment: .leading)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The double click works great, but only if you double click on the text. If you double click on the white space then nothing happens. How do I get the whole row to respond to the double click?
You can extend the tappable/clickable area with .contentShape
ArrayRow(colour: colour)
.contentShape(Rectangle())
.gesture(TapGesture(count: 2).onEnded
{
print("double clicked")
})
.padding(.bottom, 15)

SWIFTUI: Take out a gray rectangle on top of the TabBar

I have a problem related on my List view. The question is simple: how can I get rid of that wierd gray rectangle showing on top of the TabBar? I didn't code that, I just implemented a controller with a List and NavigationBar and then it showed that thing.
For more clear explanation I post the images:
ItemRow.swift code:
import SwiftUI
struct ItemRow: View {
static let colors: [String: Color] = ["D": .purple, "G": .orange, "N": .red, "S": .yellow, "V": .pink]
var item: MenuItem
var body: some View {
NavigationLink(destination: Text(item.name)) {
HStack {
Image(item.thumbnailImage)
.clipShape(Circle())
.overlay(Circle().stroke(Color("IkeaBlu"), lineWidth: 2))
VStack(alignment: .leading){
Text(item.name)
.font(.headline)
Text("€ \(item.price)")
}.layoutPriority(1)
Spacer()
ForEach(item.restrictions, id: \.self) { restriction in
Text(restriction)
.font(.caption)
.fontWeight(.black)
.padding(5)
.background(Self.colors[restriction, default: .black])
.clipShape(Circle())
.foregroundColor(.white)
}
}
}
}
}
struct ItemRow_Previews: PreviewProvider {
static var previews: some View {
ItemRow(item: MenuItem.example)
}
}
thanks a lot for the help
Remove the marked part of hack from TabBar view and that glitch will go.
Tested with Xcode 11.4 / iOS 13.4
} .onAppear {
// UITabBar.appearance().isTranslucent = false // << this one !!
UITabBar.appearance().barTintColor = UIColor(named: "IkeaBlu")
}.accentColor(Color(.white))

How can I align a progress indicator to the right of a label and keep the label centred in a VStack in SwiftUI?

I want to display a label in the centre of a view with a progress indicator to the right of the label. How can I do this in SwiftUI on macOS?
The code below aligns the HStack in the centre of the VStack but I want the text centered and the progress indicator aligned with the text's trailing edge. I guess I could replace the HStack with a ZStack but it's still not clear how one aligns two controls to each other or how one prevents the container from be centered by its container.
import SwiftUI
struct AlignmentTestView: View {
var body: some View {
VStack(alignment: .center, spacing: 4) {
HStack {
Text("Some text")
ActivityIndicator()
}
}.frame(width: 200, height: 200)
.background(Color.pink)
}
}
struct AlignmentTestView_Previews: PreviewProvider {
static var previews: some View {
AlignmentTestView()
}
}
Here is possible approach (tested replacing ActivityIndicator with just circle).
Used Xcode 11.4 / iOS 13.4 / macOS 10.15.4
var body: some View {
VStack {
HStack {
Text("Some text")
.alignmentGuide(.hAlignment) { $0.width / 2.0 }
ActivityIndicator()
}
}
.frame(width: 200, height: 200, alignment:
Alignment(horizontal: .hAlignment, vertical: VerticalAlignment.center))
.background(Color.pink)
}
extension HorizontalAlignment {
private enum HAlignment : AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[HorizontalAlignment.center]
}
}
static let hAlignment = HorizontalAlignment(HAlignment.self)
}

Resources