Conditional animation in SwiftUI stops working - swift

I have this code:
Group{
if self.done{
Image(systemName: "plus").font(.system(size: 40)).foregroundColor(.gray)
.padding().overlay(Circle().stroke(Color.gray, lineWidth: 3)).opacity(0.6)
}
else{
Button(action: {self.showSearchTrash = 1}){
Image(systemName: "plus").font(.system(size: 40)).foregroundColor(green)
.padding().overlay(Circle().stroke(green, lineWidth: 3).scaleEffect(1+self.animationAmount).animation(Animation.easeInOut(duration: 1).repeatForever(autoreverses: true)).onAppear {self.animationAmount = 0.1})
}
}
}.padding(.bottom, 5)
And the intention is that if self.done is false, the circle on the plus button will expand and contract indefinitely.
This works. However, if I use a toggle to set self.done to be true and then turn it back to false, the animation no longer occurs. I know that the issue is not with the toggle, because it does return to being green.
Also, the lack of . before green is intentional - I defined a specific Color green.
Any idea why the animation stops working/how to fix this?

Actually works fine as-is with Xcode 12.1 / iOS 14.1, so you might observe either bug of new iOS version or result of some other code.
Anyway, I would added turn-offs scaling on button disappear:
Button(action: {}){
Image(systemName: "plus").font(.system(size: 40)).foregroundColor(green)
.padding().overlay(Circle().stroke(green, lineWidth: 3).scaleEffect(1+self.animationAmount).animation(Animation.easeInOut(duration: 1).repeatForever(autoreverses: true)))
}
.onAppear {self.animationAmount = 0.1} // << put here !!
.onDisappear {self.animationAmount = 0} // << add this !!

You can specify Animation in the withAnimation block and create separate functions for starting/stopping the animation.
Here is a possible solution:
struct ContentView: View {
#State private var done = false
#State private var animationAmount: CGFloat = 0
var body: some View {
VStack {
Toggle("Done", isOn: $done)
plusImage
.opacity(done ? 0.6 : 1)
.foregroundColor(done ? .gray : .green)
}
.onAppear(perform: startAnimation)
.onChange(of: done) { done in
if done {
stopAnimation()
} else {
startAnimation()
}
}
}
var plusImage: some View {
Image(systemName: "plus")
.font(.system(size: 40))
.padding()
.overlay(
Circle()
.stroke(Color.gray, lineWidth: 3)
.scaleEffect(1 + animationAmount)
)
}
}
private extension ContentView {
func startAnimation() {
withAnimation(Animation.easeInOut(duration: 1).repeatForever()) {
animationAmount = 0.1
}
}
func stopAnimation() {
withAnimation {
animationAmount = 0
}
}
}

Related

Delay transition of a button

I'm attempting to transition a button in a SwiftUI project such that, when the button is pressed, it will do a .move(edge: .trailing) and after approximately a half a second a different image for the button will come into view.
Here's what I have so far and as expected the image changes simultaneously. I'm curious if this is something that can be accomplished with asymmetric transitions. Trying to avoid using two different buttons that have animating offsets changing.
#State var delayedMove = false
#ViewBuilder
var moveMe: some View {
HStack {
Spacer()
Button(action: {
withAnimation {
delayedMove.toggle()
}
}) {
if delayedMove {
selectImageButton
.animation(.linear.delay(delayedMove ? 0 : 1))
.transition(.move(edge: .trailing))
} else {
deleteImage
.animation(.linear.delay(!delayedMove ? 0 : 1))
.transition(.move(edge: .trailing))
}
}
.padding()
}
}
Yes you can use asymmetric, and no need to repeat the code. But Transition in SwiftUI needs 2 things to works correctly first a Group and Second an if & else, it is how it works right now maybe in future it changes but you can see the Code:
struct ContentView: View {
#State private var toggleButton = false
var body: some View {
HStack {
Button("Toggle Button") { toggleButton.toggle() }
Spacer()
Button(action: { toggleButton.toggle() }, label: {
Group {
if toggleButton {
Image(systemName: "trash")
}
else {
Image(systemName: "plus")
}
}
.transition(AnyTransition.asymmetric(insertion: AnyTransition.move(edge: .trailing), removal: AnyTransition.move(edge: .trailing)))
})
}
.padding()
.animation(Animation.linear(duration: 0.5), value: toggleButton)
}
}
You are quite close. I just split up the single if-else into 2 ifs, wrapped in a ZStack.
The button also makes secondaryDelay equal true for half a second, and then it switches back to false.
When the view leaves the hierarchy, that's when the transition starts. I worked out the way to do this by considering all the scenarios for the selectImageButton:
Showing - show: delayedMove = true, secondaryDelay = false
Starting to hide - hide: delayedMove = false, secondaryDelay = true
Hiding - hide: delayedMove = false, secondaryDelay = false
Starting to show - hide: delayedMove = true, secondaryDelay = true
From above, we only need to show when delayedMove && !secondaryDelay equals true. Similar method for other deleteImage.
Code:
struct ContentView: View {
#State var delayedMove = false
#State var secondaryDelay = false
var body: some View {
moveMe
}
#ViewBuilder
var moveMe: some View {
HStack {
Spacer()
Button(action: {
withAnimation {
delayedMove.toggle()
secondaryDelay = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
secondaryDelay = false
}
}
}) {
ZStack {
if delayedMove && !secondaryDelay {
selectImageButton
.animation(.linear)
.transition(.move(edge: .trailing))
}
if !delayedMove && !secondaryDelay {
deleteImage
.animation(.linear)
.transition(.move(edge: .trailing))
}
}
}
.padding()
}
}
}
Result:
You can still use the same button and have the label animated with an offset if you want a clean and smooth animation like the first image going out and the other entering. you can try this view
#State var delayedMove = false
var moveMe: some View {
HStack {
Spacer()
Button(action: {
withAnimation (.linear(duration: 0.5)){
delayedMove.toggle()
}
}) {
ZStack{
Image(systemName: "trash")
.offset(x: delayedMove ? 0 : 200)
Image(systemName: "plus")
.offset(x: delayedMove ? 200 : 0)
}
.imageScale(.large)
}
.padding()
}
}

SwiftUI Swipe/Drag over NavigationLink

I want to be able to update a view with a swipe left/right over a NavigationView that has multiple NavigationLinks in it. The NavigationLinks go to different presentation views.
When I begin the Drag gesture on the outer most view, if that gesture begins over a NavigationLink, the link changes color as though it has been pressed. Continuing the drag gesture, the main view does update as expected and the NavigationLink returns to it's normal state (color).
What I need is a way to have the NavigationLink NOT change color when it is a drag gesture that is occuring. Maybe a way to have the NavigationLink react "if" the touch is a long touch or something.
Here is some code that demonstrates what I am seeing. This is not my actual project, but a very stripped down example.
Any suggestions or solutions appreciated!
import SwiftUI
struct ContentView: View {
#State var outputText: String = ""
var body: some View {
VStack(alignment: .center, spacing: 20) {
NavigationView {
VStack {
Text("Navigation View")
NavigationLink(destination: Text("Showwing Widget")) {
HStack {
Text("Navigation Link")
}
.frame(width: 300, height: 200)
.border(Color.blue)
}
.border(Color.red)
Spacer()
}
.border(Color.yellow)
}
Text(outputText)
.font(.title)
.fontWeight(.bold)
Text("My Green Oval")
.foregroundColor(.white)
.fontWeight(.bold)
.font(.title)
.frame(width: 300, height: 200)
.background(
Ellipse()
.fill(Color.green)
)
Button(action: {
outputText = "Button tapped"
}) {
Text("Button to Tap")
}
Text("Just some words...")
Spacer()
}
.highPriorityGesture(DragGesture(minimumDistance: 25, coordinateSpace: .local)
.onEnded { value in
if abs(value.translation.height) < abs(value.translation.width) {
if abs(value.translation.width) > 50.0 {
if value.translation.width < 0 {
self.swipeRightToLeft()
} else if value.translation.width > 0 {
self.swipeLeftToRight()
}
}
}
}
)
}
func swipeRightToLeft() {
outputText = "Swiped Right to Left <--"
}
func swipeLeftToRight() {
outputText = "Swiped Left to Right -->"
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
After toying around forever I have a solution here! Hacky maybe, but it works with little effort. Configure your view with an offset that is conditional on whether your drawer row is open or not and also create a state variable to keep track on whether or not your user is dragging. You can get the screenWidth by using UIScreen.main.bounds.width.
.offset(x: self.isOpen ? -screenWidth/12 : 0, y: 0)
.simultaneousGesture(DragGesture()
.onChanged{ gesture in
self.isDragging = true
self.offset = gesture.translation.width
}
.onEnded { _ in
self.isDragging = false
if self.offset > 0 {
withAnimation {
self.isOpen = false
self.offset = 0
}
} else if self.offset < 0 {
withAnimation {
self.isOpen = true
self.offset = 0
}
}
})
Then add this disable modifier on your NavigationLink
.disabled(self.isDragging || self.isOpen)
Good luck! Hopefully this works for you as well if you haven't found a solution.

How to get bounce animation in SwiftUI?

How do I achieve the bounce animation similar to the one on macOS dock when an application needs attention and it bounces on the dock. SwiftUI seems to only have ease-curves and spring, which don't really emphasize the way a bounce does.
I've attempted various spring animations and combinations of ease curves and timingCurves to try and get the bounce animation working but nothing really did the job.
The closest to a bounce animation is interpolating spring but the main problem with these animations is that they overshoot during the animation, whereas bounce animations don't.
struct ContentView: View {
#State var bounce = false
#State private var initialVelocity:Double = 1
#State private var damping:Double = 1
#State private var stiffness:Double = 1
var body: some View {
VStack {
Circle().fill(Color.red).frame(width:50,height:50)
.offset(y: bounce ? 0 : -80)
.animation(.interpolatingSpring(stiffness: self.stiffness, damping: self.damping, initialVelocity: self.initialVelocity))
HStack(){
Text("stiffness")
Slider(value: $stiffness, in: 0...100)
}
HStack(){
Text("damping")
Slider(value: $damping, in: 0...100)
}
HStack(){
Text("initialVelocity")
Slider(value: $initialVelocity, in: 0...100)
}
Button("Animate" ){
self.bounce.toggle()
}
}.padding(.horizontal)
}
}
What I'm looking for is a bounce animation that replicates gravity, it's a pretty common animation that is available in a lot of games and software
What's about .interpolatingSpring(...)?
Consider the following example but keep in mind, that you might have to play around with the values for stiffness, etc.:
struct ContentView: View {
#State var test = false
var body: some View {
VStack {
Text("Animatable")
.offset(y: test ? 0 : -80)
.animation(.interpolatingSpring(stiffness: 350, damping: 5, initialVelocity: 10))
Button(action: {self.test.toggle()}) {
Text("Animate")
}
}
}
}
Using multiple offsets, delays and easing I was able to get fairly close to replicating that specific animation.
struct ContentView: View {
#State var bounceHeight: BounceHeight? = nil
func bounceAnimation() {
withAnimation(Animation.easeOut(duration: 0.3).delay(0)) {
bounceHeight = .up100
}
withAnimation(Animation.easeInOut(duration: 0.04).delay(0)) {
bounceHeight = .up100
}
withAnimation(Animation.easeIn(duration: 0.3).delay(0.34)) {
bounceHeight = .base
}
withAnimation(Animation.easeOut(duration: 0.2).delay(0.64)) {
bounceHeight = .up40
}
withAnimation(Animation.easeIn(duration: 0.2).delay(0.84)) {
bounceHeight = .base
}
withAnimation(Animation.easeOut(duration: 0.1).delay(1.04)) {
bounceHeight = .up10
}
withAnimation(Animation.easeIn(duration: 0.1).delay(1.14)) {
bounceHeight = .none
}
}
var body: some View {
VStack {
Text("☝️")
.font(.system(size: 200))
.multilineTextAlignment(.center)
.minimumScaleFactor(0.2)
.lineLimit(1)
}
.padding(8)
.frame(width: 72, height: 72)
.background(.purple.opacity(0.4))
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
.shadow(radius: 3)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(.purple, lineWidth: 2)
)
.offset(y: bounceHeight?.associatedOffset ?? 0)
.onTapGesture {
bounceAnimation()
}
}
}
enum BounceHeight {
case up100, up40, up10, base
var associatedOffset: Double {
switch self {
case .up100:
return -100
case .up40:
return -40
case .up10:
return -10
case .base:
return 0
}
}
}

SwiftUI inverse animation delay on removal

I am trying to choreograph animations in SwiftUI. I want two colored bars to move in to the view with a delay between them and then move out of the view with the delays switched so that the removal is the reverse of the insertion.
I think I understand why the below code doesn't work, but I can't figure out how to make it do what I need:
import SwiftUI
struct TestAnimControl: View {
#State var show: Bool = false
#State var reverseDelay: Bool = false
var body: some View {
VStack {
Button(action:{
self.show.toggle()
}) {
Text("Animate")
.font(.largeTitle)
}
if show {
Rectangle()
.foregroundColor(.blue)
.frame(height: 100)
.transition(.move(edge: .trailing))
.animation(Animation.spring().delay(show ? 0.3 : 0.5))
Rectangle()
.foregroundColor(.red)
.frame(height: 100)
.transition(.move(edge: .trailing))
.animation(Animation.spring().delay(show ? 0.5 : 0.3))
}
}
}
}
When you run this and hit the button, the blue bar moves in and then the red bar moves in. Hit the button again and the blue bar moves out and then the red bar moves out. What I want is when you hit the button for removal, the red bar moves out and then the blue bar moves out, reverse of the way the bars came in. In this code I believe the ternary doesn't work because the animation is set when the Rectangle is created and the delay can't change after that. I may be wrong, but either way is there a way to do what I am trying to do?
Update: Xcode 13.4 / iOS 15.5
A proposed solution now is based on explicit animation with modification in every transaction so each transition have own parametrised variant of animation.
Main part:
Button(action:{
withAnimation {
self.show.toggle()
}
}) {
Text("Animate")
.font(.largeTitle)
}
if show {
Rectangle()
.foregroundColor(.blue)
.frame(height: 100)
.transition(.move(edge: .trailing))
.transaction {
$0.animation = Animation.spring().delay(delay1)
}
.onAppear { self.delay1 = 0.5 }
.onDisappear { self.delay1 = 0.3 }
Rectangle()
.foregroundColor(.yellow)
.frame(height: 100)
.transition(.move(edge: .trailing))
.transaction {
$0.animation = Animation.spring().delay(delay2)
}
.onAppear { self.delay2 = 0.3 }
.onDisappear { self.delay2 = 0.5 }
}
Test code on GitHub
Original:
!!! It does not work anymore with modern OS
Here is a solution - based on applied implicit animations to every transition. Tested with Xcode 11.4 / iOS 13.4
struct TestAnimControl: View {
#State var show: Bool = false
#State var reverseDelay: Bool = false
#State var blueDelay = 0.3
#State var redDelay = 0.5
var body: some View {
VStack {
Button(action:{
self.show.toggle()
}) {
Text("Animate")
.font(.largeTitle)
}
if show {
Rectangle()
.foregroundColor(.blue)
.frame(height: 100)
.transition(.move(edge: .trailing))
.animation(Animation.spring().delay(blueDelay))//(show ? 0.3 : 0.5))
.onAppear { self.blueDelay = 0.5 }
.onDisappear { self.blueDelay = 0.3 }
Rectangle()
.foregroundColor(.red)
.frame(height: 100)
.transition(.move(edge: .trailing))
.animation(Animation.spring().delay(redDelay))//(show ? 0.5 : 0.3))
.onAppear { self.redDelay = 0.3 }
.onDisappear { self.redDelay = 0.5 }
}
}
}
}
You cannot change delay value according to state value because Animation struct is marked as #frozen that has no effect on property observers - state properties (check it out https://docs.swift.org/swift-book/ReferenceManual/Attributes.html). The proper way to do is using DispatchQueue.main.asyncAfter(deadline:_:) to specify each delay. Here is my sample code...
struct ContentView: View {
#State var blue = false
#State var red = false
var body: some View {
VStack {
Button(action:{
let redDelay = self.red ? 0.3 : 0.5
DispatchQueue.main.asyncAfter(deadline: .now() + redDelay) {
self.red.toggle()
}
let blueDelay = self.blue ? 0.5 : 0.3
DispatchQueue.main.asyncAfter(deadline: .now() + blueDelay) {
self.blue.toggle()
}
}) {
Text("Animate")
.font(.largeTitle)
}
if blue {
Rectangle()
.foregroundColor(.blue)
.frame(height: 100)
.transition(AnyTransition.move(edge: .trailing))
.animation(Animation.spring())
}
if red {
Rectangle()
.foregroundColor(.red)
.frame(height: 100)
.transition(AnyTransition.move(edge: .trailing))
.animation(Animation.spring())
}
}
.animation(.spring())
}
}
Thanks. X_X

SwiftUI Text animation on opacity does not work

Question is simple: how in the world do i get a Text to animate properly?
struct ContentView: View {
#State var foozle: String = ""
var body: some View {
VStack() {
Spacer()
Text(self.foozle)
.frame(maxWidth: .infinity)
.transition(.opacity)
Button(action: {
withAnimation(.easeInOut(duration: 2)) {
self.foozle = "uuuuuuuuu"
}
}) { Text("ugh") }
Spacer()
}.frame(width: 320, height: 240)
}
}
The problem: the view insists on doing some dumb animation where the text is replaced with the new text, but truncated with ellipses, and it slowly expands widthwise until the entirety of the new text is shown.
Naturally, this is not an animation on opacity. It's not a frame width problem, as I've verified with drawing the borders.
Is this just another dumb bug in SwiftUI that i'm going to have to deal with, and pray that someone fixes it?
EDIT: ok, so thanks to #Mac3n, i got this inspiration, which works correctly, even if it's a little ugly:
Text(self.foozle)
.frame(maxWidth: .infinity)
.opacity(op)
Button(action: {
withAnimation(.easeOut(duration: 0.3)) {
self.op = 0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.foozle += "omo"
withAnimation(.easeIn(duration: 0.3)) {
self.op = 1
}
}
}
}) {
Text("ugh")
}
The problem is that SwiftUI sees Text view as the same view. You can use the .id() method on the view to set it. In this case I've just set the value to a hash of the text itself, so if you change the text, the entire view will get replaced.
struct ContentView: View {
#State var foozle: String = ""
var body: some View {
VStack() {
Spacer()
Text(self.foozle)
.id(self.foozle.hashValue)
.frame(maxWidth: .infinity)
.transition(.opacity)
Button(action: {
withAnimation(.easeInOut(duration: 2)) {
self.foozle = "uuuuuuuuu"
}
}) { Text("ugh") }
Spacer()
}.frame(width: 320, height: 240)
}
}
Transition works when view appeared/disappeared. In your use-case there is no such workflow.
Here is a demo of possible approach to hide/unhide text with opacity animation:
struct DemoTextOpacity: View {
var foozle: String = "uuuuuuuuu"
#State private var hidden = true
var body: some View {
VStack() {
Spacer()
Text(self.foozle)
.frame(maxWidth: .infinity)
.opacity(hidden ? 0 : 1)
Button(action: {
withAnimation(.easeInOut(duration: 2)) {
self.hidden.toggle()
}
}) { Text("ugh") }
Spacer()
}.frame(width: 320, height: 240)
}
}
If you want to animate on opacity you need to change opacity value on your text element.
code example:
#State private var textValue: String = "Sample Data"
#State private var opacity: Double = 1
var body: some View {
VStack{
Text("\(textValue)")
.opacity(opacity)
Button("Next") {
withAnimation(.easeInOut(duration: 0.5), {
self.opacity = 0
})
self.textValue = "uuuuuuuuuuuuuuu"
withAnimation(.easeInOut(duration: 1), {
self.opacity = 1
})
}
}
}