How do I make an HStack of SwiftUI buttons have height? - swift

I have a simple SwiftUI row of buttons to mimic a keyboard layout. Just can't get the buttons to fill the height of the HStack. I can't tell where I need to give something height so it will look like a keyboard. No matter where I try to add height, the buttons still are small.
struct KeyboardView: View {
let rows = [String.keyboardFKeyRow, String.keyboardNumKeyRow, String.alpha1KeyRow, String.alpha2KeyRow, String.alpha3KeyRow, String.arrowKeyRow]
var body: some View {
VStack() {
KeyRow(labels: String.keyboardFKeyRow).frame(height: 100)
KeyRow(labels: String.keyboardNumKeyRow).frame(height: 100)
KeyRow(labels: String.alpha1KeyRow).frame(height: 100)
KeyRow(labels: String.alpha2KeyRow).frame(height: 100)
KeyRow(labels: String.alpha3KeyRow).frame(height: 100)
KeyRow(labels: String.arrowKeyRow).frame(height: 100)
}
}
}
struct KeyRow: View {
var labels: [String]
var body: some View {
HStack() {
Spacer()
ForEach(labels, id: \.self) { label in
Button {
print("Key Clicked")
} label: {
Text(label.capitalized)
.font(.body)
.frame(minWidth: 60, maxWidth: 80, minHeight: 80, maxHeight: .infinity)
}
}
Spacer()
}
}
}

The .bordered button style on macOS has a hard-coded size and refuses to stretch vertically. If you need a different button height, either use the .borderless style or create your own ButtonStyle or PrimitiveButtonStyle, and (either way) draw the background yourself. For example, I got this result:
from this playground:
import PlaygroundSupport
import SwiftUI
PlaygroundPage.current.setLiveView(HStack {
// Default button style, Text label.
Button("a", action: {})
.background { Rectangle().stroke(.mint.opacity(0.5), lineWidth: 1).clipped() }
.frame(maxHeight: .infinity)
// Default button style, vertically greedy Text label.
Button(action: {}, label: {
Text("b")
.frame(maxHeight: .infinity)
})
.background { Rectangle().stroke(.mint.opacity(0.5), lineWidth: 1).clipped() }
.frame(maxHeight: .infinity)
Button(action: {}, label: {
Text("c")
.fixedSize()
.padding()
.frame(maxHeight: .infinity)
.background {
RoundedRectangle(cornerRadius: 10)
.fill(Color(nsColor: .controlColor))
}
}).buttonStyle(.borderless)
.background { Rectangle().stroke(.mint.opacity(0.5), lineWidth: 1).clipped() }
.frame(maxHeight: .infinity)
}
.background { Rectangle().stroke(.red.opacity(0.5), lineWidth: 1).clipped() }
.frame(height: 100)
)

Related

How to avoid Bottom View to come over the Keyboard in SwiftUI?

My View is something like this:
VStack{
ScrollView {
ForEach(0..<50) {_ in
Text("Editor")
}
VStack {
TextField("My Text Filed, text: $text)
}
.frame(minHeight: 200, maxHeight: 200)
.overlay(
RoundedRectangle(cornerRadius: 6.0)
.stroke(.gray, lineWidth: 1.0)
)
.padding()
}
Button("My Button")
.background(.green)
.padding()
}
My problem is that whenever clicking on text field my Button comes on the top of keyboard. I know this is cool feature and expected that view out side if scroll will come to top of keyboard. I want to avoid this, can any body give some solution how to avoid this.
I want my Button to always be below the ScrollView in the bottom not on the top of keyboard
Try the below code:
#State private var text = ""
var body: some View {
ZStack{
VStack{
Spacer()
Button("Button title") {
print("Button tapped!")
}
}
.ignoresSafeArea(.keyboard)
ScrollView {
ForEach(0..<50) {_ in
Text("Editor")
}
VStack {
TextField("My Text Filed", text: $text)
}
.frame(minHeight: 200, maxHeight: 200)
.overlay(
RoundedRectangle(cornerRadius: 6.0)
.stroke(.gray, lineWidth: 1.0)
)
.padding()
}
.padding(.bottom, 20)
}
}

SwiftUI Button content uses its own animation where it shouldn't

I have a custom transition to present/dismiss a custom sheet. My problem is that the button content is using its own animation, where it should just follow the rest:
See how the "OK" button is jumping to the bottom in the dismiss animation. It should just be following the rest of the sheet.
Full code:
import SwiftUI
#main
struct SwiftUITestsApp: App {
var body: some Scene {
WindowGroup {
SheetButtonAnimationTestView()
}
}
}
struct SheetButtonAnimationTestView: View {
#State private var sheetPresented = false
var body: some View {
ZStack(alignment: .bottom) {
Button("Present sheet", action: { sheetPresented = true })
.frame(maxWidth: .infinity, maxHeight: .infinity)
if sheetPresented {
Sheet(title: "Sheet", dismiss: { sheetPresented = false })
.transition(.move(edge: .bottom))
}
}
.animation(.easeOut(duration: 2), value: sheetPresented)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white.edgesIgnoringSafeArea(.all))
}
}
struct Sheet: View {
var title: String
var dismiss: () -> ()
var body: some View {
VStack(spacing: 16) {
HStack {
Text(title)
.foregroundColor(.white)
Spacer()
Button(action: dismiss) {
Text("OK").font(.headline.bold()).foregroundColor(.blue)
.padding(10)
.background(Capsule().fill(Color.white))
}
}
Text("This is the sheet content")
.foregroundColor(.white)
.frame(height: 300)
.frame(maxWidth: .infinity)
}
.padding(24)
.frame(maxWidth: .infinity)
.background(
Rectangle().fill(Color.black).edgesIgnoringSafeArea(.bottom)
)
.ignoresSafeArea(.container, edges: .bottom) // allows safe area for keyboard
}
}
How to make the button follow the sheet animation?
Tested on iOS 16.0.3, iPhone 11, XCode 14.1
Seems like a bug in the library. You can add a slight delay in the button action, which will solve the issue:
Button(action: {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.001) {
dismiss()
}
}) {
Text("OK").font(.headline.bold()).foregroundColor(.blue)
.padding(10)
.background(Capsule().fill(Color.white))
}
Or if you don't want the click animation, you can remove the button and do the following:
Text("OK").font(.headline.bold()).foregroundColor(.blue)
.padding(10)
.background(Capsule().fill(Color.white))
.onTapGesture {
dismiss()
}

SwiftUI Centering Image between Views

I have an HStack that I am using as a tool bar. The HStack contains a button on the left, image in the middle, and then another HStack containing 2 buttons. Everything looks fine except for the image in the middle, it's slightly to the left. How can I get the image centered?
var toolbar: some View {
HStack(alignment: .center) {
Button {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "xmark")
.tint(.black)
}
Spacer()
Image("nav_logo")
.resizable()
.frame(width: 30, height: 30)
Spacer()
optionsButton
}
.frame(height: 30)
}
var optionsButton: some View {
HStack {
Button {
UIPasteboard.general.string = viewModel.take.shareLinkString
self.buildBottomAlert(type: .linkCopied)
} label: {
Image("shareIcon")
}
.padding(.trailing, 10)
Button {
showingAlert = true
} label: {
Image("moreMenu")
}
}
The image in the middle is offset because the first button and optionsButton do not have the same width. But the SpacerĀ“s inside the containing Stack do have equal width.
To solve this you could wrap them in to a container that is big enough so both have the same width.
e.g.:
//GeometryReader to get the size of the container this is in
GeometryReader{proxy in
HStack(alignment: .center) {
//Hstack to wrap the button
HStack(){
Button {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "xmark")
.tint(.black)
}
// Spacer to move the button to the left
Spacer()
}
// depending on the use case you would need to tweak this value
.frame(width: proxy.size.width / 4)
Spacer()
Image(systemName: "square.and.arrow.up.trianglebadge.exclamationmark")
.resizable()
.frame(width: 30, height: 30)
Spacer()
HStack(){
Spacer()
optionsButton
}
.frame(width: proxy.size.width / 4)
}
.frame(height: 30)
}

SwiftUI Size Struct by .frame modifier

Hello I got a Custom Button Struct which has an Image and a Text and also a Style for that Button, which is added at the end of the Struct. My Question is: Is there any way I can use the .frame(width: , height: ) and .frame(minWidth: maxWidth) modifier, without passing a variable to the struct which then passes it to the button style?
CustomButton:
struct CustomButtonWithImage: View {
let title: String
let image: String
let action: () -> Void
var body: some View {
Button(action: action) {
HStack {
Image(image)
.resizable()
.renderingMode(.template)
.frame(maxWidth: imageSize.width, maxHeight: imageSize.height)
.aspectRatio(contentMode: .fit)
Text(title)
.font(.system(size: fontSize))
.bold()
.minimumScaleFactor(0.8)
.lineLimit(1)
}
.padding(.horizontal, horizontalPadding)
}
.buttonStyle(CustomButtonStyle(size: buttonSize)) // <- Button Style
.accentColor(Color.white)
}
}
struct CustomButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration
.label
.contentShape(Rectangle())
.foregroundColor(Color.white)
.accentColor(Color.white)
.background(Color("CustomButtonColor"))
}
}
Basically I just want to Do this:
var body: some View {
VStack {
CustomButton(text: "hello": image("someImage.png") { print("hello") }
// Is there a way to set this without passing the struct a var or let?
// I can set this but this wont make the button bigger, because the label in the button style wont change size with this
.frame(width: 40, height: 50)
.frame(maxWidth: ..., maxHeight: ...)
}
}
}
Remove hardcoded frame and add max allowed, so it will consume available space shrank only by other views layout. Also you can use only one, either maxWidth or maxHeight restriction if needed.
CustomButton(text: "hello": image("someImage.png") { print("hello") }
// .frame(width: 40, height: 50) // remove hardcode
.frame(maxWidth: .infinity, maxHeight: .infinity) // << max allowed

How to do a "reveal"-style collapse/expand animation in SwiftUI?

I'd like to implement an animation in SwiftUI that "reveals" the content of a view to enable expand/collapse functionality. The content of the view I want to collapse and expand is complex: It's not just a simple box, but it's a view hierarchy of dynamic height and content, including images and text.
I've experimented with different options, but it hasn't resulted in the desired effect. Usually what happens is that when I "expand", the whole view was shown right away with 0% opacity, then gradually faded in, with the buttons under the expanded view moving down at the same time. That's what happened when I was using a conditional if statement that actually added and removed the view. So that makes sense.
I then experimented with using a frame modifier: .frame(maxHeight: isExpanded ? .infinity : 0). But that resulted in the contents of the view being "squished" instead of revealed.
I made a paper prototype of what I want:
Any ideas on how to achieve this?
Something like this might work. You can modify the height of what you want to disclose to be 0 when hidden or nil when not so that it'll go for the height defined by the views. Make sure to clip the view afterwards so the contents are not visible outside of the frame's height when not disclosed.
struct ContentView: View {
#State private var isDisclosed = false
var body: some View {
VStack {
Button("Expand") {
withAnimation {
isDisclosed.toggle()
}
}
.buttonStyle(.plain)
VStack {
GroupBox {
Text("Hi")
}
GroupBox {
Text("More details here")
}
}
.frame(height: isDisclosed ? nil : 0, alignment: .top)
.clipped()
HStack {
Text("Cancel")
Spacer()
Text("Book")
}
}
.frame(maxWidth: .infinity)
.background(.thinMaterial)
.padding()
}
}
No, this wasn't trying to match your design, either. This was just to provide a sample way of creating the animation.
Consider the utilization of DisclosureGroup. The following code should be a good approach to your idea.
struct ContentView: View {
var body: some View {
List(0...20, id: \.self) { idx in
DisclosureGroup {
HStack {
Image(systemName: "person.circle.fill")
VStack(alignment: .leading) {
Text("ABC")
Text("Test Test")
}
}
HStack {
Image(systemName: "globe")
VStack(alignment: .leading) {
Text("ABC")
Text("X Y Z")
}
}
HStack {
Image(systemName: "water.waves")
VStack(alignment: .leading) {
Text("Bla Bla")
Text("123")
}
}
HStack{
Button("Cancel", role: .destructive) {}
Spacer()
Button("Book") {}
}
} label: {
HStack {
Spacer()
Text("Expand")
}
}
}
}
The result looks like:
I coded this in under 5 minutes. So of course the design can be optimized to your demands, but the core should be understandable.
import SwiftUI
struct TaskViewCollapsible: View {
#State private var isDisclosed = false
let header: String = "Review Page"
let url: String
let tasks: [String]
var body: some View {
VStack {
HStack {
VStack(spacing: 5) {
Text(header)
.font(.system(size: 22, weight: .semibold))
.foregroundColor(.black)
.padding(.top, 10)
.padding(.horizontal, 20)
.frame(maxWidth: .infinity, alignment: .leading)
Text(url)
.font(.system(size: 12, weight: .regular))
.foregroundColor(.black.opacity(0.4))
.padding(.horizontal, 20)
.frame(maxWidth: .infinity, alignment: .leading)
}
Spacer()
Image(systemName: self.isDisclosed ? "chevron.up" : "chevron.down")
.padding(.trailing)
.padding(.top, 10)
}
.onTapGesture {
withAnimation {
isDisclosed.toggle()
}
}
FetchTasks()
.padding(.horizontal, 20)
.padding(.bottom, 5)
.frame(height: isDisclosed ? nil : 0, alignment: .top)
.clipped()
}
.background(
RoundedRectangle(cornerRadius: 8)
.fill(.black.opacity(0.2))
)
.frame(maxWidth: .infinity)
.padding()
}
#ViewBuilder
func FetchTasks() -> some View {
ScrollView(.vertical, showsIndicators: true) {
VStack {
ForEach(0 ..< tasks.count, id: \.self) { value in
Text(tasks[value])
.font(.system(size: 16, weight: .regular))
.foregroundColor(.black)
.padding(.vertical, 0)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
.frame(maxHeight: CGFloat(tasks.count) * 20)
}
}
struct TaskViewCollapsible_Previews: PreviewProvider {
static var previews: some View {
TaskViewCollapsible(url: "trello.com", tasks: ["Hello", "Hello", "Hello"])
}
}