8

Trying to implement the following animation in SwiftUI and finding it quite impossible:

enter image description here

To summarize the effect: A repeating pulse, animating each segment with a staggered delay. For each segment:

  • Starting at opacity=0.5, scale = 1
  • Animate to opacity=1.0, scale = 1.3
  • The animation curve is starts quickly with a longer ease-out

In addition, there is delay between each 'pulse'.

The closest I've been able to get to this with SwiftUI is a continuously repeating animation using the .repeatForever modifier. (Code below. Ignore the mismatch in timing for now.)

How can I Add a delay between each loop of the animation?

Here is the result of my code below:

enter image description here

import SwiftUI

struct ArrowShape : Shape {

    func path(in rect: CGRect) -> Path {
        var path = Path()

        path.move(to: CGPoint(x: 0, y: 0))
        path.addLine(to: CGPoint(x: rect.size.width, y: rect.size.height/2.0))
        path.addLine(to: CGPoint(x: 0, y: rect.size.height))

        return path
    }
}

struct Arrows: View {
    private let arrowCount = 3

    @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.8)
                            .repeatForever(autoreverses: true)
                            .delay(0.2 * Double(i))
                        )
                }
            }

            .onAppear() {
                self.scale = 1.2
                self.fade = 1.0
            }
        }
    }
}



struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Arrows()
    }
}
3
  • stackoverflow.com/a/29158824/793607 shows how to make an animating loop with the delay after each animation. Commented Mar 25, 2020 at 16:20
  • 3
    @HalR huh? That answer is a UIKit solution. Commented Mar 25, 2020 at 17:52
  • I blame it on too many days in isolation. :) Commented Mar 25, 2020 at 19:08

3 Answers 3

9

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
                            }
                    }
            }
        }
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

Produces desired effect. Thanks!
CAVEAT: This only works if the view is never refreshed. If a view hierarchy containing this view is rebuilt, then the timers do not have a chance to fire.
5

January 2024. Found a have a full cycle animations cycle, and then pause 3 seconds before starting again.

enum AnimationPhase: CaseIterable {
    case start, middle, end
    
    var offset: CGFloat {
        switch self {
        case .start: 0
        case .middle: -10
        case .end: 0
        }
    }
}

...

Image("bouncy_thumbs_up_icon")
    .phaseAnimator(AnimationPhase.allCases) { content, phase in
        content
            .offset(y: phase.offset)
    } animation: { phase in
        switch phase {
        case .start: .bouncy(duration: 0.5)
        case .middle: .bouncy.delay(3) // DISCOVERY: Took forever to learn! Pause between full cycle animations can be done like this.
        case .end: .bouncy(duration: 0.5)
        }
    }

Comments

1

Referencing @Mac3n's answer - replacing the DispatchQueue with withAnimation, then changing the timer's every: parameter to 1 instead of 2 seconds, produces the same effect, and has no need for a repeatCount:

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

...

.onReceive(timer) { _ in
    withAnimation {
        scale = scale > 1 ? 1 : 1.2
        fade = fade > 0.0 ? 0.0 : 1.0
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.