SwiftUI MacOS Form Custom Layout - macos

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.

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.

How to align baselines across multiple VStacks?

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

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

Add Image to TextField/SecureField in SwiftUI, add padding to placeholder text

I made a textfield and a securetextfield in SwiftUI but I have no idea how to add in an image into my textfield/secure textfield in SwiftUI. There is not much documentation online for SwiftUI like there was for the older versions of Swift. I also want to shift over the (placeholder/typed in text) over by a designated amount say for example like 30 points to the right. I also was trying out to see if the background color would change from white to red, but as you can see, it is in my code with no effect on the UI.
Note:I have the GeometryReader called earlier in my code as well as the #state variables for the username and the password.
My goal is to have it look like this , right now it looks like this
VStack (spacing: deviceSize.size.height * (50/812)) {
TextField ("Username", text: self.$username)
.foregroundColor(.black)//text color when you type
.accentColor(.blue)//cursor color
.background(Color(.red))//????
.textFieldStyle(RoundedBorderTextFieldStyle())
.cornerRadius(50)
// .border(Color.white)
//.font(.title)
SecureField ("Password", text: self.$password)
.textFieldStyle(RoundedBorderTextFieldStyle())
.cornerRadius(50)
}
.padding(.init(top: 0, leading: deviceSize.size.width * (38/375), bottom: 0, trailing: deviceSize.size.width * (38/375)))
The easiest way to achieve such a design would be to place the Image and TextField in a HStack and give it one Rounded background. It is slightly more complicated with the password field as it needs an extra Button, and when you hide/show the password you need to change between TextField and SecureField. Here is my take on it:
struct ContentView: View {
#State private var username = ""
#State private var password = ""
#State private var showPassword = false
var body: some View {
ZStack {
Color.blue
VStack {
HStack {
Image(systemName: "person")
.foregroundColor(.secondary)
TextField("Username",
text: $username)
} .padding()
.background(Capsule().fill(Color.white))
HStack {
Image(systemName: "lock")
.foregroundColor(.secondary)
if showPassword {
TextField("Password",
text: $password)
} else {
SecureField("Password",
text: $password)
}
Button(action: { self.showPassword.toggle()}) {
Image(systemName: "eye")
.foregroundColor(.secondary)
}
} .padding()
.background(Capsule().fill(Color.white))
} .padding()
}
}
}
I'm really new to SwiftUI, but I found a workaround for this that I hope doesn't cause any issues in the future or it will be a big lesson learned. If anyone has any suggestion I'd appreciate it too! =]
I embedded the TextField and the image in a ZStack and I put the image inside a View and gave the view a padding.
struct FormInputBox: View {
#State private var text: String = ""
#State private var textFieldState: TextFieldState = .empty
private var textFieldType: TextFieldType
private var textViewPlaceholder = ""
init(placeholder: String,
textFieldType: TextFieldType) {
self.textViewPlaceholder = placeholder
self.textFieldType = textFieldType
}
var body: some View {
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .center), content: {
TextField(textViewPlaceholder, text: $text)
.textFieldStyle(MyTextFieldStyle(textFieldState: $textFieldState))
AnyView(
Image("tick")
.resizable()
.frame(width: 20, height: 20, alignment: .leading)
)
.padding(32)
})
}
I have created a reusable SwiftUI Textfield named ASTextField which works similar to the textField in UIKit, where you can add the leftView and rightView of the textField and can handle the events related them.
You can find the implementation of this at gist.
This the way you can consume it:-
struct ContentView : View , ASTextFieldDelegate {
let leftImage = UIImage(systemName: "calendar")
let rightImage = UIImage(systemName: "eye")
let rightImage1 = UIImage(systemName: "trash")
#State var text : String? = "with simple binding"
#State var text1 : String? = "with closure for right item"
#State var text2 : String? = "for secure entry"
var body: some View {
VStack {
Spacer()
ASTextField(text: $text)
Spacer()
ASTextField(rightItem: rightImage1, leftItem: leftImage, handleLeftTap: {
print("right icon tapped.....")
}, delegate: self, text: $text1)
Spacer()
ASTextField(rightItem: rightImage, leftItem: leftImage, isSecuredEntry: true, delegate: self, text: $text2)
Spacer()
}
}
}
"Introspect" will work for you
Textfield()
.introspectTextField { textfield in
textfield.rightViewMode = .unlessEditing
textfield.rightView = UIImageView(image: UIImage(named: ImageCatalog.error.content))
}
I am totally newborn toddle in iOS Dev. So i wrote just like this. My apologises in advance if someone will get blind from the ugliness of the written code.
struct ContentView: View {
#State private var nameSearch: String = ""
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 25)
.frame(width: 230, height: 30)
.border(.black, width: 0.2)
.foregroundColor(.white)
HStack {
ZStack {
Image(systemName: "magnifyingglass.circle")
.foregroundColor(.gray)
.frame(width: 10, height: 10, alignment: .leading)
.padding(.trailing, 200)
TextField( "Search", text: $nameSearch)
.frame(width: 180, height: 30)
.padding(.leading, 20 )
}
}
}

SwiftUI: Custom Modal Animation

I made a custom modal using SwiftUI. It works fine, but the animation is wonky.
When played in slow motion, you can see that the ModalContent's background disappears immediately after triggering ModalOverlay's tap action. However, ModalContent's Text views stay visible the entire time.
Can anyone tell me how I can prevent ModalContent's background from prematurely disappearing?
Slow-mo video and code below:
import SwiftUI
struct ContentView: View {
#State private var isShowingModal = false
var body: some View {
GeometryReader { geometry in
ZStack {
Button(
action: { withAnimation { self.isShowingModal = true } },
label: { Text("Show Modal") }
)
ZStack {
if self.isShowingModal {
ModalOverlay(tapAction: { withAnimation { self.isShowingModal = false } })
ModalContent().transition(.move(edge: .bottom))
}
}.edgesIgnoringSafeArea(.all)
}
}
}
}
struct ModalOverlay: View {
var color = Color.black.opacity(0.4)
var tapAction: (() -> Void)? = nil
var body: some View {
color.onTapGesture { self.tapAction?() }
}
}
struct ModalContent: View {
var body: some View {
GeometryReader { geometry in
VStack {
Spacer()
VStack(spacing: 16) {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
.frame(width: geometry.size.width)
.padding(.top, 16)
.padding(.bottom, geometry.safeAreaInsets.bottom)
.background(Color.white)
}
}
}
}
The solution (thanks to #JWK):
It's probably a bug. It seems that, during the transition animation (when the views are disappearing) the zIndex of the two views involved (the ModalContent and the ModalOverlay) is not respected. The ModalContent (that is supposed to be in front of the ModalOverlay) is actually moved under the ModalOverlay at the beginning of the animation. To fix this we can manually set the zIndex to, for example, 1 on the ModalContent view.
struct ContentView: View {
#State private var isShowingModal = false
var body: some View {
GeometryReader { geometry in
ZStack {
Button(
action: { withAnimation { self.isShowingModal = true } },
label: { Text("Show Modal") }
)
ZStack {
if self.isShowingModal {
ModalOverlay(tapAction: { withAnimation(.easeOut(duration: 5)) { self.isShowingModal = false } })
ModalContent()
.transition(.move(edge: .bottom))
.zIndex(1)
}
}.edgesIgnoringSafeArea(.all)
}
}
}
}
The investigation that brings to a solution
Transition animations in SwiftUI have still some issues. I think this is a bug. I'm quite sure because:
1) Have you tried to change the background color of your ModalContent from white to green?
struct ModalContent: View {
var body: some View {
GeometryReader { geometry in
VStack {
Spacer()
VStack(spacing: 16) {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
.frame(width: geometry.size.width)
.padding(.top, 16)
.padding(.bottom, geometry.safeAreaInsets.bottom)
.background(Color.green)
}
}
}
}
This way it works (see the following GIF):
2) Another way to make the bug occur is to change the background color of your ContentView to, for example, green, leaving the ModalContent to white:
struct ContentView: View {
#State private var isShowingModal = false
var body: some View {
GeometryReader { geometry in
ZStack {
Button(
action: { withAnimation(.easeOut(duration: 5)) { self.isShowingModal = true } },
label: { Text("Show Modal") }
)
ZStack {
if self.isShowingModal {
ModalOverlay(tapAction: { withAnimation(.easeOut(duration: 5)) { self.isShowingModal = false } })
ModalContent().transition(.move(edge: .bottom))
}
}
}
}
.background(Color.green)
.edgesIgnoringSafeArea(.all)
}
}
struct ModalOverlay: View {
var color = Color.black.opacity(0.4)
var tapAction: (() -> Void)? = nil
var body: some View {
color.onTapGesture { self.tapAction?() }
}
}
struct ModalContent: View {
var body: some View {
GeometryReader { geometry in
VStack {
Spacer()
VStack(spacing: 16) {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
.frame(width: geometry.size.width)
.padding(.top, 16)
.padding(.bottom, geometry.safeAreaInsets.bottom)
.background(Color.white)
}
}
}
}
Even in this case it works as expected:
3) But if you change your ModalContent background color to green (so you have both the ContentView and the ModalContent green), the problem occurs again (I won't post another GIF but you can easily try it yourself).
4) Yet another example: if you change the appearance of you iPhone to Dark Appearance (the new feature of iOS 13) your ContentView will automatically become black and, since your ModalView is white, the problem won't occur and everything goes fine.

Resources