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

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

Related

2-step animated transition in SwiftUI

I'd like to make a custom view transition in SwiftUI, animated in 2 steps:
Presenting child
Fade out parent (opacity to 0)
(Then) fade in child and slide from bottom
Dismissing child
Fade out child and slide to bottom
(Then) fade in parent
Demo (created artificially)
Base code:
struct TransitionTestView: View {
#State var presented = false
var body: some View {
ZStack {
if presented {
Button("Hide", action: { presented = false })
.foregroundColor(.black)
.font(.largeTitle)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.purple)
}
else {
Button("Show", action: { presented = true })
.foregroundColor(.black)
.font(.largeTitle)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.orange)
}
}
.padding(30)
}
}
I've experimented with:
Different placements of the animation modifier: to the ZStack, or to each element individually
Applying the animation directly to the transition: .opacity.animation(.linear(duration: 1).delay(1))
But I couldn't make it work. Any help appreciated, thanks a lot!
A possible approach can be using transaction transformation by injecting additional delay to main animation for each phase.
Tested with Xcode 13.4 / iOS 15.5
Main part:
ZStack {
VStack { // << animating container
if presented {
Button("Hide", action: { presented = false })
.foregroundColor(.black)
.font(.largeTitle)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.purple)
.transition(.move(edge: .bottom))
}
}
.transaction { // << transform animation for direct transition
$0.animation = $0.animation?.delay(presented ? 1.2 : 0)
}
VStack { // << animating container
if !presented {
Button("Show", action: { presented = true })
.foregroundColor(.black)
.font(.largeTitle)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.orange)
.transition(.opacity)
}
}
.transaction { // << transform animation for reverse transition
$0.animation = $0.animation?.delay(presented ? 0 : 1.2)
}
}
.animation(.linear(duration: 1), value: presented) // << main !!
Test module on GitHub

Can't show dropdown menu over HStack in SwiftUI

I have a list item that needs to have a dropdown menu that shows on top of the other views in the HStack.
This is what I have currently:
struct ListRow: View {
#Binding var item: Item
#State var showDropdown: Bool = false
var body: some View {
ZStack {
HStack {
VStack {
Text(item.name)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .topLeading)
ProgressView(value: item.percentDone)
}.padding(.all, 10)
Button(action: {
self.showDropdown.toggle()
}, label: {
Image(systemName: "ellipsis.circle")
}).buttonStyle(BorderlessButtonStyle())
.overlay (
VStack {
if self.showDropdown {
Spacer(minLength: 40)
Text("TEST")
.onTapGesture {
print("Delete")
}
}
}.zIndex(10)
)
}
}
}
}
But when I run it and click the button this is what I see
It looks like the Text is not showing due to a view overflow/overlap with the rest of the HStack, but I thought that adding the .zIndex would place the dropdown on top of the rest of the views in the ZStack
Just set frame for your overlay view to make it exceed its main view 's size
.overlay (
VStack {
if self.showDropdown {
Spacer(minLength: 40)
Text("TEST")
.onTapGesture {
print("Delete")
}
}
}.frame(width: 100)
)
Or you can try another approach like ZStack or Menu (as you mentioned a better solution)
I figured out a better solution.
struct ListRow: View {
#Binding var item: Item
var body: some View {
HStack {
VStack {
Text(item.name)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .topLeading)
ProgressView(value: item.percentDone)
}.padding(.all, 10)
Menu {
Button("Delete", action: {
print("Delete")
})
} label: {
}
.menuStyle(BorderlessButtonMenuStyle())
.frame(width: 10, height: 10, alignment: .center)
}
}
}

SwiftUI button glitches on keyboard dismiss

I have a button on my view that, when pressed, calls a hideKeyboard function which is the following:
func hideKeyboard() {
let resign = #selector(UIResponder.resignFirstResponder)
UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
}
However, when the button is pressed and the keyboard gets dismissed, a glitch occurs where the button stays in place while the view moves downward:
https://giphy.com/gifs/Z7qCDpRSGoOb9CbVRQ
Reproducible example:
import SwiftUI
struct TestView: View {
#State private var username: String = ""
#State private var password: String = ""
#State private var isAnimating: Bool = false
var lightGrey = Color(red: 239.0/255.0,
green: 243.0/255.0,
blue: 244.0/255.0)
var body: some View {
ZStack() {
VStack() {
Spacer()
Text("Text")
.font(.title)
.fontWeight(.semibold)
.padding(.bottom, 15)
.frame(maxWidth: .infinity, alignment: .leading)
Text("Text")
.font(.subheadline)
.padding(.bottom)
.frame(maxWidth: .infinity, alignment: .leading)
TextField("username", text: $username)
.padding()
.background(self.lightGrey)
.cornerRadius(5.0)
.padding(.bottom, 20)
SecureField("password", text: $password)
.padding()
.background(self.lightGrey)
.cornerRadius(5.0)
.padding(.bottom, 20)
Button(action: {
self.hideKeyboard()
login()
})
{
if isAnimating {
ProgressView()
.colorScheme(.dark)
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.green)
.cornerRadius(10.0)
.padding(.bottom, 20)
}
else {
Text("Text")
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.green)
.cornerRadius(10.0)
.padding(.bottom, 20)
}
}
.disabled(username.isEmpty || password.isEmpty || isAnimating)
Text("Text")
.font(.footnote)
.frame(maxWidth: .infinity, alignment:.leading)
Spacer()
}
.padding()
.padding(.bottom, 150)
.background(Color.white)
}
}
func hideKeyboard() {
let resign = #selector(UIResponder.resignFirstResponder)
UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
}
func login() {
isAnimating = true
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
This is due to view replacement inside button, find below a fixed re-layout.
Tested with Xcode 13.2 / iOS 15.2 (slow animation was activated for demo)
Button(action: {
self.hideKeyboard()
login()
})
{
VStack { // << persistent container !!
if isAnimating {
ProgressView()
.colorScheme(.dark)
}
else {
Text("Text")
}
}
.foregroundColor(.white)
.font(.headline)
.padding()
.frame(maxWidth: .infinity)
.background(Color.green)
.cornerRadius(10.0)
.padding(.bottom, 20)
}
.disabled(username.isEmpty || password.isEmpty || isAnimating)
Looks like a conflict between UIApplication.shared.sendAction and SwiftUI: hiding the keyboard starts the animation, but updating the isAnimating property resets it.
Changing the order of calls solves the problem:
Button(action: {
login()
self.hideKeyboard()
})

Filling HStack swiftUI

I want to create a button stack like having a play and record button using SwiftUI, after creating it just does not look anything like what I wanted.
var body: some View {
HStack(alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/, spacing: 8) {
Spacer()
Button(action: {
print("Recordinggg")
}, label: {
Text("Record")
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(40.0)
})
Spacer()
Button(action: {
print("Recordinggg")
}, label: {
Text("Play")
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(40.0)
})
Spacer()
}
}
What I actually want is something like this
Use proper frame, padding you can achieve this. Here is an example code.
Create ButtonStyle.
struct ThemeButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.padding([.top, .bottom], 10)
.foregroundColor(.white)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
.background(Color.blue)
.cornerRadius(40.0)
}
}
Your view
struct ContentView: View {
#State private var phase: CGFloat = 0
var body: some View {
HStack(alignment: .center, spacing: 0) {
Button(action: {
print("Recordinggg")
}, label: {
Text("Record")
})
.buttonStyle(ThemeButtonStyle())
Spacer()
Button(action: {
print("Recordinggg")
}, label: {
Text("Play")
})
.buttonStyle(ThemeButtonStyle())
}
.padding()
}
}

How to align the an image on top of a button in swiftui?

I wish to add a 'trash' image on the top-right side of each button when 'Delete Button' is pressed, so that when user hits the trash image, the button will be removed from the vstack.
I think I should use zstack to position the trash image but I don't know how for now.
Below shows where the trash image should be located in each button.
Also, when I press the 'Delete Button', it seems that each button's text size and spacing with another button is changed slightly. How do I overcome this problem? The button position, spacing, textsize should be unchanged when 'Delete Button' is hit.
struct someButton: View {
#Environment(\.editMode) var mode
#ObservedObject var someData = SomeData()
#State var newButtonTitle = ""
#State var isEdit = false
var body: some View {
NavigationView{
// List{ // VStack
VStack{
VStack{
ForEach(Array(someData.buttonTitles.keys.enumerated()), id: \.element){ ind, buttonKeyName in
//
Button(action: {
self.someData.buttonTitles[buttonKeyName] = !self.someData.buttonTitles[buttonKeyName]!
print("Button pressed! buttonKeyName is: \(buttonKeyName) Index is \(ind)")
print("bool is \(self.someData.buttonTitles[buttonKeyName]!)")
}) {
HStack{ //HStack, ZStack
if self.isEdit{
Image(systemName: "trash")
.foregroundColor(.red)
.onTapGesture{
print("buttonkey \(buttonKeyName) will be deleted")
self.deleteItem(ind: ind)
}
}
Text(buttonKeyName)
// .fontWeight(.semibold)
// .font(.title)
}
}
.buttonStyle(GradientBackgroundStyle(isTapped: self.someData.buttonTitles[buttonKeyName]!))
.padding(.bottom, 20)
}
}
HStack{
TextField("Enter new button name", text: $newButtonTitle){
self.someData.buttonTitles[self.newButtonTitle] = false
self.newButtonTitle = ""
}
}
}
.navigationBarItems(leading: Button(action: {self.isEdit.toggle()}){Text("Delete Button")},
trailing: EditButton())
// .navigationBarItems(leading: Button(action: {}){Text("ergheh")})
// }
}
}
func deleteItem(ind: Int) {
let key = Array(someData.buttonTitles.keys)[ind]
print(" deleting ind \(ind), key: \(key)")
self.someData.buttonTitles.removeValue(forKey: key)
}
}
struct GradientBackgroundStyle: ButtonStyle {
var isTapped: Bool
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.frame(maxWidth: .infinity, maxHeight: 50)
.padding()
.foregroundColor(isTapped ? Color.blue : Color.black)
.background(LinearGradient(gradient: Gradient(colors: [Color("DarkGreen"), Color("LightGreen")]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(40)
.overlay(RoundedRectangle(cornerRadius: 40)
.stroke(isTapped ? Color.blue : Color.black, lineWidth: 4))
.shadow(radius: 40)
.padding(.horizontal, 20)
.scaleEffect(configuration.isPressed ? 0.9 : 1.0)
//
}
}
class SomeData: ObservableObject{
#Published var buttonTitles: [String: Bool] = ["tag1": false, "tag2": false]
}
Here is a demo of possible approach. Tested with Xcode 11.4 / iOS 13.4 (with some replicated code)
var body: some View {
Button(action: { }) {
Text("Name")
}
.buttonStyle(GradientBackgroundStyle(isTapped: tapped))
.overlay(Group {
if self.isEdit {
ZStack {
Button(action: {print(">> Trash Tapped")}) {
Image(systemName: "trash")
.foregroundColor(.red).font(.title)
}.padding(.trailing, 40)
.alignmentGuide(.top) { $0[.bottom] }
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
}
})
.padding(.bottom, 20)
}