Quinn
Quinn

Reputation: 9454

Making text pulsate in SwiftUI

So I am trying to make some text fade in and out to give a pulsate type effect, this is the code I have now:

struct SignInView: View {
    @State private var opacity: Double = 0.0
    @State private var pulseDown: Bool = false
    
    var body: some View {
        VStack(alignment: .center) {
            Spacer()
            
            Button(action: {
                AppDelegate.signInWithGoogle()
            }, label: {
                Text("Sign In")
                    .foregroundColor(Color.green)
                    .opacity(opacity)
            })
            
            Spacer()
        }
        .padding()
        .onAppear {
            self.pulsateText()
        }
    }
    
    private func pulsateText() {
        DispatchQueue.init(label: "Pulse").asyncAfter(deadline: .now() + 0.01) {
            if self.pulseDown {
                self.opacity -= 0.02
            } else {
                self.opacity += 0.02
            }
            
            if self.opacity > 1 {
                self.pulseDown = true
            } else if self.opacity < 0.1 {
                self.pulseDown = false
            }
            
            self.pulsateText()
        }
    }
}

It does exactly what I want and looks good, but I can't help but feel that an infinite recursive loop is not the right way to be doing it. I suppose I could make an infinite while instead of the infinite recursion, though that still seem not ideal. Is there a better way to achieve this?

Upvotes: 1

Views: 1600

Answers (2)

Nikolay Suvandzhiev
Nikolay Suvandzhiev

Reputation: 9075

Here's a simple ViewModifier version, which isolatest the logic into a pulseEffect() similiar to the native scaleEffect():

struct PulseEffect: ViewModifier {
    @State private var pulseIsInMaxState: Bool = true
    private let range: ClosedRange<Double>
    private let duration: TimeInterval

    init(range: ClosedRange<Double>, duration: TimeInterval) {
        self.range = range
        self.duration = duration
    }

    func body(content: Content) -> some View {
        content
            .opacity(pulseIsInMaxState ? range.upperBound : range.lowerBound)
            .onAppear { pulseIsInMaxState = false }
            .animation(.smooth(duration: duration).repeatForever(), value: pulseIsInMaxState)
    }
}

public extension View {
    func pulseEffect(range: ClosedRange<Double> = 0.5...1, duration: TimeInterval = 1) -> some View  {
        modifier(PulseEffect(range: range, duration: duration))
    }
}

#Preview {
    Text("Hello, world!")
        .pulseEffect()
}

Upvotes: 1

George
George

Reputation: 30421

There is an easier more SwiftUI-like way. It works by using an Animation's repeatForever(autoreverses:) method:

struct SignInView: View {

    @State private var visible = true
    
    var body: some View {
        VStack {
            Spacer()
            
            Button(action: {
                print("Sign in with Google")
//                AppDelegate.signInWithGoogle()
            }, label: {
                Text("Sign In")
                    .foregroundColor(Color.green)
                    .opacity(visible ? 1 : 0)
            })
            
            Spacer()
        }
        .padding()
        .onAppear(perform: pulsateText)
    }
    
    private func pulsateText() {
        withAnimation(Animation.easeInOut.repeatForever(autoreverses: true)) {
            visible.toggle()
        }
    }
}

Upvotes: 8

Related Questions