Good day!
I have two buttons, one with X and one that says Cancel. Depending on which button I press, I want the X button to have a different transition.
When pressing X: I want the removal of the X button to be .opacity.animation(.default)
When pressing Cancel: I want the removal of the X button to be .move(edge: .trailing).combined(with: .opacity).animation(.default)
For context, it's a textField, when I type something the X button appears.
I've tried with a State variable, setting the transition in withAnimation in the Button action. That seems to set the transition for the next cycle/refresh and not the current one. I've also looked into transactions, but I can only manipulate animations from there?
So when I press the X button, the next time I press either X or Cancel it will be what I've set the transition to be. And when I press cancel, the next time it will set transition to be their other one.
As you can see in the GIF, it cycles through the transitions, but I want them to be set for each button.
Expected behaviour:
When pressing X, I want it to fade out using .opacity.
When pressing Cancel, I want the X button to "slide" out, using .move as removal transition.
import Combine
import Foundation
import SwiftUI
struct SearchBar: View {
@Binding var searchField: String
let clearSearchField: () -> Void
let isFocus: Bool
let setFocus: (Bool) -> Void
@State var transition: AnyTransition = .opacity.animation(.default)
var body: some View {
HStack(spacing: 16) {
ZStack {
Color.gray
VStack(alignment: .leading) {
HStack {
TextField(
"Placeholder",
text: $searchField,
onEditingChanged: { _ in setFocus(true) }
)
.foregroundColor(.white)
Spacer()
if searchField.isNonEmpty {
Button {
transition = .opacity.animation(.default)
withAnimation {
clearSearchField()
}
} label: {
Text("X")
}
.transition(
.asymmetric(
insertion: .opacity.animation(.default),
removal: transition
)
)
}
}
}
.padding(16)
}
.cornerRadius(8)
if isFocus {
Button(
action: {
transition = .move(edge: .trailing).combined(with: .opacity)
hideKeyboard()
clearSearchField()
setFocus(false)
},
label: {
Text("Cancel")
.foregroundColor(.white)
}
)
.transition(.move(edge: .trailing).combined(with: .opacity).animation(.default))
}
}
.frame(height: 48)
.foregroundColor(searchField.isEmpty ? .grey: .white)
.padding(16)
.animation(.default, value: [isFocus])
}
}
Is this possible?
Appreciate any help!
From what I have observed, you cannot change the transition of a view without also changing its identity. Once the view has appeared with a transition, you can't tell it to disappear with another transition.
So one way to work around this is to have two X buttons, one with .opacity
transition, the other with .move
. Use a @State
to decide which button to show:
Example:
@State var showButtons = false
@State var shouldMove = false
var body: some View {
VStack {
// imagine this is your text field
Rectangle().frame(width: 100, height: 100).onTapGesture {
withAnimation {
showButtons = true
}
}
Spacer()
if showButtons {
// make buttons with different transitions
// if shouldMove, show the button with the move transition
// otherwise show the button with the opacity transition
if shouldMove {
makeButton(transition: .asymmetric(insertion: .opacity, removal: .move(edge: .trailing)))
} else {
makeButton(transition: .opacity)
}
Button("Cancel") {
shouldMove = true
withAnimation {
showButtons.toggle()
}
}
}
}.frame(height: 300)
}
@ViewBuilder func makeButton(transition: AnyTransition) -> some View {
Button("X") {
shouldMove = false
withAnimation {
showButtons.toggle()
}
}.transition(transition)
}
Another approach is to give the X button a new id
each time you want to change transitions, hence making a "new" view.
@State var showButtons = false
@State var transition: AnyTransition = .opacity
@State var buttonId = UUID()
var body: some View {
VStack {
Rectangle().frame(width: 100, height: 100).onTapGesture {
withAnimation {
showButtons = true
}
}
Spacer()
if showButtons {
Button("X") {
transition = .opacity
buttonId = UUID() // new id!
withAnimation {
showButtons.toggle()
}
}
.transition(transition)
.id(buttonId)
Button("Cancel") {
transition = .asymmetric(insertion: .opacity, removal: .move(edge: .trailing))
buttonId = UUID() // new id!
withAnimation {
showButtons.toggle()
}
}
}
}.frame(height: 300)
}