Delay a Repeating Animation in Swiftui with Between Full Autoreverse Repeat Cycles

Delay a repeating animation in SwiftUI with between full autoreverse repeat cycles

A possible solution is to chain single pieces of animation using DispatchQueue.main.asyncAfter. This gives you control when to delay specific parts.

Here is a demo:

Sample Image

struct SimpleBeatingView: View {
@State private var isBeating = false
@State private var heartState: HeartState = .normal

@State private var beatLength: TimeInterval = 1
@State private var beatDelay: TimeInterval = 3

var body: some View {
VStack {
Image(systemName: "heart.fill")
.imageScale(.large)
.font(.largeTitle)
.foregroundColor(.red)
.scaleEffect(heartState.scale)
Button("isBeating: \(String(isBeating))") {
isBeating.toggle()
}
HStack {
Text("beatLength")
Slider(value: $beatLength, in: 0.25...2)
}
HStack {
Text("beatDelay")
Slider(value: $beatDelay, in: 0...5)
}
}
.onChange(of: isBeating) { isBeating in
if isBeating {
startAnimation()
} else {
stopAnimation()
}
}
}
}
private extension SimpleBeatingView {
func startAnimation() {
isBeating = true
withAnimation(Animation.linear(duration: beatLength * 0.25)) {
heartState = .large
}
DispatchQueue.main.asyncAfter(deadline: .now() + beatLength * 0.25) {
withAnimation(Animation.linear(duration: beatLength * 0.5)) {
heartState = .small
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + beatLength * 0.75) {
withAnimation(Animation.linear(duration: beatLength * 0.25)) {
heartState = .normal
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + beatLength + beatDelay) {
withAnimation {
if isBeating {
startAnimation()
}
}
}
}

func stopAnimation() {
isBeating = false
}
}
enum HeartState {
case small, normal, large

var scale: CGFloat {
switch self {
case .small: return 0.5
case .normal: return 0.75
case .large: return 1
}
}
}

Swift 3 - Delay Repeating Animation

Use a Timer:

// note that I used 17.5 here because the animation itself takes 7.5 seconds
// so that will be 7.5 seconds of animating, 10 seconds of doing nothing
// and start animating again
Timer.scheduledTimer(withTimeInterval: 17.5, repeats: true) {
UIView.animate(withDuration: 7.5, animations: {
self.imageAnimate.center.x += self.view.bounds.width * 2
})
}

Delay SwiftUI combined transitions

Try this

extension AnyTransition {
static var delayAndFade: AnyTransition {
return AnyTransition.identity
.combined(with: .opacity)
.animation(.default.delay(3))
}
}

If you want to move a view, you should animate its offset using the withAnimation function.

         Text("Move and fade.")
.offset(y: offset)
.transition(.delayAndFade)

struct ContentView: View {
@State private var showDetails = false
@State var offset:CGFloat = 0

var body: some View {
VStack {
Button("Press to show details") {
showDetails.toggle()
withAnimation(.default.delay(3)) {
self.offset = -20
}
}


if showDetails {
Text("Move and fade.")
.offset(y: offset)
.transition(.delayAndFade)
}
}
}
}

Update

extension AnyTransition {
static var moveAndFade: AnyTransition {
return AnyTransition.move(edge: .top)
.combined(with: .opacity)
}
}

Try this

HStack {
Text("Move and fade.")
}
.animation(Animation.default.delay(2))
.transition(.moveAndFade)

It works with all kind of views except Text.

struct ContentView: View {
@State private var showDetails = false
@State var offset:CGFloat = 0

var body: some View {
VStack {
Button("Press to show details") {
showDetails.toggle()

}


if showDetails {

// Works!
HStack {
Text("Move and fade.")
}
.animation(Animation.default.delay(2))
.transition(.moveAndFade)

Button("Move and fade.") {}
.animation(Animation.default.delay(2))
.transition(.moveAndFade)

// Does not work
Text("Move and fade.")
.animation(Animation.default.delay(2))
.transition(.moveAndFade)
}
}
}
}

SwiftUI animation: How to stagger repeating animation with delay

I think you can implement it using Timer and DispatchQueue, try this and see it's working as you want or no

struct Arrows: View {
private let arrowCount = 3

let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()

@State var scale:CGFloat = 1.0
@State var fade:Double = 0.5

var body: some View {

ZStack {
Color(red: 29.0/255.0, green: 161.0/255.0, blue: 224.0/255.0).edgesIgnoringSafeArea(.all)

HStack{
ForEach(0..<self.arrowCount) { i in
ArrowShape()
.stroke(style: StrokeStyle(lineWidth: CGFloat(10),
lineCap: .round,
lineJoin: .round ))
.foregroundColor(Color.white)
.aspectRatio(CGSize(width: 28, height: 70), contentMode: .fit)
.frame(maxWidth: 20)
.animation(nil)
.opacity(self.fade)
.scaleEffect(self.scale)
.animation(
Animation.easeOut(duration: 0.5)
//.repeatForever(autoreverses: true)
.repeatCount(1, autoreverses: true)
.delay(0.2 * Double(i))
)
}.onReceive(self.timer) { _ in
self.scale = self.scale > 1 ? 1 : 1.2
self.fade = self.fade > 0.5 ? 0.5 : 1.0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.scale = 1
self.fade = 0.5
}
}
}
}
}
}

How to STOP Animation().repeatForever in SwiftUI

Update - retested with Xcode 13.4 / iOS 15.5

Sample Image

The following should work, moved animation into overlay-only and added conditional to .default (tested with Xcode 11.2 / iOS 13.2)

Button("Start", action: { self.start.toggle() })
.font(.largeTitle)
.padding()
.foregroundColor(.white)
.background(RoundedRectangle(cornerRadius: 10).fill(Color.green))
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.green, lineWidth: 4)
.scaleEffect(start ? 2 : 0.9)
.opacity(start ? 0 : 1)
.animation(start ? Animation.easeOut(duration: 0.6)
.delay(1) // Add 1 second between animations
.repeatForever(autoreverses: false) : .default, value: start)
)

On GitHub



Related Topics



Leave a reply



Submit