Fill circle with wave animation in SwiftUI
Here's a complete standalone example. It features a slider which allows you to change the percentage:
import SwiftUI
struct ContentView: View {
@State private var percent = 50.0
var body: some View {
VStack {
CircleWaveView(percent: Int(self.percent))
Slider(value: self.$percent, in: 0...100)
}
.padding(.all)
}
}
struct Wave: Shape {
var offset: Angle
var percent: Double
var animatableData: Double {
get { offset.degrees }
set { offset = Angle(degrees: newValue) }
}
func path(in rect: CGRect) -> Path {
var p = Path()
// empirically determined values for wave to be seen
// at 0 and 100 percent
let lowfudge = 0.02
let highfudge = 0.98
let newpercent = lowfudge + (highfudge - lowfudge) * percent
let waveHeight = 0.015 * rect.height
let yoffset = CGFloat(1 - newpercent) * (rect.height - 4 * waveHeight) + 2 * waveHeight
let startAngle = offset
let endAngle = offset + Angle(degrees: 360)
p.move(to: CGPoint(x: 0, y: yoffset + waveHeight * CGFloat(sin(offset.radians))))
for angle in stride(from: startAngle.degrees, through: endAngle.degrees, by: 5) {
let x = CGFloat((angle - startAngle.degrees) / 360) * rect.width
p.addLine(to: CGPoint(x: x, y: yoffset + waveHeight * CGFloat(sin(Angle(degrees: angle).radians))))
}
p.addLine(to: CGPoint(x: rect.width, y: rect.height))
p.addLine(to: CGPoint(x: 0, y: rect.height))
p.closeSubpath()
return p
}
}
struct CircleWaveView: View {
@State private var waveOffset = Angle(degrees: 0)
let percent: Int
var body: some View {
GeometryReader { geo in
ZStack {
Text("\(self.percent)%")
.foregroundColor(.black)
.font(Font.system(size: 0.25 * min(geo.size.width, geo.size.height) ))
Circle()
.stroke(Color.blue, lineWidth: 0.025 * min(geo.size.width, geo.size.height))
.overlay(
Wave(offset: Angle(degrees: self.waveOffset.degrees), percent: Double(percent)/100)
.fill(Color(red: 0, green: 0.5, blue: 0.75, opacity: 0.5))
.clipShape(Circle().scale(0.92))
)
}
}
.aspectRatio(1, contentMode: .fit)
.onAppear {
withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
self.waveOffset = Angle(degrees: 360)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
CircleWaveView(percent: 58)
}
}
SwiftUI Circle line width change stops trim animation
I'm not sure you can directly do what you hope to do. The stroking of the line uses the lineWidth
, so I don't believe you can animate it with a separate time interval.
What you can do is change the lineWidth
over time so that while the circle animation is running and redrawing the circle, it will use the new values.
With that in mind, I created the function changeValueOverTime(value:newValue:duration)
to do this:
struct ContentView: View {
@State var progress: Double = 0.01
@State var lineWidth: CGFloat = 20
var body: some View {
VStack{
CircleView(progress: progress, lineWidth: $lineWidth)
.foregroundColor(.orange)
.onAppear{
withAnimation(.linear(duration: 20)){
progress = 1
}
}
.padding()
Button(action: {
changeValueOverTime(value: $lineWidth, newValue: 40, duration: 0.5)
}, label: {Text("Change Line Width")})
}
}
func changeValueOverTime(value: Binding<CGFloat>, newValue: CGFloat, duration: Double) {
let timeIncrements = 0.02
let steps = Int(duration / timeIncrements)
var count = 0
let increment = (newValue - value.wrappedValue) / CGFloat(steps)
Timer.scheduledTimer(withTimeInterval: timeIncrements, repeats: true) { timer in
value.wrappedValue += increment
count += 1
if count == steps {
timer.invalidate()
}
}
}
}
Animation triggered using a Button stops a repeatForever animation added onAppear
SwiftUI animations are not added(cumulated), at least for now (SwiftUI 2.0). So here is possible workaround.
Tested with Xcode 12 / iOS 14
struct WaterWaveView: View {
@State var progress: CGFloat = 0.1
@State var phase: CGFloat = 0.5
var body: some View {
VStack {
WaterWave(progress: self.progress, phase: self.phase)
.fill(Color.blue)
.clipShape(Circle())
.frame(width: 250, height: 250)
.animation(phase == 0 ? .default : Animation.linear(duration: 1).repeatForever(autoreverses: false), value: phase)
.animation(.easeOut(duration: 1), value: progress)
.onAppear {
self.phase = .pi * 2
}
Button("Add") {
self.phase = 0
self.progress += 0.1
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.phase = .pi * 2
}
}
Button("Reset") {
self.progress = 0.0
}
}
}
}
Animate drawing of a circle
The easiest way to do this is to use the power of core animation to do most of the work for you. To do that, we'll have to move your circle drawing code from your drawRect
function to a CAShapeLayer
. Then, we can use a CABasicAnimation
to animate CAShapeLayer
's strokeEnd
property from 0.0
to 1.0
. strokeEnd
is a big part of the magic here; from the docs:
Combined with the strokeStart property, this property defines the
subregion of the path to stroke. The value in this property indicates
the relative point along the path at which to finish stroking while
the strokeStart property defines the starting point. A value of 0.0
represents the beginning of the path while a value of 1.0 represents
the end of the path. Values in between are interpreted linearly along
the path length.
If we set strokeEnd
to 0.0
, it won't draw anything. If we set it to 1.0
, it'll draw a full circle. If we set it to 0.5
, it'll draw a half circle. etc.
So, to start, lets create a CAShapeLayer
in your CircleView
's init
function and add that layer to the view's sublayers
(also be sure to remove the drawRect
function since the layer will be drawing the circle now):
let circleLayer: CAShapeLayer!
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clearColor()
// Use UIBezierPath as an easy way to create the CGPath for the layer.
// The path should be the entire circle.
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(Double.pi * 2.0), clockwise: true)
// Setup the CAShapeLayer with the path, colors, and line width
circleLayer = CAShapeLayer()
circleLayer.path = circlePath.CGPath
circleLayer.fillColor = UIColor.clearColor().CGColor
circleLayer.strokeColor = UIColor.redColor().CGColor
circleLayer.lineWidth = 5.0;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
layer.addSublayer(circleLayer)
}
Note: We're setting circleLayer.strokeEnd = 0.0
so that the circle isn't drawn right away.
Now, lets add a function that we can call to trigger the circle animation:
func animateCircle(duration: NSTimeInterval) {
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeEnd))
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = 0
animation.toValue = 1
// Do a linear animation (i.e. the speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// right value when the animation ends.
circleLayer.strokeEnd = 1.0
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
}
Then, all we need to do is change your addCircleView
function so that it triggers the animation when you add the CircleView
to its superview
:
func addCircleView() {
let diceRoll = CGFloat(Int(arc4random_uniform(7))*50)
var circleWidth = CGFloat(200)
var circleHeight = circleWidth
// Create a new CircleView
var circleView = CircleView(frame: CGRectMake(diceRoll, 0, circleWidth, circleHeight))
view.addSubview(circleView)
// Animate the drawing of the circle over the course of 1 second
circleView.animateCircle(1.0)
}
All that put together should look something like this:
Note: It won't repeat like that, it'll stay a full circle after it animates.
SwiftUI, create a circle for each CGpoint in Array
For your second approach, you can create a separate path for each point in your array and add them to the path you already defined as var path = Path()
. This way your second approach would work fine
struct MakeCircle: Shape {
var arrayViso: [CGPoint]
func path(in rect: CGRect) -> Path {
var path = Path()
for punti in arrayViso{
var circle = Path()
let radius = 2
circle.addArc(
center: punti,
radius: CGFloat(radius),
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: 360),
clockwise: true)
path.addPath(circle)
}
return path
}
}
Related Topics
Swift: Set Insertion Point in Nstextfield
How to Transfer the User's Score to Another Scene in Swift and Spritekit
"Cgfloat Is Not Convertible to Int" When Trying to Calculate an Expression
How to Share Data Between Tab View Controllers
Can You Override Nsdateformatter 12 VS 24 Hour Time Format Without Using a Custom Dateformat
Scngeometryelement Setup in Swift 3
New Firebase Retrieve Data and Put on the Tableview (Swift)
Add Environmentobject in Swiftui 2.0
Pdfview Does Not Display My PDF
Callkit - How to Bring the Cxcallcontroller to the Front
How to Loop Through an Array from the Second Element in Elegant Way Using Swift
How Should You Handle Closure Arguments for Uialertaction
Differencebetween ":" and "=" in Swift
How to Make an Enum Conform to a Protocol in Swift
Firebasefirestoreswift Won't Install (Cocoapods)
Change Time Interval in Skaction.Waitforduration() as Game Goes On