Conditional Animation don't stop - animation

I have a Button with a scaleEffect(bool1 ? bool2 ? 1.2 : 1 : 1).
If bool1 is false it should be a scaleEffect of 1.
bool2 is getting toggled withAnimation(.easeInOut(duration: 0.5).repeatForever).
So if bool1 is true it should scale between 2 values and if bool1 is false it should stop but it doesn't.
What is the problem?
Code:
struct FullAnimationView: View {
#State var bool1 = false
#State var bool2 = false
var body: some View {
Button {
bool1.toggle()
} label: {
Text(String(bool1))
.frame(width: 60, height: 30)
.background(RoundedRectangle(cornerRadius: 6).stroke(bool1 ? bool2 ? Color.red : Color.orange : Color.orange))
}
.background(bool1 ? bool2 ? .red.opacity(0.3) : .orange.opacity(0.3) : .orange.opacity(0.3))
.cornerRadius(6)
.scaleEffect(bool1 ? bool2 ? 1.2 : 1 : 1)
.onAppear {
withAnimation(.easeInOut(duration: 0.5).repeatForever()) {
bool2.toggle()
}
bool1.toggle()
}
}
}

Your view is animating from the start. You need to do something to explicitly stop the animation. One way to do that is to replace your view (that is, your button) with a new one that isn't animating.
Create a buttonID as a UUID (universally unique identifier) and use it with .id(buttonID) on your button. When you change buttonID, SwiftUI will create a new view for you.
When the button is pressed, if bool1 is true, start a new animation, just as you did when the view first appeared. If bool1 is false, assign a new buttonID to create a new view to stop the animation. Also set bool2 to false so that it is ready to start the new animation.
struct FullAnimationView: View {
#State private var bool1 = false
#State private var bool2 = false
#State private var buttonID = UUID()
var body: some View {
Button {
bool1.toggle()
if bool1 {
withAnimation(.easeInOut(duration: 0.5).repeatForever()) {
bool2.toggle()
}
} else {
bool2 = false
buttonID = UUID()
}
} label: {
Text(String(bool1))
.frame(width: 60, height: 30)
.background(RoundedRectangle(cornerRadius: 6).stroke(bool1 ? bool2 ? Color.red : Color.orange : Color.orange))
}
.background(bool1 ? bool2 ? .red.opacity(0.3) : .orange.opacity(0.3) : .orange.opacity(0.3))
.cornerRadius(6)
.scaleEffect(bool1 ? bool2 ? 1.2 : 1 : 1)
.id(buttonID)
.onAppear {
bool1.toggle()
withAnimation(.easeInOut(duration: 0.5).repeatForever()) {
bool2.toggle()
}
}
}
}

Related

How to fix Scrolling while pressing an object that shrinks when pressing - SwiftUI

I've been trying to add a shrinking affect on buttons while they're being pressed, and I was able to establish it with with LongPressGesture
But at the same time, I noticed that this also stops the container from being scrolled when the box is pressed to start scrolling, for example, I can scroll when I press an empty space first to start scrolling, but I can't scroll when I press the box first to start scrolling.
I'm using Xcode 14.0.1 on macOS Monterey 12.6 and I tested the code both in virtual and physical phones running on iOS 16.
And here's a simplified version of my code,
struct TestView: View {
#GestureState var isDetectingLongPress = false
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 25) {
RoundedRectangle(cornerRadius: 12.5, style: .continuous)
.fill(Color.blue)
.frame(height: 200)
.padding(25)
.scaleEffect(!isDetectingLongPress ? 1.0 : 0.875)
.brightness(!isDetectingLongPress ? 0.0 : -0.125)
.animation(.easeInOut(duration: 0.125), value: isDetectingLongPress)
.gesture(
LongPressGesture(minimumDuration: 3)
.updating($isDetectingLongPress) { currentState, gestureState,
transaction in
gestureState = currentState
transaction.animation = Animation.easeIn(duration: 2.0)
}
.onEnded { finished in
}
)
}
}
}
}
How can I fix this? Thanks!
Approach 1:
Add a modifier that delays the recognition of the tap gesture. If a user starts their scroll on the button, the user will still be able to scroll. The downside to this is if the user wants to tap the button, it will be slightly delayed.
This approach was found here
One thing you'll notice is that I moved the RoundedRectange you created into its own view. Hope this makes things easier to read!
import SwiftUI
struct ScrollTest: View {
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 25) {
AnimatedButtonView()
}
}
}
}
struct AnimatedButtonView: View {
#GestureState var isDetectingLongPress = false
var body: some View {
RoundedRectangle(cornerRadius: 12.5, style: .continuous)
.fill(Color.blue)
.frame(height: 200)
.padding(25)
.scaleEffect(!isDetectingLongPress ? 1.0 : 0.875)
.brightness(!isDetectingLongPress ? 0.0 : -0.125)
.animation(.easeInOut(duration: 0.125), value: isDetectingLongPress)
.delaysTouches(for: 0.1) {
//some code here, if needed
}
.gesture(
LongPressGesture(minimumDuration: 3)
.updating($isDetectingLongPress) { currentState, gestureState,
transaction in
gestureState = currentState
transaction.animation = Animation.easeIn(duration: 2.0)
}
.onEnded { finished in
print("pooop")
})
}
}
extension View {
func delaysTouches(for duration: TimeInterval = 0.25, onTap action: #escaping () -> Void = {}) -> some View {
modifier(DelaysTouches(duration: duration, action: action))
}
}
fileprivate struct DelaysTouches: ViewModifier {
#State private var disabled = false
#State private var touchDownDate: Date? = nil
var duration: TimeInterval
var action: () -> Void
func body(content: Content) -> some View {
Button(action: action) {
content
}
.buttonStyle(DelaysTouchesButtonStyle(disabled: $disabled, duration: duration, touchDownDate: $touchDownDate))
.disabled(disabled)
}
}
fileprivate struct DelaysTouchesButtonStyle: ButtonStyle {
#Binding var disabled: Bool
var duration: TimeInterval
#Binding var touchDownDate: Date?
func makeBody(configuration: Configuration) -> some View {
configuration.label
.onChange(of: configuration.isPressed, perform: handleIsPressed)
}
private func handleIsPressed(isPressed: Bool) {
if isPressed {
let date = Date()
touchDownDate = date
DispatchQueue.main.asyncAfter(deadline: .now() + max(duration, 0)) {
if date == touchDownDate {
disabled = true
DispatchQueue.main.async {
disabled = false
}
}
}
} else {
touchDownDate = nil
disabled = false
}
}
}
Approach 2:
Similarly hacky to approach 1. Add a .onTapGesture before .gesture. Downside is that it adds a delay and I am not sure if that is behavior you are looking for:
import SwiftUI
struct ScrollTest: View {
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 25) {
AnimatedButtonView()
}
}
}
}
struct AnimatedButtonView: View {
#GestureState var isDetectingLongPress = false
var body: some View {
RoundedRectangle(cornerRadius: 12.5, style: .continuous)
.fill(Color.blue)
.frame(height: 200)
.padding(25)
.scaleEffect(!isDetectingLongPress ? 1.0 : 0.875)
.brightness(!isDetectingLongPress ? 0.0 : -0.125)
.animation(.easeInOut(duration: 0.125), value: isDetectingLongPress)
.onTapGesture {}
.gesture(
LongPressGesture(minimumDuration: 3)
.updating($isDetectingLongPress) { currentState, gestureState,
transaction in
gestureState = currentState
transaction.animation = Animation.easeIn(duration: 2.0)
}
.onEnded { finished in
print("pooop")
})
}
}

SwiftUI animation not continuous after view change

Here is the testing code:
import SwiftUI
struct ContentView: View {
#State private var pad: Bool = false
#State private var showDot: Bool = true
var body: some View {
VStack {
Button {showDot.toggle()} label: {Text("Toggle Show Dot")}
Spacer().frame(height: pad ? 100 : 10)
Circ(showDot: showDot)
Spacer()
}.onAppear {
withAnimation(.linear(duration: 3).repeatForever()) {pad = true}
}
}
}
struct Circ: View {
let showDot: Bool
var body: some View {
Circle().stroke().frame(height: 50).overlay {if showDot {Circle().frame(height: 20)}}
}
}
It happens that after I toggle showDot, the dot circle is not on the center of the stroke circle again! How can I fix this?
The Circ View is given, I can't change that view!
Edit
If you can just hide the view, see solution 1, which is preferred. If you need to re-build the view, see solution 2.
Solution 1
Replace the if condition with a .opacity() modifier that reads 1 when showDot is true.
This way, the dot does not disappear completely, it is there but you just can't see it. You will be toggling the visibility, not the view itself.
Like this:
#State private var pad: Bool = false
#State private var showDot: Bool = true
var body: some View {
VStack {
Button {
showDot.toggle()
} label: {
Text("Toggle Show Dot")
}
Spacer()
.frame(height: pad ? 100 : 10)
Circle().stroke()
.frame(height: 50)
.overlay {
Circle()
.frame(height: 20)
.opacity(showDot ? 1 : 0) // <- Here
}
Spacer()
}
.onAppear {
withAnimation(.linear(duration: 3).repeatForever()) {pad = true}
}
}
Solution 2
You can replace the animation with a timer. Every time it triggers, it will move the whole view by changing the height of the Spacer().
// These variables will track the position and moving direction of the dot
#State private var pos: CGFloat = 0
#State private var movingUp = false
#State private var showDot: Bool = true
// This variable will change the position
// This is a dummy iniatialization, the .onAppear modifier sets the real timer
#State private var timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in }
var body: some View {
VStack {
Button {
showDot.toggle()
} label: {
Text("Toggle Show Dot")
}
Spacer()
.frame(height: pos)
Circle().stroke()
.frame(height: 50)
.overlay {
if showDot {
Circle().frame(height: 20)
}
}
Spacer()
}
.onAppear {
// The timer interval will define the speed
timer = Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true) { _ in
moveCircle()
}
}
}
private func moveCircle() {
if movingUp {
if pos <= 0 {
pos = 0
movingUp = false
} else {
pos -= 1
}
} else {
if pos >= 100 {
pos = 100
movingUp = true
} else {
pos += 1
}
}
}

swiftui, animation applied to parent effect child animation

RectangleView has a slide animation, his child TextView has a rotation animation. I suppose that RectangleView with his child(TextView) as a whole slide(easeInOut) into screen when Go! pressed, and TextView rotate(linear) forever. But in fact, the child TextView separates from his parent, rotating(linear) and sliding(linear), and repeats forever.
why animation applied to parent effect child animation?
struct AnimationTestView: View {
#State private var go = false
var body: some View {
VStack {
Button("Go!") {
go.toggle()
}
if go {
RectangleView()
.transition(.slide)
.animation(.easeInOut)
}
}.navigationTitle("Animation Test")
}
}
struct RectangleView: View {
var body: some View {
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(.pink)
.overlay(TextView())
}
}
struct TextView: View {
#State private var animationRotating: Bool = false
let animation = Animation.linear(duration: 3.0).repeatForever(autoreverses: false)
var body: some View {
Text("Test")
.foregroundColor(.blue)
.rotationEffect(.degrees(animationRotating ? 360 : 0))
.animation(animation)
.onAppear { animationRotating = true }
.onDisappear { animationRotating = false }
}
}
If there are several simultaneous animations the generic solution (in majority of cases) is to use explicit state value for each.
So here is a corrected code (tested with Xcode 12.1 / iOS 14.1, use Simulator or Device, Preview renders some transitions incorrectly)
struct AnimationTestView: View {
#State private var go = false
var body: some View {
VStack {
Button("Go!") {
go.toggle()
}
VStack { // container needed for correct transition !!
if go {
RectangleView()
.transition(.slide)
}
}.animation(.easeInOut, value: go) // << here !!
}.navigationTitle("Animation Test")
}
}
struct RectangleView: View {
var body: some View {
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(.pink)
.overlay(TextView())
}
}
struct TextView: View {
#State private var animationRotating: Bool = false
let animation = Animation.linear(duration: 3.0).repeatForever(autoreverses: false)
var body: some View {
Text("Test")
.foregroundColor(.blue)
.rotationEffect(.degrees(animationRotating ? 360 : 0))
.animation(animation, value: animationRotating) // << here !!
.onAppear { animationRotating = true }
.onDisappear { animationRotating = false }
}
}

swiftui, animation applied to parent effect child animation(Part II)

PREVIOUS Q: swiftui, animation applied to parent effect child animation
Now the TextView has its own state. RectangleView with TextView slide into screen in 3 seconds, but state of TextView changed after a second while sliding. Now you can see TextView stops sliding, go to the destination at once. I Just want RectangleView with TextView as a whole when sliding, and TextView rotates or keeps still as I want.
import SwiftUI
struct RotationEnvironmentKey: EnvironmentKey {
static let defaultValue: Bool = false
}
extension EnvironmentValues {
var rotation: Bool {
get { return self[RotationEnvironmentKey] }
set { self[RotationEnvironmentKey] = newValue }
}
}
struct AnimationTestView: View {
#State private var go = false
#State private var rotation = false
var body: some View {
VStack {
Button("Go!") {
go.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
rotation.toggle()
print("set rotation = \(rotation)")
}
}
Group {
if go {
RectangleView()
.transition(.slide)
.environment(\.rotation, rotation)
}
}.animation(.easeInOut(duration: 3.0), value: go)
}.navigationTitle("Animation Test")
}
}
struct RectangleView: View {
var body: some View {
Rectangle()
.frame(width: 200, height: 100)
.foregroundColor(.pink)
.overlay(TextView())
}
}
struct TextView: View {
#Environment(\.rotation) var rotation
#State private var animationRotating: Bool = false
let animation = Animation.linear(duration: 3.0).repeatForever(autoreverses: false)
var body: some View {
print("refresh, rotation = \(rotation)"); return
HStack {
Spacer()
if rotation {
Text("R")
.foregroundColor(.blue)
.rotationEffect(.degrees(animationRotating ? 360 : 0))
.animation(animation, value: animationRotating)
.onAppear { animationRotating = true }
.onDisappear { animationRotating = false }
} else {
Text("S")
}
}
}
}
I found the issue here: cannot use if cause in TextView, because when rotation changed, will create a new Text in new position, so we can see the R 'jump' to the right position.
code as following:
Text(rotation ? "R" : "S")
.foregroundColor(rotation ? .blue : .white)
.rotationEffect(.degrees(animationRotating ? 360 : 0))
.animation(rotation ? animation : nil, value: animationRotating)
.onChange(of: rotation) { newValue in
animationRotating = true
}

SwiftUI Animate Image on Button

Using SwiftUI, I'm trying to show an animated image and hide the text when the user clicks the button. Here is my code:
#State private var showingActivity = false
var body: some View {
Button(action: {
self.showingActivity.toggle()
}) {
HStack {
if self.showingActivity {
Image(systemName: "arrow.2.circlepath")
.font(.system(size: 29))
.rotationEffect(.degrees(self.showingActivity ? 360.0 : 0.0))
.animation(Animation.linear(duration: 1.5).repeatForever(autoreverses: false))
}
else {
Text("Continue")
}
}
}
}
The Continue text appears and when clicked it disappears (as expected) and the image shows, but no animation. Any ideas what I might be doing wrong.
Thanks
or try this:
struct ImageView : View {
#State private var showAction = false
var body: some View {
Image(systemName: "arrow.2.circlepath")
.font(.system(size: 29))
.rotationEffect(.degrees(self.showAction ? 360.0 : 0.0))
.animation(self.showAction ? Animation.linear(duration: 1.5).repeatForever(autoreverses: false) : nil)
.onAppear() {
self.showAction = true
}
}
}
struct ContentView: View {
#State private var showingActivity = false
var body: some View {
Button(action: {
self.showingActivity.toggle()
}) {
HStack {
if self.showingActivity {
ImageView()
}
else {
Text("Continue")
}
}
}
}
}
You can use .opacity instead of if...else statements, in this case rotation works on image. But in canvas I still see some glitches, if tap button several times but I didn't try on real device (behavior there can be other). Hope it'll help:
struct AnimatableView: View {
#State private var showingActivity = false
var body: some View {
Button(action: {
self.showingActivity.toggle()
}) {
ZStack {
Image(systemName: "arrow.2.circlepath")
.font(.system(size: 29))
.rotationEffect(.degrees(self.showingActivity ? 360.0 : 0.0))
.animation(Animation.linear(duration: 1.5).repeatForever(autoreverses: false))
.opacity(self.showingActivity ? 1 : 0)
Text("Continue")
.opacity(self.showingActivity ? 0 : 1)
}
}
}
}

Resources