SwiftUI - how to pulsate image opacity? - animation

I have an Image in SwiftUI that I would like to "pulsate" (come & go every second or so) forever.
I tried a number of things but I can't seem to get the effect I want. One of the things I tried is the code below (which seems to do nothing! :) ).
Image(systemName: "dot.radiowaves.left.and.right" )
.foregroundColor(.blue)
.transition(.opacity)
.animation(Animation.easeInOut(duration: 1)
.repeatForever(autoreverses: true))
Any ideas?

Here is an approach. Tested with Xcode 11.4 / iOS 13.4
struct DemoImagePulsate: View {
#State private var value = 1.0
var body: some View {
Image(systemName: "dot.radiowaves.left.and.right" )
.foregroundColor(.blue)
.opacity(value)
.animation(Animation.easeInOut(duration: 1).repeatForever(autoreverses: true))
.onAppear { self.value = 0.3 }
}
}

Related

Animation is lost when view is not displayed (yet) in SwiftUI

I have a button implemented like so:
struct CircleButtonView: View {
init(_ content: Image, tutorial: Bool = false, _ action: (() -> ())? = nil) {
self.content = content
self.repetitions = tutorial ? 6.0 : 0.0
self.action = action
}
let content: Image
var repetitions: CGFloat
let action: (() -> ())?
var body: some View {
Button(action: {
action?()
}) {
Circle()
.fill(.white)
.shadow(color: Color(hex: 0x000000, alpha: 0.1), radius: 10, x: 0, y: 10)
.overlay(
GeometryReader { proxy in
content
.resizable()
.aspectRatio(contentMode: .fit) // 1:1
.frame(width: proxy.size.width * 0.4)
.position(x: proxy.size.width / 2, y: proxy.size.height / 2)
}
)
.aspectRatio(1 / 1, contentMode: .fit)
}
.shake(repetitions: repetitions)
.animation(repetitions != 0 ? .linear(duration: 1).delay(2).repeatForever(autoreverses: false) : .default, value: repetitions)
}
}
Via the tutorial bool i want to manipulate whether the button animates (an initial shake animation used to explain the button, if the user clicked it once i remove the animation).
This works nicely. However i have to use a "hack", which i dont like and will describe now.
I call the button in some parent view like so:
CircleButtonView(Image("flash.selected"), tutorial: <condition for tutorial> && appeared) {
// vm.boost()
}
.onAppear {
appeared = true
}
As you can see, despite my condition i also have to check wheter the button has appeared. If i would not do that, the animation would be lost ... (this is, as the init block runs and the view did not appear yet, any future calls, where the view appeared, do work perfectly)
As this does not feel right, is this the correct way ?
In order to animate, SwiftUI needs to observe a before state of the view and an after state. As you've seen, SwiftUI needs to see the repetitions value change from 0 to 6 in order to perform the animation. Processing the .onAppear { } from the user of CircleButtonView is the wrong place. Put that code inside of CircleButtonView.
Make repetitions into an #State private var. Don't set it in init().
#State private var repetitions = 0
In init(), store tutorial in a local let:
self.tutorial = tutorial
Then add this .onAppear to your Button:
}
.shake(repetitions: repetitions)
.animation(repetitions != 0 ? .linear(duration: 1).delay(2).repeatForever(autoreverses: false) : .default, value: repetitions)
.onAppear {
repetitions = tutorial ? 6.0 : 0.0
}

Animation doesn't work on AnyTransition SwiftUI

I'm currently using SwiftUI on a function where a list of images change automatically with opacity animation for each image.
While I've currently managed to get a transition going, the opacity animation does not work no matter what I try.
Could someone please help me out with this...
The code I'm working on is as follows:
//
// EpilogueThree.swift
//
import SwiftUI
struct EpilogueThree: View {
let images = ["1", "2", "3"]
let imageChangeTimer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
let transition = AnyTransition.asymmetric(insertion: .slide, removal: .scale).combined(with: .opacity)
#State private var imagePage = 2
#State private var currentImageIndex = 0
var body: some View {
ZStack {
VStack {
Text("Page: \(imagePage)")
.font(.largeTitle)
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 5)
.background(.black.opacity(0.75))
Image(images[currentImageIndex])
.resizable()
.ignoresSafeArea()
.transition(transition)
.onReceive(imageChangeTimer) { _ in
if imagePage > 0 {
self.currentImageIndex = (self.currentImageIndex + 1) % self.images.count
imagePage -= 1
}
}
}
}
}
}
struct EpilogueThree_Previews: PreviewProvider {
static var previews: some View {
EpilogueThree()
.previewInterfaceOrientation(.landscapeRight)
}
}
The current code acts something like this:
But the code I want needs to do something like this:
We can make image removable in this case by add id (new id - new view) and add animation to container... for animation.
Here is fixed part. Tested with Xcode 13.4 / iOS 15.5
VStack {
// .. other code
Image(images[currentImageIndex])
.resizable()
.ignoresSafeArea()
.id(imagePage) // << here !!
.transition(transition)
.onReceive(imageChangeTimer) { _ in
if imagePage > 0 {
self.currentImageIndex = (self.currentImageIndex + 1) % self.images.count
imagePage -= 1
}
}
}
.animation(.default, value: imagePage) // << here !!

SwiftUI - Pulsating Animation & Change Colour

I'm trying to change the colour of my animation based on the state of something. The colour change works but it animates the previous (orange) colour with it. I can't quite work out why both colours are showing. Any ideas?
struct PulsatingView: View {
var state = 1
func colourToShow() -> Color {
switch state {
case 0:
return Color.red
case 1:
return Color.orange
case 2:
return Color.green
default:
return Color.orange
}
}
#State var animate = false
var body: some View {
VStack {
ZStack {
Circle().fill(colourToShow().opacity(0.25)).frame(width: 40, height: 40).scaleEffect(self.animate ? 1 : 0)
Circle().fill(colourToShow().opacity(0.35)).frame(width: 30, height: 30).scaleEffect(self.animate ? 1 : 0)
Circle().fill(colourToShow().opacity(0.45)).frame(width: 15, height: 15).scaleEffect(self.animate ? 1 : 0)
Circle().fill(colourToShow()).frame(width: 6.25, height: 6.25)
}
.onAppear { self.animate.toggle() }
.animation(Animation.easeInOut(duration: 1.5).repeatForever(autoreverses: true))
}
}
}
You change the color, but animating view, which is set up forever is not changed - it remains as set and continues as specified - forever. So it needs to re-set the animation.
Please find below a working full module demo code (tested with Xcode 11.2 / iOS 13.2). The idea is to use ObservableObject view model as it allows and refresh view and perform some actions on receive. So receiving color changes it is possible to reset and view color and animation.
Updated for Xcode 13.3 / iOS 15.4
Main part is:
}
.onAppear { self.animate = true }
.animation(animate ? Animation.easeInOut(duration: 1.5).repeatForever(autoreverses: true) : .default, value: animate)
.onChange(of: viewModel.colorIndex) { _ in
self.animate = false // << here !!
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.animate = true // << here !!
}
}
Complete findings and code is here

Animations triggered by events in SwiftUI

SwiftUI animations are typically driven by state, which is great, but sometimes you really want to trigger a temporary (often reversible) animation in response to some event. For example, I want to temporarily increase the size of a button when a it is tapped (both the increase and decrease in size should happen as a single animation when the button is released), but I haven't been able to figure this out.
It can sort of be hacked together with transitions I think, but not very nicely. Also, if I make an animation that uses autoreverse, it will increase the size, decrease it and then jump back to the increased state.
That is something I have been into as well.
So far my solution depends on applying GeometryEffect modifier and misusing the fact that its method effectValue is called continuously during some animation. So the desired effect is actually a transformation of interpolated values from 0..1 that has the main effect in 0.5 and no effect at 0 or 1
It works great, it is applicable to all views not just buttons, no need to depend on touch events or button styles, but still sort of seems to me as a hack.
Example with random rotation and scale effect:
Code sample:
struct ButtonEffect: GeometryEffect {
var offset: Double // 0...1
var animatableData: Double {
get { offset }
set { offset = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
let effectValue = abs(sin(offset*Double.pi))
let scaleFactor = 1+0.2*effectValue
let affineTransform = CGAffineTransform(rotationAngle: CGFloat(effectValue)).translatedBy(x: -size.width/2, y: -size.height/2).scaledBy(x: CGFloat(scaleFactor), y: CGFloat(scaleFactor))
return ProjectionTransform(affineTransform)
}
}
struct ButtonActionView: View {
#State var animOffset: Double = 0
var body: some View {
Button(action:{
withAnimation(.spring()) {
self.animOffset += 1
}
})
{
Text("Press ME")
.padding()
}
.background(Color.yellow)
.modifier(ButtonEffect(offset: animOffset))
}
}
You can use a #State variable tied to a longPressAction():
Code updated for Beta 5:
struct ContentView: View {
var body: some View {
HStack {
Spacer()
MyButton(label: "Button 1")
Spacer()
MyButton(label: "Button 2")
Spacer()
MyButton(label: "Button 3")
Spacer()
}
}
}
struct MyButton: View {
let label: String
#State private var pressed = false
var body: some View {
return Text(label)
.font(.title)
.foregroundColor(.white)
.padding(10)
.background(RoundedRectangle(cornerRadius: 10).foregroundColor(.green))
.scaleEffect(self.pressed ? 1.2 : 1.0)
.onLongPressGesture(minimumDuration: .infinity, maximumDistance: .infinity, pressing: { pressing in
withAnimation(.easeInOut(duration: 0.2)) {
self.pressed = pressing
}
}, perform: { })
}
}
I believe this is what you're after. (this is how I solved this problem)
Based on dfd's link in i came up with this, which is not dependent on any #State variable. You simply just implement your own button style.
No need for Timers, #Binding, #State or other complex workarounds.
import SwiftUI
struct MyCustomPressButton: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.padding(10)
.cornerRadius(10)
.scaleEffect(configuration.isPressed ? 0.8 : 1.0)
}
}
struct Play: View {
var body: some View {
Button("Tap") {
}.buttonStyle(MyCustomPressButton())
.animation(.easeIn(duration: 0.2))
}
}
struct Play_Previews: PreviewProvider {
static var previews: some View {
Play()
}
}
There is no getting around the need to update via state in SwiftUI. You need to have some property that is only true for a short time that then toggles back.
The following animates from small to large and back.
struct ViewPlayground: View {
#State var enlargeIt = false
var body: some View {
Button("Event!") {
withAnimation {
self.enlargeIt = true
}
}
.background(Momentary(doIt: self.$enlargeIt))
.scaleEffect(self.enlargeIt ? 2.0 : 1.0)
}
}
struct Momentary: View {
#Binding var doIt: Bool
var delay: TimeInterval = 0.35
var body: some View {
Group {
if self.doIt {
ZStack { Spacer() }
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + self.delay) {
withAnimation {
self.doIt = false
}
}
}
}
}
}
}
Unfortunately delay was necessary to get the animation to occur when setting self.enlargeIt = true. Without that it only animates back down. Not sure if that's a bug in Beta 4 or not.

Xcode 11 Beta 3 animation no longer works

I have just changed from Xcode 11 Beta 2, to Beta 3, and although I had to also change the navigationButton to navigationLink, all is ok, expect for the .animation()
Has anyone else seen this issue? Have they changed something? I was working just fine in Beta 2.
Thanks !!
import SwiftUI
struct BackGround : View {
var body: some View {
ZStack{
Rectangle()
.fill(Color.gray)
.opacity(0.9)
.cornerRadius(15.0)
.shadow(radius: /*#START_MENU_TOKEN#*/10/*#END_MENU_TOKEN#*/)
.blur(radius: 5)
.padding(20)
.animation(.basic())
}
}
}
I found that if I wrapped the content of a view in a VStack, perhaps other Stacks would also work, the view will animate in the previewer
Heres a quick example of a view that if wrapped in a VStack in the PreviewProvider previews the button will animate. But if the VStack is removed it will no longer animate. Try it out!
struct AnimatedButton : View {
#State var isAnimating: Bool = false
var body: some View {
Button(action: {
self.isAnimating.toggle()
}) {
Text("asdf")
}.foregroundColor(Color.yellow)
.padding()
.background(Color(.Green))
.cornerRadius(20)
.animation(.spring())
.scaleEffect(isAnimating ? 2.0 : 1.0)
}
}
#if DEBUG
struct FunButton_Previews : PreviewProvider {
static var previews: some View {
VStack {
AnimatedButton()
}
}
}
#endif

Resources