import SwiftUI

// Animation effect that changes size of the heart icon from 1 to 0 and then back to 1
// with a cosine function
struct Scale: GeometryEffect {
    var offsetValue: Double
    
    var animatableData: Double {
        get { offsetValue }
        set { offsetValue = newValue }
    }
    
    func effectValue(size: CGSize) -> ProjectionTransform {
        let reducedValue: Double = offsetValue - floor(offsetValue)
        let scale = abs(cos(CGFloat(reducedValue) * .pi))
  
        let affineTransform = CGAffineTransform(translationX: size.width * 0.5, y: size.height * 0.5)
            .scaledBy(x: scale, y: scale)
            .translatedBy(
                x: -size.width * 0.5,
                y: -size.height * 0.5
            )
        
        return ProjectionTransform(affineTransform)
    }
}

struct ImgurLike: View {
    @State private var touch: Bool = false
    @State private var start: Bool = true
    @State private var finish: Bool = false
    @State private var animate: Double = 0
    
    var body: some View {
        ZStack {

            // Primary Icon button with active an inactive state
            ZStack {
                Image(systemName: "heart")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .opacity(touch ? 0 : 1)
                    .foregroundColor(Color.white)
                    .animation(Animation.easeInOut(duration: 0.2))
                
                Image(systemName: "heart.fill")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .opacity(touch ? 1 : 0)
                    .foregroundColor(Color(UIColor.systemGreen))
                    .animation(Animation.easeInOut(duration: 0.3))
                    .modifier(Scale(offsetValue: self.animate))

            }.frame(width: 60)

            // Two circles with different border stroke patterns that animate with a slight
            // delay one after another.
            if self.start {
                Circle()
                    .strokeBorder(
                        RadialGradient(
                            gradient: Gradient(colors: [Color.green.opacity(0), Color.green, Color.green]),
                            center: .center, startRadius: 30, endRadius: 60
                        ),
                        style: StrokeStyle(
                            lineWidth: 50,
                            dash: [2, 5]
                        )
                    )
                    .opacity(touch ? 0.8 : 0)
                    .scaleEffect(x: touch ? 1 : 0, y: touch ? 1 : 0)
                    .animation(Animation.easeInOut(duration: 0.2))
                    .mask(
                        Circle()
                            .stroke(lineWidth: touch ? 0 : 60)
                    )
                    .animation(Animation.easeInOut(duration: 0.3).delay(0.1))
                    .frame(width: 110)
                
                Circle()
                    .strokeBorder(
                        RadialGradient(
                            gradient: Gradient(colors: [Color.green.opacity(0), Color.green, Color.green]),
                            center: .center, startRadius: 30, endRadius: 60
                        ),
                        style: StrokeStyle(
                            lineWidth: 50,
                            dash: [4, 5],
                            dashPhase: 1
                        )
                    )
                    .opacity(touch ? 0.8 : 1)
                    .scaleEffect(x: touch ? 1 : 0, y: touch ? 1 : 0)
                    .animation(Animation.easeInOut(duration: 0.2).delay(0.4))
                    .mask(
                        Circle()
                            .stroke(lineWidth: touch ? 0 : 60)
                    )
                    .animation(Animation.easeInOut(duration: 0.3).delay(0.5))
                    .frame(width: 110)
                    
            }
        }
        .frame(width: 120)
        .onTapGesture{
            self.touch.toggle()

            // We have to keep track of start and finish state to make sure that some elements of the animation
            // are shown during inactive -> active transition and hidden during active -> inactive transition
            if self.touch {
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
                    if self.touch {
                        self.finish = true
                    }
                }
            } else {
                self.start = false
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                    if !self.touch {
                        self.start = true
                        self.finish = false
                    }
                }
            }
            
            withAnimation(Animation.timingCurve(0.57,2.12,0.69,0.74, duration: 0.7).delay(0.1)) {
                self.animate += 1
            }
        }
    }
}

Imgur Like button animation in SwiftUI

by Kane
Continuing the "Like" button theme - I've made a SwiftUI version of Imgur Like button animation.

Compared to Spotify version this one is much more straightforward - uses just one GeometryEffect and the rest is done using .animation() modifiers.