I am attempting to create a ripple effect on a button, but the animated circles are going below the button, and the ZStack is not functioning as expected.
Expected Result
I want the outer two circles to be continuously going out of the central dark gray circle without any break like this animation
Current Result:
This is my code in XCode Preview it looking good but when I run it on simulator it not working then.
import SwiftUI
struct SwiftUIView: View {
@State var vpnIsActive = false
let delayArr = [0.0,0.5,1.0,1.5,2.0]
var body: some View {
VStack {
ZStack {
ForEach(1...4,id:\.self){i in
RippleEffect(vpnIsActive: $vpnIsActive, delay: delayArr[i])
}
Circle()
.fill(vpnIsActive ? Color("homeBlue") : Color("homeGray"))
.frame(width: Constants.width * 0.4, height: Constants.width * 0.4)
VStack {
Button {
vpnIsActive.toggle()
} label: {
Image("power")
}
Text(vpnIsActive ? "Tap to Disconnect" : "Tap to Connect")
.foregroundColor(.white)
}
}
.frame(width: Constants.width * 0.4, height: Constants.width * 0.4)
}
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
struct Constants{
static let height = UIScreen.main.bounds.height
static let width = UIScreen.main.bounds.width
}
struct RippleEffect:View{
@State private var scale = 1.0
@Binding var vpnIsActive:Bool
let delay :Double
var body: some View{
VStack{
Circle()
.fill(vpnIsActive ? LinearGradient(colors: [Color("homeBlue").opacity(0.5), Color("homeBlue").opacity(0.05)], startPoint: .top, endPoint: .bottom) : LinearGradient(colors: [Color("homeGray").opacity(0.8), Color("homeGray").opacity(0.1)], startPoint: .top, endPoint: .bottom))
.scaleEffect(scale)
.opacity(2 - scale)
.animation(Animation.easeInOut(duration: 2).repeatForever(autoreverses: false).delay(delay))
.onAppear {
scale = 2.5
}
}
.frame(width: Constants.width * 0.3, height: Constants.width * 0.3)
}
}


You actually have more of a math problem than anything else.
This issue you are having is simple. You intended to, I believe, have the
RippleEffectONLY on the button, but you placed your outer circle, yourRippleEffectand theStack()with the button and label into aZStackwhich takes those 3 views and stacks them one on top of the other. You need to place theRippleEffectin the background of your button. Based on your sizings, I don't believe this is what you intended.If, however, you intended the ripple effect to be on your circle, and not the button, then you have one view that is 40% of your constant, and your
RippleEffectis 30% of your constant. Your concern is that theRippleEffectgrows to be large than the other view. For the sake of ease of math, let's say the width constant is 100. That means the on view is 40x40, and theRippleEffectis 30x30. You then apply a scale effect that scales theRippleEffectby 2.4 times. That means that ultimately, theRippleEffectreaches a size of 2.5 * 30, or 75. 75 is, of course, bigger than 40. It is not more noticeable because as the view scales up, your are reducing the opacity, so by the time you are 80% of the way there (scale of 2.5 * 0.8 = 2 which is where you opacity hits 0), so the appearance of the ripples only looks like it has grown to 60, and not 75, and that is what you see as "going below the button".The other issue with your code is that you are using a deprecated
.animation()call that is deprecated for a good reason. Don't use it. Instead, wrap the scale change in awithAnimation()block in your.onAppear(). It will work more consistently. I have implemented it below.edit:
That is simply just playing with the numbers. Here, I removed the frame to make the ripple view the same size as the other view. Because it is an
.overlay()the ripple view will be governed by the frame on the view above. I then changed the beginning scale back to 1.0 and the maxScale to 1.5. Play with these to achieve the look you want.