Multiple animations do not work as expected in SwiftUI - ios

I want to create a pulsing circle with forever repeating animation. It should have a gradual change of size and a momentary change of color when animation direction changes.
I have two different animations for that, but only the last one having an effect on both (size and color change).
struct SwiftUIView: View {
#State private var animationStarted = false
let side = UIScreen.main.bounds.size.width
let animation1 = Animation.easeInOut(duration: 0.3).delay(5.7)
.repeatForever(autoreverses: true)
let animation2 = Animation.easeInOut(duration: 6)
.repeatForever(autoreverses: true)
var body: some View {
HStack {
VStack {
HStack {
Circle()
.foregroundColor(self.animationStarted ? Color.pink : Color.blue)
.animation(animation1)
.frame(width: self.animationStarted ? 10 : side, height: self.animationStarted ? 10 : side)
.animation(animation2)
.onAppear {
self.animationStarted.toggle()
}
}
}.frame(height: side)
}.frame(width: side)
}
}

Related

In SwiftUI, the ternary operator is ignored for animation duration

This is the code of my app:
struct ContentView: View {
#State private var animationAmount: CGFloat = 1
#State private var animate: Bool = true
var body: some View {
Button("Tap Mee") {
animate = !animate
// self.animationAmount += 1
print("yo, animate is", animate)
}
.padding(40)
.background(Color.red)
.foregroundColor(.white)
.clipShape(Circle())
.overlay(
Circle()
.stroke(Color.red)
.scaleEffect(animationAmount)
.opacity(Double(2 - animationAmount))
.animation(
Animation.easeOut(duration: animate ? 5 : 1)
.repeatForever(autoreverses: false)
)
)
.onAppear {
self.animationAmount = 2
}
}
}
I want to set animation to 1 or 5 depending on the click. On the click a property is changed (true or false) and I want to use that to control the duration, but it is ignored. How can I fix this, so on each click, the duration of the animation changes?

Scaled view unexpectedly moving up and down when pushed from navigation view

I have a view that should scale in and out, starting immediately when the view is shown and repeating forever. However, I find that it's actually animating up and down as well as scaling much like in this post when it's pushed from a navigation view:
struct PlaceholderView: View {
#State private var isAnimating = false
var body: some View {
Circle()
.frame(width: 30, height: 30)
.scaleEffect(self.isAnimating ? 0.8 : 1)
.animation(Animation.easeInOut(duration: 1).repeatForever())
.onAppear {
self.isAnimating = true
}
.frame(width: 50, height: 50)
.contentShape(
Rectangle()
)
}
}
struct SettingsView: View {
#State private var showPlaceholder = false
var body: some View {
NavigationView {
ZStack {
Button(
action: {
showPlaceholder = true
}, label: {
Text("Go to placeholder")
}
)
NavigationLink(
destination: PlaceholderView(),
isActive: $showPlaceholder
) {
EmptyView()
}
.hidden()
}
}
.navigationViewStyle(.stack)
}
}
Why is this and how can I stop this from happening?
UPDATE:
Wrapping self.isAnimating = true in DispatchQueue.main.async {} fixes the issue, but I don't understand why...
I can reproduce the problem, but I wasn't able to reproduce your DispatchQueue.main.async {} fix.
Here is how I fixed it:
Animation is made by comparing a before state and and after state and animating between them. With an implicit animation, the before state is the position of the circle before it is added to the navigation view. By switching to an explicit animation and setting your before state after the view appears, the move animation is avoided and you only get the scaling:
Replace isAnimating with scale. In onAppear {}, first establish the before scale of 0.8, then animate the change to scale 1:
struct PlaceholderView: View {
// To work correctly, this initial value should be different
// than the before value set in .onAppear {}.
#State private var scale = 1.0
var body: some View {
Circle()
.frame(width: 30, height: 30)
.scaleEffect(self.scale)
.onAppear {
self.scale = 0.8 // before
withAnimation(.easeInOut(duration: 1).repeatForever()) {
self.scale = 1 // after
}
}
.frame(width: 50, height: 50)
.contentShape(
Rectangle()
)
}
}

Animation conflict issue with another animation

I have this LoaderView which grow with given value, I want add some highlight effect to it as well, So basically there is 2 deferent and separate animation happening, one for activeValue and one for the highlight. i did not combine them together because they are 2 deferent things, but my output of codes combine these 2 values together and destroys highlight effect. I am looking a solid answer for my issue to having this 2 animation works side by side.
The issue happens when i add value.
I want this view be usable in macOS as well, so there is a possibility user changes the Window size, then my view would be changed as well.
struct ContentView: View {
#State private var value: CGFloat = 50
var body: some View {
LoaderView(activeValue: value, totalValue: 100)
.padding()
Button("add") { value += 10 }
.padding()
}
}
struct LoaderView: View {
let activeValue: CGFloat
let totalValue: CGFloat
let activeColor: Color
let totalColor: Color
init(activeValue: CGFloat, totalValue: CGFloat, activeColor: Color = Color.green, totalColor: Color = Color.secondary) {
if activeValue >= totalValue { self.activeValue = totalValue }
else if activeValue < 0 { self.activeValue = 0 }
else { self.activeValue = activeValue }
self.totalValue = totalValue
self.activeColor = activeColor
self.totalColor = totalColor
}
#State private var startAnimation: Bool = .init()
private let heightOfCapsule: CGFloat = 5.0
var body: some View {
let linearGradient = LinearGradient(colors: [Color.yellow.opacity(0.1), Color.yellow], startPoint: .leading, endPoint: .trailing)
return GeometryReader { proxy in
totalColor
.overlay(
Capsule()
.fill(activeColor)
.frame(width: (activeValue / totalValue) * proxy.size.width)
.overlay(
Capsule()
.fill(linearGradient)
.frame(width: 100)
.offset(x: startAnimation ? 100 : -100),
alignment: startAnimation ? Alignment.trailing : Alignment.leading)
.clipShape(Capsule()),
alignment: Alignment.leading)
.clipShape(Capsule())
}
.frame(height: heightOfCapsule)
.onAppear(perform: { startAnimation.toggle() })
.animation(Animation.default, value: activeValue)
.animation(Animation.linear(duration: 3.0).repeatForever(autoreverses: false), value: startAnimation)
}
}
The issue isn't the .animations it is that .onAppear is only called once, so your animation never restarts. startAnimation needs to be toggled when activeValue changes. The way your animation works, toggling startAnimation reverses the animation direction. Therefore, I set autoreverses: true. It is a compromise either way.
struct LoaderView: View {
//Nothing changed in this portion
.onAppear{
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
startAnimation.toggle()
}
}
// The issue is that .onAppear() is only called once.
.onChange(of: activeValue, perform: { _ in
startAnimation.toggle()
})
.animation(Animation.default, value: activeValue)
// As a side effect, changing startAnimation to opposite values reverses the animation
// you have made, so setting autoreverses: true hides this.
.animation(Animation.linear(duration: 3.0).repeatForever(autoreverses: true), value: startAnimation)
}
}
Edit:
I apologize for not getting back to this sooner. The issue with the rubber band animation is because of the transition when the app firsts launches. To prevent this, you need to wrap your variable change in .onAppear() in a DispatchQueue.main.asyncAfter(deadline:) with enough time for the view to load. Then when you start the animation, it works correctly.

Why does my SwiftUI animation revert to the previous frame on rotation?

I have a SwiftUI View that is meant to be sort of a "warning" background that flashes on and off. It appears inside another View with a size that changes depending on device orientation. Everything works properly upon the first appearance, but if the device is rotated while the warning view is active it incorporates the frame of the previous orientation into the animation, making it not only flash on and off, but also move in and out of the view its positioned in, as well as change to and from the size it was on the previous orientation. Why is this happening and how can I fix it? Generic code:
struct PulsatingWarningBackground: View {
let color : Color
let speed : Double
#State private var opacity : Double = 1
var body: some View {
GeometryReader {geometry in
self.color
.opacity(self.opacity)
.animation(
Animation
.linear(duration: self.speed)
.repeatForever(autoreverses: true)
)
.onAppear(perform: {self.opacity = 0})
}
}
}
struct PulsatingWarningViewHolder: View {
var body: some View {
GeometryReader {geometry in
ZStack {
Color.white
ZStack {
Color.black
PulsatingWarningBackground(color: .red, speed: 1/4)
}.frame(width: geometry.frame(in: .local).width/5, height: geometry.frame(in: .local).height/10, alignment: .center)
}
}
}
}
You can apply animation only to opacity by using withAnimation(_:_:) inside onAppear(perform:). It works properly as you want.
struct PulsatingWarningBackground: View {
let color : Color
let speed : Double
#State private var opacity : Double = 1
var body: some View {
self.color
.opacity(self.opacity)
.onAppear(perform: {
withAnimation(Animation.linear(duration: self.speed).repeatForever()) {
self.opacity = 0
}
})
}
}
struct PulsatingWarningViewHolder: View {
var body: some View {
GeometryReader {geometry in
ZStack {
Color.white
ZStack {
Color.black
PulsatingWarningBackground(color: .red, speed: 1/4)
}
.frame(width: geometry.frame(in: .local).width/5, height: geometry.frame(in: .local).height/10, alignment: .center)
}
}
}
}

Anchor point in SwiftUI

I'm playing with SwiftUI animations. I wanted to make a square bigger modifying its frame with an animation this way:
struct ContentView: View {
let squareWidth = CGFloat(100)
let squareHeight = CGFloat(100)
#State private var bigger = false
var body: some View {
VStack {
Color.green
.frame(width: bigger ? self.squareWidth * 2 : self.squareWidth, height: self.squareHeight)
.animation(.default)
Button(action: {
self.bigger.toggle()
}) {
Text("Click me!")
}
}
}
}
The animation happens from the centre of the View, that is the anchor point (0.5, 0.5). Is there a way to change this anchor point? I found out that I can use scaleEffect on the view specifying an anchor:
struct ContentView: View {
let squareWidth = CGFloat(100)
let squareHeight = CGFloat(100)
#State private var bigger = false
var body: some View {
VStack {
Color.green
.frame(width: self.squareWidth, height: self.squareHeight)
.scaleEffect(x: bigger ? 2 : 1, y: 1, anchor: .leading)
.animation(.default)
Button(action: {
self.bigger.toggle()
}) {
Text("Click me!")
}
}
}
}
But it doesn't seem exactly the same. If I needed to apply several effects I should specify the anchor point for each of them, instead of specifying the anchor point on the view just one time. On UIKit I could write:
var myView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
myView.layer.anchorPoint = CGPoint(x: 0, y: 0.5) //that is, the .leading of SwiftUI
It is possible, but your approach must change.
There is an implied, built-in centring of views in SwiftUI, which can be confusing.
The parent view is the one placing the content in the centre, recalculating the position of its subviews each time their frames change.
Embedding the VStack in a HStack and adding borders to both clearly shows this behaviour.
Also, note the Spacer(), which pushes the view to the left.
HStack {
VStack {
Color.green
.frame(width: bigger ? self.squareWidth * 2 : self.squareWidth)
.frame(height: self.squareHeight)
.animation(.default)
Button("Click me!") { self.bigger.toggle() }
}.border(Color.black)
Spacer()
}.border(Color.red)
Using the .layer.anchorPoint in UIKit should have the same effect as using scaleEffect/offset/rotationEffect in SwiftUI, as it should affect the origin of the underlying geometry/texture which is being rendered by the GPU.
If you have a complex view (composed of multiple subviews) that needs the same scale effect, you can group them using a Group { } and use .scaleEffect on it.

Resources