Animation going put effect on another animation in SwiftUI-macOS - swift

I have 2 deferent and separate Animation in my code for SwiftUI-macOS, I tried this down code, but the animation about opacity slow down the animation about value for some reason, that effect is not what I want or need! I want the animation about opacity take it place while it works smoothly with animation about value. What could be the issue? I need to refactor my code as they are to solve the animation issue. Xcode: Version 13.2.1 (13C100)
struct ContentView: View {
#State private var value: Bool = true
var body: some View {
VStack {
Group {
if value { Color.green.frame(height: 50).transition(AnyTransition.slide) }
}
.animation(Animation.linear(duration: 2.0), value: value)
HStack {
ButtonView(color: .red, action: { value.toggle() })
ButtonView(color: .blue, action: { value.toggle() })
}
}
}
}
struct ButtonView: View {
let color: Color
let action: () -> Void
#State private var opacity: CGFloat = 1.0
var body: some View {
color
.frame(height: 50)
.opacity(opacity)
.onTapGesture {
if opacity == 1.0 { opacity = 0.2 }
else { opacity = 1.0 }
action()
}
.animation(Animation.linear(duration: 5.0), value: opacity)
}
}

The Group is not real container, actually, it is better to use instead a V/HStack with preserved space, like
VStack {
if value { Color.green.transition(AnyTransition.slide) }
}
.frame(height: 50) // << here !1
.animation(Animation.linear(duration: 2.0), value: value)
Tested with Xcode 13.2 / iOS 15.2

Related

SwiftUI how do I temporarily animate a view color's foregroundColor?

When a View is pressed I know through a model button.isSelected. How do I animate the view's foreground color, similar to the IOS calculators button press animation?
Something like:
White -> Grey -> White
struct ButtonView: View {
let button: ViewModel.Button
var body: some View {
let shape = Rectangle()
ZStack {
shape.fill().foregroundColor(button.isSelected ? Color.gray : Color.white)
.animation(Animation.linear(duration: 0.01))
.border(Color.black, width: 0.33)
Text(button.content)
.font(Font.system(size:32))
}
}
}
I think there are many ways to do this.
Among them, I will write an example using DispatchQueue.main.asyncAfter()
struct ContentView: View {
#State private var isSelected: Bool = false
var body: some View {
VStack {
Button {
isSelected = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2 ) {
// To change the time, change 0.2 seconds above
isSelected = false
}
} label: {
Text("Button")
.foregroundColor(isSelected ? Color.red : Color.blue)
}
}
}
}
While DispatchQueue.main.asyncAfter() will work as Taeeun answered, note how the calculator app doesn't use a set delay. Instead, it changes color when the finger presses down, then reverts back upon release.
So, you probably want something like ButtonStyle.
struct ContentView: View {
var body: some View {
ButtonView()
}
}
struct CalculatorButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding() /// no need to use `shape` + `ZStack`, normal padding is ok
.background(configuration.isPressed ? Color.gray : Color.white) /// use `isPressed` to determine if button is currently pressed or not
.animation(Animation.linear(duration: 0.01))
.cornerRadius(10)
}
}
struct ButtonView: View {
var body: some View {
ZStack {
Color.black /// for testing purposes (see the button better)
Button {} label: {
Text("Button")
.font(.system(size: 32))
}
.buttonStyle(CalculatorButtonStyle()) /// apply the style
}
}
}
Result:

Get offset of element while moving using animation

How can i get the Y offset of a a moving element at the same time while it's moving?
This is the code that I'm tring the run:
import SwiftUI
struct testView: View {
#State var showPopup: Bool = false
var body: some View {
ZStack {
VStack {
Button(action: {
self.showPopup.toggle()
}) {
Text("show popup")
}
Color.black
.frame(width: 200, height: 200)
.offset(y: showPopup ? 0 : UIScreen.main.bounds.height)
.animation(.easeInOut)
}
}
}
}
struct testView_Previews: PreviewProvider {
static var previews: some View {
testView()
}
}
I want to get the Y value of the black squar when the button is clicked the squar will move to a 0 position however I want to detect when the squar did reach the 0 value how can i do that?
Default animation duration (for those animations which do not have explicit duration parameter) is usually 0.25-0.35 (independently of where it is started & platform), so in your case it is completely safe (tested with Xcode 11.4 / iOS 13.4) to use the following approach:
withAnimation(.spring()){
self.offset = .zero
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.animationRunning = false
}
}
SwiftUI doesn't provide a callback with an animation completion, so there are two methods to accomplish detecting when the square has finished animating.
Method 1: Using AnimatableModifier. Here's a well-written Stack Overflow post on how to set that up.
Method 2: Using a Timer to run after the animation has completed. The idea here is to create a scheduled timer that runs after the timer has finished.
struct ContentView: View {
#State var showPopup: Bool = false
var body: some View {
ZStack {
VStack {
Button(action: {
// add addition here with specified duration
withAnimation(.easeInOut(duration: 1), {
self.showPopup.toggle()
})
// set timer to run after the animation's specified duration
_ = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { timer in
// run your completion code here eg.
withAnimation(.easeInOut(duration: 1)) {
self.showPopup.toggle()
}
timer.invalidate()
}
}) {
Text("show popup")
}
Color.black
.frame(width: 200, height: 200)
.offset(y: showPopup ? 0 : UIScreen.main.bounds.height)
}
}
}
}

Having trouble creating a star animation in swiftUI

I'm trying to create a star animation using blur, but my stars end up disappearing and I can't figure out why.
Through debugging a bit I'm pretty sure this has to do with how I'm using onAppear. I'm just trying to make sure the stars blur and unblur on the screen forever - I always want the stars to be visible though.
Could anyone help me fix this problem (attached code below) and any design tips would be appreciated haha.
struct ContentView: View {
#State private var radius = 2
private var opacity = 0.25
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
VStack {
ForEach(0..<8) {_ in
HStack {
ForEach(0..<5) { _ in
Circle().fill(Color.white)
.frame(width: 3, height: 3)
.blur(radius: CGFloat(self.radius))
.animation(Animation.easeInOut(duration: 6).
repeatForever(autoreverses: true))
.padding(EdgeInsets(top: self.calculateRandom(), leading: 0,
bottom: 0, trailing: self.calculateRandom()))
.onAppear() {
self.radius += 2
}
}
}
}
}
}
}
func calculateRandom() -> CGFloat {
return CGFloat(Int.random(in: 30..<150))
}
}
To have animation activated you need to toggle some values, so animator has range to animate in between.
Here is fixed code. Tested with Xcode 12 / iOS 14.
struct ContentView: View {
#State private var run = false // << here !!
private var opacity = 0.25
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
VStack {
ForEach(0..<8) {_ in
HStack {
ForEach(0..<5) { _ in
Circle().fill(Color.white)
.frame(width: 3, height: 3)
.blur(radius: run ? 4 : 2) // << here !!
.animation(Animation.easeInOut(duration: 6).repeatForever(autoreverses: true))
.padding(EdgeInsets(top: self.calculateRandom(), leading: 0,
bottom: 0, trailing: self.calculateRandom()))
.onAppear() {
self.run = true // << here !!
}
}
}
}
}
}
}
func calculateRandom() -> CGFloat {
return CGFloat(Int.random(in: 30..<150))
}
}
Update: variant with static star positions (movement animation is caused by layout in V/H/Stacks as soon as new elements added, so to avoid this it needs to remove those inner stacks and layout manually in ZStack with .position modifier)
struct BlurContentView: View {
#State private var run = false
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea(.all)
GeometryReader { gp in
ForEach(0..<8) {_ in
ForEach(0..<5) { _ in
Circle().fill(Color.white)
.frame(width: 3, height: 3)
.position(x: calculateRandom(in: gp.size.width),
y: calculateRandom(in: gp.size.height))
.animation(nil) // << no animation for above modifiers
.blur(radius: run ? 4 : 2)
}
}
}
.animation(Animation.easeInOut(duration: 6)
.repeatForever(autoreverses: true), value: run) // animate one value
.onAppear() {
self.run = true
}
}
}
func calculateRandom(in value: CGFloat) -> CGFloat {
return CGFloat(Int.random(in: 10..<Int(value) - 10))
}
}

SwiftUI: Stop an Animation that Repeats Forever

I would like to have a 'badge' of sorts on the screen and when conditions are met, it will bounce from normal size to bigger and back to normal repeatedly until the conditions are no longer met. I cannot seem to get the badge to stop 'bouncing', though. Once it starts, it's unstoppable.
What I've tried:
I have tried using a few animations, but they can be classified as animations that use 'repeatForever' to achieve the desired effect and those that do not. For example:
Animation.default.repeatForever(autoreverses: true)
and
Animation.spring(response: 1, dampingFraction: 0, blendDuration: 1)(Setting damping to 0 makes it go forever)
followed by swapping it out with .animation(nil). Doesn't seem to work. Does anyone have any ideas? Thank you so very much ahead of time! Here is the code to reproduce it:
struct theProblem: View {
#State var active: Bool = false
var body: some View {
Circle()
.scaleEffect( active ? 1.08: 1)
.animation( active ? Animation.default.repeatForever(autoreverses: true): nil )
.frame(width: 100, height: 100)
.onTapGesture {
self.active = !self.active
}
}
}
I figured it out!
An animation using .repeatForever() will not stop if you replace the animation with nil. It WILL stop if you replace it with the same animation but without .repeatForever(). ( Or alternatively with any other animation that comes to a stop, so you could use a linear animation with a duration of 0 to get a IMMEDIATE stop)
In other words, this will NOT work: .animation(active ? Animation.default.repeatForever() : nil)
But this DOES work: .animation(active ? Animation.default.repeatForever() : Animation.default)
In order to make this more readable and easy to use, I put it into an extension that you can use like this: .animation(Animation.default.repeat(while: active))
Here is an interactive example using my extension you can use with live previews to test it out:
import SwiftUI
extension Animation {
func `repeat`(while expression: Bool, autoreverses: Bool = true) -> Animation {
if expression {
return self.repeatForever(autoreverses: autoreverses)
} else {
return self
}
}
}
struct TheSolution: View {
#State var active: Bool = false
var body: some View {
Circle()
.scaleEffect( active ? 1.08: 1)
.animation(Animation.default.repeat(while: active))
.frame(width: 100, height: 100)
.onTapGesture {
self.active.toggle()
}
}
}
struct TheSolution_Previews: PreviewProvider {
static var previews: some View {
TheSolution()
}
}
As far as I have been able to tell, once you assign the animation, it will not ever go away until your View comes to a complete stop. So if you have a .default animation that is set to repeat forever and auto reverse and then you assign a linear animation with a duration of 4, you will notice that the default repeating animation is still going, but it's movements are getting slower until it stops completely at the end of our 4 seconds. So we are animating our default animation to a stop through a linear animation.
How about using a Transaction
In the code below, I turn off or turn on the animation depending on the state of the active
Warning: Be sure to use withAnimation otherwise nothing will work
#State var active: Bool = false
var body: some View {
Circle()
.scaleEffect(active ? 1.08: 1)
.animation(Animation.default.repeatForever(autoreverses: true), value: active)
.frame(width: 100, height: 100)
.onTapGesture {
useTransaction()
}
}
func useTransaction() {
var transaction = Transaction()
transaction.disablesAnimations = active ? true : false
withTransaction(transaction) {
withAnimation {
active.toggle()
}
}
}
After going through many things, I found out something that works for me. At the least for the time being and till I have time to figure out a better way.
struct WiggleAnimation<Content: View>: View {
var content: Content
#Binding var animate: Bool
#State private var wave = true
var body: some View {
ZStack {
content
if animate {
Image(systemName: "minus.circle.fill")
.foregroundColor(Color(.systemGray))
.offset(x: -25, y: -25)
}
}
.id(animate) //THIS IS THE MAGIC
.onChange(of: animate) { newValue in
if newValue {
let baseAnimation = Animation.linear(duration: 0.15)
withAnimation(baseAnimation.repeatForever(autoreverses: true)) {
wave.toggle()
}
}
}
.rotationEffect(.degrees(animate ? (wave ? 2.5 : -2.5) : 0.0),
anchor: .center)
}
init(animate: Binding<Bool>,
#ViewBuilder content: #escaping () -> Content) {
self.content = content()
self._animate = animate
}
}
Use
#State private var editMode = false
WiggleAnimation(animate: $editMode) {
VStack {
Image(systemName: image)
.resizable()
.frame(width: UIScreen.screenWidth * 0.1,
height: UIScreen.screenWidth * 0.1)
.padding()
.foregroundColor(.white)
.background(.gray)
Text(text)
.multilineTextAlignment(.center)
.font(KMFont.tiny)
.foregroundColor(.black)
}
}
How does it work?
.id(animate) modifier here does not refresh the view but just replaces it with a new one, so it is back to its original state.
Again this might not be the best solution but it works for my case.
There is nothing wrong in your code, so I assume it is Apple's defect. It seems there are many with implicit animations (at least with Xcode 11.2). Anyway...
I recommend to consider alternate approach provided below that gives expected behaviour.
struct TestAnimationDeactivate: View {
#State var active: Bool = false
var body: some View {
VStack {
if active {
BlinkBadge()
} else {
Badge()
}
}
.frame(width: 100, height: 100)
.onTapGesture {
self.active.toggle()
}
}
}
struct Badge: View {
var body: some View {
Circle()
}
}
struct BlinkBadge: View {
#State private var animating = false
var body: some View {
Circle()
.scaleEffect(animating ? 1.08: 1)
.animation(Animation.default.repeatForever(autoreverses: true))
.onAppear {
self.animating = true
}
}
}
struct TestAnimationDeactivate_Previews: PreviewProvider {
static var previews: some View {
TestAnimationDeactivate()
}
}
Aspid comments on the accepted solution that an Xcode update broke it. I was struggling with a similar problem while playing around with an example from Hacking with Swift, and
.animation(active ? Animation.default.repeatForever() : Animation.default)
was not working for me either on Xcode 13.2.1. The solution I found was to encapsulate the animation in a custom ViewModifier. The code below illustrates this; the big button toggles between active and inactive animations.
`
struct ContentView: View {
#State private var animationAmount = 1.0
#State private var animationEnabled = false
var body: some View {
VStack {
Button("Tap Me") {
// We would like to stop the animation
animationEnabled.toggle()
animationAmount = animationEnabled ? 2 : 1
}
.onAppear {
animationAmount = 2
animationEnabled = true
}
.padding(50)
.background(.red)
.foregroundColor(.white)
.clipShape(Circle())
.overlay(
Circle()
.stroke(.red)
.scaleEffect(animationAmount)
.opacity(2 - animationAmount)
)
.modifier(AnimatedCircle(animationAmount: $animationAmount, animationEnabled: $animationEnabled))
}
}
}
struct AnimatedCircle: ViewModifier {
#Binding var animationAmount: Double
#Binding var animationEnabled: Bool
func body(content: Content) -> some View {
if animationEnabled {
return content.animation(.easeInOut(duration: 2).repeatForever(autoreverses: false),value: animationAmount)
}
else {
return content.animation(.easeInOut(duration: 0),value: animationAmount)
}
}
}
`
It may not be the best conceivable solution, but it works. I hope it helps somebody.

Repeating Action Continuously In SwiftUI

How can I make an element such as a text field scale up and then down continuously?
I have this:
struct ContentView : View {
#State var size:Double = 0.5
var body: some View {
ZStack {
Text("Hello!")
.padding()
.scaleEffect(size)
}
}
}
I know I need to increase size and then decrease it in some sort of loop but the following cannot be done in SwiftUI:
while true {
self.size += 0.8
sleep(0.2)
self.size -= 0.8
}
A possible solution is to use a (repeating, auto-reversing) animation:
struct ContentView : View {
#State var size: CGFloat = 0.5
var repeatingAnimation: Animation {
Animation
.easeInOut(duration: 2) //.easeIn, .easyOut, .linear, etc...
.repeatForever()
}
var body: some View {
Text("Hello!")
.padding()
.scaleEffect(size)
.onAppear() {
withAnimation(self.repeatingAnimation) { self.size = 1.3 }
}
}
}
Animation.basic is deprecated. Basic animations are now named after their curve types: like linear, etc:
var foreverAnimation: Animation {
Animation.linear(duration: 0.3)
.repeatForever()
}
Source:
https://forums.swift.org/t/swiftui-animation-basic-duration-curve-deprecated/27076
The best way is to create separate animation struct and configure all the options you need(this way your code will be more compact).
To make it more clear and logical use #State property isAnimating. You will be able to stop your animation and resume again and understand when it is in progress.
#State private var isAnimating = false
var foreverAnimation: Animation {
Animation.linear(duration: 0.3)
.repeatForever()
}
var body: some View {
Text("Hello")
.scaleEffect(isAnimating ? 1.5 : 1)
.animation(foreverAnimation)
.onAppear {
self.isAnimating = true
}
}
Using a repeating animation on a view has weird behaviour when used inside if statements.
If you want to do:
if something {
BlinkingView()
}
use a transition with an animation modifier, otherwise the view stays on the screen even after something is set to false.
I made this extension to show a view that repeats change from one state to the next and back:
extension AnyTransition {
static func repeating<T: ViewModifier>(from: T, to: T, duration: Double = 1) -> AnyTransition {
.asymmetric(
insertion: AnyTransition
.modifier(active: from, identity: to)
.animation(Animation.easeInOut(duration: duration).repeatForever())
.combined(with: .opacity),
removal: .opacity
)
}
}
This makes the view appear and disappear with AnyTransition.opacity and while it is shown it switches between the from and to state with a delay of duration.
Example usage:
struct Opacity: ViewModifier {
private let opacity: Double
init(_ opacity: Double) {
self.opacity = opacity
}
func body(content: Content) -> some View {
content.opacity(opacity)
}
}
struct ContentView: View {
#State var showBlinkingView: Bool = false
var body: some View {
VStack {
if showBlinkingView {
Text("I am blinking")
.transition(.repeating(from: Opacity(0.3), to: Opacity(0.7)))
}
Spacer()
Button(action: {
self.showBlinkingView.toggle()
}, label: {
Text("Toggle blinking view")
})
}.padding(.vertical, 50)
}
}
Edit:
When the show condition is true on appear, the transition doesn't start. To fix this I do toggle the condition on appear of the superview (The VStack in my example):
.onAppear {
if self.showBlinkingView {
self.showBlinkingView.toggle()
DispatchQueue.main.async {
self.showBlinkingView.toggle()
}
}
}