How to add shadow to SwiftUI arc? - ios

I have the following arc:
struct Arc : Shape
{
#Binding var endAngle: Double
var center: CGPoint
var radius: CGFloat
func path(in rect: CGRect) -> Path
{
var path = Path()
path.addArc(center: center, radius: radius, startAngle: .degrees(270), endAngle: .degrees(endAngle), clockwise: false)
return path.strokedPath(.init(lineWidth: 50, lineCap: .round))
}
}
How can I add shadow, similar to the activity arcs on Apple Watch, such that at full circle the endAngle is still discernible?
EDIT:
There's the additional issue at 360+ degrees (so full circle), that both arc ends get combined and are shown as a radial line (I see this because I've a applied an AngularGradient). Of course Arc won't do advanced things like continuing above the startAngle position, like the Apple Watch arcs do. But this was what I was looking for. Does anyone know how to do that in SwiftUI?

what about this? ok...bit "tricky"
import SwiftUI
struct Arc: Shape {
#Binding var startAngle: Double
#Binding var endAngle: Double
var center: CGPoint
var radius: CGFloat
var color: Color
func path(in rect: CGRect) -> Path {
var path = Path()
let cgPath = CGMutablePath()
cgPath.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: radius, startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: true)
// path.addArc(center: center, radius: radius, startAngle: .degrees(270), endAngle: .degrees(endAngle), clockwise: false)
path = Path(cgPath)
return path.strokedPath(.init(lineWidth: 50, lineCap: .round))
}
}
struct ContentView: View {
var body: some View {
ZStack() {
// Arc(endAngle: .constant(269), center: CGPoint(x: 200, y: 200), radius: 150, color: .red).foregroundColor(.red).opacity(0.2)
Arc(startAngle: .constant(80 + 271), endAngle: .constant(80 + 271 + 340), center: CGPoint(x: 205, y: 205), radius: 150, color: .red).foregroundColor(.red)//.shadow(color: .black, radius: 5, x: -30, y: -170)
Arc(startAngle: .constant(90), endAngle: .constant(80 + 270), center: CGPoint(x: 200, y: 200), radius: 150, color: .red).foregroundColor(.red).shadow(color: .black, radius: 5, x: -114, y: -230)
}
.background(Color.black)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

do you mean something like this?
struct Arc : Shape
{
#Binding var endAngle: Double
var center: CGPoint
var radius: CGFloat
var color: Color
func path(in rect: CGRect) -> Path
{
var path = Path()
path.addArc(center: center, radius: radius, startAngle: .degrees(270), endAngle: .degrees(endAngle), clockwise: false)
return path.strokedPath(.init(lineWidth: 50, lineCap: .round))
}
}
struct ContentView: View {
var body: some View {
ZStack() {
Arc(endAngle: .constant(269), center: CGPoint(x: 200, y: 200), radius: 150, color: .red).foregroundColor(.red).opacity(0.2)
Arc(endAngle: .constant(90), center: CGPoint(x: 200, y: 200), radius: 150, color: .red).foregroundColor(.red)
}
}
}

Hi you can fix and handle all of these features by writing an extension in your project file
import UIKit
extension UIView {
func roundCorners(corners: UIRectCorner, radius: CGFloat) {
let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
self.layer.mask = mask
}
// MARK: Shadow extension UIView {
func dropShadow(scale: Bool = true) {
layer.masksToBounds = false
layer.shadowColor = UIColor.blue.cgColor
layer.shadowOpacity = 1.0
layer.shadowOffset = CGSize(width: -1.5, height: 3)
layer.shadowRadius = 3
layer.shadowPath = UIBezierPath(rect: bounds).cgPath
layer.shouldRasterize = true
layer.rasterizationScale = scale ? UIScreen.main.scale : 1
}
func dropShadow(color: UIColor, opacity: Float = 0.5, offSet: CGSize, radius: CGFloat = 1, scale: Bool = true) {
layer.masksToBounds = false
layer.shadowColor = color.cgColor
layer.shadowOpacity = opacity
layer.shadowOffset = offSet
layer.shadowRadius = radius
layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath
layer.shouldRasterize = true
layer.rasterizationScale = scale ? UIScreen.main.scale : 1
}
func setAnchorPoint(_ point: CGPoint) {
var newPoint = CGPoint(x: bounds.size.width * point.x, y: bounds.size.height * point.y)
var oldPoint = CGPoint(x: bounds.size.width * layer.anchorPoint.x, y: bounds.size.height * layer.anchorPoint.y)
newPoint = newPoint.applying(transform)
oldPoint = oldPoint.applying(transform)
var position = layer.position
position.x -= oldPoint.x
position.x += newPoint.x
position.y -= oldPoint.y
position.y += newPoint.y
layer.position = position
layer.anchorPoint = point
}
class func fromNib<T: UIView>() -> T {
guard let view = Bundle.main.loadNibNamed(String(describing: T.self), owner: nil, options: nil)?.first as? T else { fatalError() }
return view
}
now in storyboard you can handle shadows
or you don't want to, and it's too much, you can use the only part you need.

Related

Chart with UIBezierPath at one below one

I am trying to make pie chart as below which is working fine but I have issue with end path (which is orange in below image).
What I want to do is make end of orange shape to below the green one so that I can achieve as below.
Any suggestion how this can be done?
Code can be found at below link.
https://drive.google.com/file/d/1ST0zNooLgRaI8s2pDK3NMjBQYjBSRoXB/view?usp=sharing
Below is what I have.
func drawBeizer(start_angle : CGFloat, end_angle : CGFloat, final_color : UIColor) {
let path1 : UIBezierPath = UIBezierPath()
path1.addArc(withCenter: CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2), radius: ((self.frame.size.width-main_view_width)/2), startAngle: start_angle, endAngle: end_angle, clockwise: true)
path1.lineWidth = main_view_width
path1.lineCapStyle = .round
final_color.setStroke()
path1.stroke()
}
This function I am passing start angle and end angle & color for the path.
This solution is for SwiftUI using Path.
struct DonutElement: Shape{
var width: CGFloat = 50
var startAngle: Angle
var endAngle: Angle
func path(in rect: CGRect) -> Path {
// define commonly used vars
// the extend of the shape
let outerRadius = min(rect.width, rect.height) / 2
// the middle of the shape
let midRadius = outerRadius - width / 2
// the inner radius
let innerRadius = outerRadius - width
// centerpoint used to move coordinate system in to center
let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
return Path{ path in
// calculate the startpoint from the startAngle. startAngle is allready normalized
let startPoint = startAngle.polarCoordinates(center: center, radius: outerRadius)
// move the path without drawing to the starting position
path.move(to: startPoint)
// add the starting concav arc
// the center point for this arc are the polar coordinates relative to midradius of the donut
path.addArc(center: startAngle.polarCoordinates(center: center, radius: midRadius), radius: width / 2, startAngle: startAngle.normalizing(), endAngle: (.init(degrees: 180) - startAngle), clockwise: true)
// add the arc that presents the inner donut line
// center point is the center of the drawing rect with normalized angles
path.addArc(center: center, radius: innerRadius, startAngle: startAngle.normalizing(), endAngle: endAngle.normalizing(), clockwise: true)
// add the convex arc at the end of the donut element
// switch clockwise to false and swap end and start angle
// replace startAngle with endAngle
path.addArc(center: endAngle.polarCoordinates(center: center, radius: midRadius), radius: width / 2, startAngle: (.init(degrees: 180) - endAngle), endAngle: endAngle.normalizing(), clockwise: false)
// add the outer stroke to close the shape
path.addArc(center: center, radius: outerRadius, startAngle: endAngle.normalizing(), endAngle: startAngle.normalizing(), clockwise: false)
// just in case
path.closeSubpath()
}
}
}
extension Shape {
func fillWithStroke<Fill: ShapeStyle, Stroke: ShapeStyle>(_ fillStyle: Fill, strokeBorder strokeStyle: Stroke, lineWidth: Double = 1) -> some View {
self
.stroke(strokeStyle, lineWidth: lineWidth)
.background(self.fill(fillStyle))
}
}
struct AngleContainer: Identifiable, Hashable{
let id = UUID()
var startAngle: Angle
var endAngle: Angle
var color: Color
}
struct Donut: View{
// each element ends here and starts at the previous or 0. Values in percent
var elementStops: [(CGFloat , Color)] = [(0.4 , .blue), (45 , .red), (55 , .green), (78 , .gray), (100 , .white)]
var width: CGFloat = 50.0
private var angles: [AngleContainer] {
var angles = [AngleContainer]()
for (index, stop) in elementStops.enumerated(){
if index == 0{
let startAngle = Angle(degrees: 0)
let endAngle = Angle(degrees: stop.0/100 * 360)
angles.append(AngleContainer(startAngle: startAngle,endAngle: endAngle,color: stop.1))
} else{
let startAngle = Angle(degrees: elementStops[index - 1].0 / 100 * 360)
let endAngle = Angle(degrees: stop.0/100 * 360)
angles.append(AngleContainer(startAngle: startAngle,endAngle: endAngle,color: stop.1))
}
}
return angles
}
var body: some View{
ZStack{
ForEach(angles){ angleContainer in
DonutElement(width: width, startAngle: angleContainer.startAngle, endAngle: angleContainer.endAngle)
.fillWithStroke(angleContainer.color, strokeBorder: .black.opacity(0.2))
}
}
.background(.white)
}
}
// used for transfering coordinate system
extension CGPoint{
mutating func transfered(to center: CGPoint){
x = center.x + x
y = center.y - y
}
func transfering(to center: CGPoint) -> CGPoint{
.init(x: center.x + x, y: center.y - y)
}
}
I am normalizing angles to behave more like in math with 90 degrees at the top and not at the bottom.
// convenience so angle feels more natural
extension Angle{
func normalizing() -> Angle {
Angle(degrees: 0) - self
}
func polarCoordinates(center: CGPoint, radius: CGFloat) -> CGPoint{
CGPoint(x: cos(self.radians) * radius, y: sin(self.radians) * radius )
.transfering(to: center)
}
}
Result:
I found a way to do this. Below is the full code.
I am adding other addArc using end angles.
ViewController.swift
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var myView: MyView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
myView.setupData(inner_color: [.green, .purple, .blue, .orange], inner_view_width: self.view.frame.width/5.0, inner_angle: [0.0, 90.0, 180.0, 270.0])
myView.backgroundColor = UIColor.white
}
}
MyView.swift
import UIKit
class MyView: UIView {
var main_radius : CGFloat = 0.0
var main_color : [UIColor] = [UIColor.black, UIColor.green]
var main_view_width : CGFloat = 1.0
var main_angle : [CGFloat] = [-CGFloat.pi, 0.0]
var actual_angle : [CGFloat] = []
func setupData(inner_color : [UIColor], inner_view_width : CGFloat, inner_angle : [CGFloat]) {
main_color = inner_color
main_view_width = inner_view_width
main_angle = inner_angle
actual_angle = inner_angle
var temp_main_angle : [CGFloat] = []
for i in 0..<main_angle.count {
temp_main_angle.append(main_angle[i]*Double.pi/180)
}
main_angle = temp_main_angle
draw(CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.width))
}
override func draw(_ rect: CGRect) {
if (main_color.count >= 2) {
var inner_position : Int = 0
// first draw with sharp corner
for i in 0..<main_color.count {
if (i == (main_color.count-1)) {
inner_position = 0
} else {
inner_position = i+1
}
drawBeizer(start_angle: main_angle[i], end_angle: main_angle[inner_position], final_color: main_color[i])
}
// append with ending icons for circle
for i in 0..<main_color.count {
if (i == (main_color.count-1)) {
inner_position = 0
} else {
inner_position = i+1
}
drawBeizer(start_angle: main_angle[inner_position], end_angle: main_angle[inner_position]+(1*Double.pi/180), final_color: main_color[i], withCircle: true) // make here final_color as .black to see what I am doing with this loop
}
}
}
func drawBeizer(start_angle : CGFloat, end_angle : CGFloat, final_color : UIColor, withCircle: Bool = false) {
let path1 : UIBezierPath = UIBezierPath()
path1.addArc(withCenter: CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2), radius: ((self.frame.size.width-main_view_width)/2), startAngle: start_angle, endAngle: end_angle, clockwise: true)
path1.lineWidth = main_view_width
if (withCircle) {
path1.lineCapStyle = .round
}
final_color.setStroke()
path1.stroke()
}
}
Full code here

Draw animated rounded rect line with gradient color

So I'm trying to create a loading indicator on an element, basically it's a path that's expanding and contracting towards the end.
The hard part is that the path is a gradient color which I have no idea to while also animating the paths strokeStart and strokeEnd
Here is what I have so far without the gradient.
//
// Created by Tommy Sadiq Hinrichsen on 07/07/2021.
// Copyright (c) 2021 Dagrofa. All rights reserved.
//
import Foundation
import UIKit
#IBDesignable
class LoadingBorderView: UIView {
private let strokeLayer = CAShapeLayer()
var startPosition: UIRectEdge = .right {
didSet { self.setNeedsDisplay() }
}
private var cornerRadius: CGFloat { return self.frame.height / 2 }
override init(frame: CGRect) {
super.init(frame: frame)
self.configureView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.configureView()
}
private func configureView() {
self.strokeLayer.fillColor = nil
self.strokeLayer.strokeColor = UIColor.black.cgColor
self.strokeLayer.strokeEnd = 0
self.strokeLayer.lineCap = .round
self.layer.addSublayer(self.strokeLayer)
}
private func getRoundedPath() -> UIBezierPath {
let path = UIBezierPath()
let drawTop: (UIBezierPath) -> Void = { (path: UIBezierPath) in
path.move(to: CGPoint(x: 0, y: self.cornerRadius))
path.addArc(withCenter: CGPoint(x: self.cornerRadius, y: self.cornerRadius), radius: self.cornerRadius, startAngle: 180.cgfloat.toRadians(), endAngle: 270.cgfloat.toRadians(), clockwise: true)
path.addLine(to: CGPoint(x: self.bounds.width - self.cornerRadius, y: 0))
path.addArc(withCenter: CGPoint(x: self.frame.width - self.cornerRadius, y: self.cornerRadius), radius: self.cornerRadius, startAngle: -90.cgfloat.toRadians(), endAngle: 0.cgfloat.toRadians(), clockwise: true)
}
let drawBottom: (UIBezierPath) -> Void = { (path: UIBezierPath) in
path.move(to: CGPoint(x: self.frame.width, y: self.cornerRadius))
path.addArc(withCenter: CGPoint(x: self.frame.width - self.cornerRadius, y: self.cornerRadius), radius: self.cornerRadius, startAngle: 0.cgfloat.toRadians(), endAngle: 90.cgfloat.toRadians(), clockwise: true)
path.addLine(to: CGPoint(x: self.cornerRadius, y: self.frame.height))
path.addArc(withCenter: CGPoint(x: self.cornerRadius, y: self.cornerRadius), radius: self.cornerRadius, startAngle: 90.cgfloat.toRadians(), endAngle: 180.cgfloat.toRadians(), clockwise: true)
}
switch self.startPosition {
case .right:
drawBottom(path)
drawTop(path)
case .left:
drawTop(path)
drawBottom(path)
default:
fatalError("LoadingBorderView must start from left or right")
}
return path
}
func startAnimation(duration: TimeInterval = 2.0, delay: TimeInterval = 0.3) {
let strokeStartAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeStart))
strokeStartAnimation.beginTime = delay
strokeStartAnimation.duration = duration
strokeStartAnimation.fromValue = 0
strokeStartAnimation.toValue = 1
let strokeEndAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeEnd))
strokeEndAnimation.duration = duration
strokeEndAnimation.fromValue = 0
strokeEndAnimation.toValue = 1
strokeEndAnimation.fillMode = .forwards
let group = CAAnimationGroup()
group.beginTime = CACurrentMediaTime()
group.timingFunction = .easeInOutCubic
group.duration = duration + delay
group.repeatCount = .infinity
group.animations = [strokeStartAnimation, strokeEndAnimation]
self.strokeLayer.add(group, forKey: "strokeLayer")
}
func stopAnimation() {
self.strokeLayer.removeAllAnimations()
}
override func draw(_ rect: CGRect) {
self.strokeLayer.frame = self.bounds
self.strokeLayer.path = self.getRoundedPath().cgPath
}
}
public extension CAMediaTimingFunction {
///https://easings.net/#easeInOutCubic
static var easeInOutCubic = CAMediaTimingFunction(controlPoints: 0.65, 0, 0.35, 1)
}

UIBezierPath partially stroke

I have this code to draw a rectangle which is rounded rect only on one side.
override func draw(_ rect: CGRect) {
// Drawing code
guard let context = UIGraphicsGetCurrentContext() else { return }
let lineWidth = CGFloat(4)
let pathRect = CGRect(x: 0, y: 0, width: rect.width, height: rect.height)
let path = UIBezierPath(roundedRect: pathRect.inset(by: UIEdgeInsets(top: lineWidth, left: lineWidth, bottom: lineWidth, right: 0)), byRoundingCorners: [.topLeft, .bottomLeft], cornerRadii: CGSize(width: 7, height: 7))
context.setFillColor(UIColor.black.cgColor)
path.fill()
context.setLineWidth(lineWidth)
}
I want to stroke it with red color on all but the right edge (no stroke on the right edge). How do I do it?
You’ll have to create your own path.
A couple of observations:
Don’t use the rect parameter. The rect is what is being asked to being drawn at this point in time, which may not be the entire view. Use bounds when figuring out what the overall path should be.
I might inset the path so that the stroke stays within the bounds of the view.
You can make this #IBDesignable if you want to also be able to see it rendered in IB.
You don’t really need UIGraphicsGetCurrentContext(). The UIKit methods fill(), stroke(), setFill(), and setStroke() methods automatically use the current context.
Thus:
#IBDesignable
class OpenRightView: UIView {
#IBInspectable var lineWidth: CGFloat = 4 { didSet { setNeedsDisplay() } }
#IBInspectable var radius: CGFloat = 7 { didSet { setNeedsDisplay() } }
#IBInspectable var fillColor: UIColor = .black { didSet { setNeedsDisplay() } }
#IBInspectable var strokeColor: UIColor = .red { didSet { setNeedsDisplay() } }
override func draw(_ rect: CGRect) {
let pathRect = bounds.inset(by: .init(top: lineWidth / 2, left: lineWidth / 2, bottom: lineWidth / 2, right: 0))
let path = UIBezierPath()
path.lineWidth = lineWidth
path.move(to: CGPoint(x: pathRect.maxX, y: pathRect.minY))
path.addLine(to: CGPoint(x: pathRect.minX + radius, y: pathRect.minY))
path.addQuadCurve(to: CGPoint(x: pathRect.minX, y: pathRect.minY + radius), controlPoint: pathRect.origin)
path.addLine(to: CGPoint(x: pathRect.minX, y: pathRect.maxY - radius))
path.addQuadCurve(to: CGPoint(x: pathRect.minX + radius, y: pathRect.maxY), controlPoint: CGPoint(x: pathRect.minX, y: pathRect.maxY))
path.addLine(to: CGPoint(x: pathRect.maxX, y: pathRect.maxY))
fillColor.setFill()
path.fill()
strokeColor.setStroke()
path.stroke()
}
}
That yields:
Theoretically, it might be more efficient to use CAShapeLayer and let Apple take care of the draw(_:) for us. E.g., they may have optimized the rendering to handle partial view updates, etc.
That might look like the following:
#IBDesignable
class OpenRightView: UIView {
#IBInspectable var lineWidth: CGFloat = 4 { didSet { updatePath() } }
#IBInspectable var radius: CGFloat = 7 { didSet { updatePath() } }
#IBInspectable var fillColor: UIColor = .black { didSet { shapeLayer.fillColor = fillColor.cgColor } }
#IBInspectable var strokeColor: UIColor = .red { didSet { shapeLayer.strokeColor = strokeColor.cgColor } }
lazy var shapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = fillColor.cgColor
shapeLayer.strokeColor = strokeColor.cgColor
shapeLayer.lineWidth = lineWidth
return shapeLayer
}()
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
override func layoutSubviews() {
super.layoutSubviews()
updatePath()
}
}
private extension OpenRightView {
func configure() {
layer.addSublayer(shapeLayer)
}
func updatePath() {
let pathRect = bounds.inset(by: .init(top: lineWidth / 2, left: lineWidth / 2, bottom: lineWidth / 2, right: 0))
let path = UIBezierPath()
path.move(to: CGPoint(x: pathRect.maxX, y: pathRect.minY))
path.addLine(to: CGPoint(x: pathRect.minX + radius, y: pathRect.minY))
path.addQuadCurve(to: CGPoint(x: pathRect.minX, y: pathRect.minY + radius), controlPoint: pathRect.origin)
path.addLine(to: CGPoint(x: pathRect.minX, y: pathRect.maxY - radius))
path.addQuadCurve(to: CGPoint(x: pathRect.minX + radius, y: pathRect.maxY), controlPoint: CGPoint(x: pathRect.minX, y: pathRect.maxY))
path.addLine(to: CGPoint(x: pathRect.maxX, y: pathRect.maxY))
shapeLayer.path = path.cgPath
shapeLayer.lineWidth = lineWidth
}
}

How to draw 4 dash lines on the corners of CAShapeLayer? [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 3 years ago.
Improve this question
I have rounded rect:
let rect = UIBezierPath(roundedRect: frame, cornerRadius: cornerRadius)
and I need to this drawing path add 4 dashes lines on each corner
Expected result:
I need in swift 5 and add to this path, because by this path I draw general mask for view
I use this path for CAShapeLayer, and I need this dashes just for this CAShapeLayer
Hey so I decided to go nuts and create an extension for UIView for what you are looking for. It's as follows:
extension UIView {
private struct Properties {
static var _radius: CGFloat = 0.0
static var _color: UIColor = .red
static var _strokeWidth: CGFloat = 1.0
static var _length: CGFloat = 20.0
}
private var radius: CGFloat {
get {
return Properties._radius
}
set {
Properties._radius = newValue
}
}
private var color: UIColor {
get {
return Properties._color
}
set {
Properties._color = newValue
}
}
private var strokeWidth: CGFloat {
get {
return Properties._strokeWidth
}
set {
Properties._strokeWidth = newValue
}
}
private var length: CGFloat {
get {
return Properties._length
}
set {
Properties._length = newValue
}
}
func drawCorners(radius: CGFloat? = nil, color: UIColor? = nil, strokeWidth: CGFloat? = nil, length: CGFloat? = nil) {
if let radius = radius {
self.radius = radius
}
if let color = color {
self.color = color
}
if let strokeWidth = strokeWidth {
self.strokeWidth = strokeWidth
}
if let length = length {
self.length = length
}
createTopLeft()
createTopRight()
createBottomLeft()
createBottomRight()
}
private func createTopLeft() {
let topLeft = UIBezierPath()
topLeft.move(to: CGPoint(x: strokeWidth/2, y: radius+length))
topLeft.addLine(to: CGPoint(x: strokeWidth/2, y: radius))
topLeft.addQuadCurve(to: CGPoint(x: radius, y: strokeWidth/2), controlPoint: CGPoint(x: strokeWidth/2, y: strokeWidth/2))
topLeft.addLine(to: CGPoint(x: radius+length, y: strokeWidth/2))
setupShapeLayer(with: topLeft)
}
private func createTopRight() {
let topRight = UIBezierPath()
topRight.move(to: CGPoint(x: frame.width-radius-length, y: strokeWidth/2))
topRight.addLine(to: CGPoint(x: frame.width-radius, y: strokeWidth/2))
topRight.addQuadCurve(to: CGPoint(x: frame.width-strokeWidth/2, y: radius), controlPoint: CGPoint(x: frame.width-strokeWidth/2, y: strokeWidth/2))
topRight.addLine(to: CGPoint(x: frame.width-strokeWidth/2, y: radius+length))
setupShapeLayer(with: topRight)
}
private func createBottomRight() {
let bottomRight = UIBezierPath()
bottomRight.move(to: CGPoint(x: frame.width-strokeWidth/2, y: frame.height-radius-length))
bottomRight.addLine(to: CGPoint(x: frame.width-strokeWidth/2, y: frame.height-radius))
bottomRight.addQuadCurve(to: CGPoint(x: frame.width-radius, y: frame.height-strokeWidth/2), controlPoint: CGPoint(x: frame.width-strokeWidth/2, y: frame.height-strokeWidth/2))
bottomRight.addLine(to: CGPoint(x: frame.width-radius-length, y: frame.height-strokeWidth/2))
setupShapeLayer(with: bottomRight)
}
private func createBottomLeft() {
let bottomLeft = UIBezierPath()
bottomLeft.move(to: CGPoint(x: radius+length, y: frame.height-strokeWidth/2))
bottomLeft.addLine(to: CGPoint(x: radius, y: frame.height-strokeWidth/2))
bottomLeft.addQuadCurve(to: CGPoint(x: strokeWidth/2, y: frame.height-radius), controlPoint: CGPoint(x: strokeWidth/2, y: frame.height-strokeWidth/2))
bottomLeft.addLine(to: CGPoint(x: strokeWidth/2, y: frame.height-radius-length))
setupShapeLayer(with: bottomLeft)
}
private func setupShapeLayer(with path: UIBezierPath) {
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = color.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = strokeWidth
shapeLayer.path = path.cgPath
layer.addSublayer(shapeLayer)
}
}
Then you can use it on any UIView. So once you've created a view, you can then use it as follows:
let myView = UIView(frame: CGRect(x: 50, y: 50, width: 300, height: 300))
myView.drawCorners(radius: 30.0, color: .red, strokeWidth: 10.0, length: 30)
And that's all you need to do! The output on a blue background is as follows:

iOS Swift - Animate drawing of arc filled, not the border stroke

I am trying to Implement drawing of Arc from certain start angle to certain end angle. I am trying to animate drawing of arc but have no luck.
I have looked for couple of implementations but all of them refers to be drawing of animating boder of circle.
Like drawing a pie but with one color. With certain start and end angle. Animated drawing of pie.
What I like is to have have animated filling while arc is being animated from start angle to end angle.
Like arc is being drawn clockwise and while drawing it is filling its inside color.
Any idea How can I acheive this in Swift?
Thanks in advance.
Edit
This is what I have drawn:
(gray color shows area covered by drawing pie)
Code for drawing Pie
import Foundation
import UIKit
#IBDesignable class PieChart : UIView {
#IBInspectable var percentage: CGFloat = 50 {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var outerBorderWidth: CGFloat = 2 {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var outerBorderColor: UIColor = UIColor.white {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var percentageFilledColor: UIColor = UIColor.primary {
didSet {
self.setNeedsDisplay()
}
}
let circleLayer = CAShapeLayer()
override func draw(_ rect: CGRect) {
drawPie(rect: rect, endPercent: percentage, color: UIColor.primary)
}
func drawPie(rect: CGRect, endPercent: CGFloat = 70, color: UIColor) {
let center = CGPoint(x: rect.origin.x + rect.width / 2, y: rect.origin.y + rect.height / 2)
let radius = min(rect.width, rect.height) / 2
let gpath = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: CGFloat(360.degreesToRadians), clockwise: true)
UIColor.white.setFill()
gpath.fill()
let circlePath = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(0), endAngle:CGFloat(360.degreesToRadians), clockwise: true)
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = outerBorderColor.cgColor
circleLayer.lineWidth = outerBorderWidth
circleLayer.borderColor = outerBorderColor.cgColor
self.layer.addSublayer(circleLayer)
let π: CGFloat = 3.14
let halfPi: CGFloat = π / 2
let startPercent: CGFloat = 0.0
let startAngle = (startPercent / 100 * π * 2 - π ) + halfPi
let endAngle = (endPercent / 100 * π * 2 - π ) + halfPi
let path = UIBezierPath()
path.move(to: center)
path.addArc(withCenter: center, radius: radius, startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: true)
path.close()
percentageFilledColor.setFill()
path.fill()
}
}
I've created this code using playground and swift. This code will draw a pie chart animating the filling color. Make sure you have the Timeline pane open on Xcode to see the animation working. I couldn't find a way to pass the path function directly to CAAnimation so my workaround was to create a list of steps, each one containing the path for a percentage stage. You can change the steps for a smother animation.
//: Playground - noun: a place where people can play
import UIKit
import XCPlayground
let STEPS_ANIMATION = 50
let initialPercentage : CGFloat = 0.10
let finalPercentage : CGFloat = 0.75
func buildPiePath(frame : CGRect, percentage : CGFloat) -> UIBezierPath {
let newPath = UIBezierPath()
let startPoint = CGPoint(x: frame.size.width, y: frame.height / 2.0)
let centerPoint = CGPoint(x: frame.size.width / 2.0, y: frame.height / 2.0)
let startAngle : CGFloat = 0
let endAngle : CGFloat = percentage * 2 * CGFloat(M_PI)
newPath.move(to: centerPoint)
newPath.addLine(to: startPoint)
newPath.addArc(withCenter: centerPoint,
radius: frame.size.width / 2.0,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
newPath.addLine(to: centerPoint)
newPath.close()
return newPath
}
func buildPiePathList(frame: CGRect,
startPercentage: CGFloat,
finalPercentage: CGFloat) -> [AnyObject] {
var listValues = [AnyObject]()
for index in 1...STEPS_ANIMATION {
let delta = finalPercentage - startPercentage
let currentPercentage = CGFloat(index) / CGFloat(STEPS_ANIMATION)
let percentage = CGFloat(startPercentage + (delta * currentPercentage))
listValues.append(buildPiePath(frame: frame,
percentage: percentage)
.cgPath)
}
return listValues
}
// Container for pie chart
let container = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
container.backgroundColor = UIColor.gray
XCPShowView(identifier: "Container View", view: container)
let circleFrame = CGRect(x: 20, y: 20, width: 360, height: 360)
// Red background
let background = CAShapeLayer()
background.frame = circleFrame
background.fillColor = UIColor.red.cgColor
background.path = buildPiePath(frame: circleFrame, percentage: 1.0).cgPath
container.layer.addSublayer(background)
// Green foreground that animates
let foreground = CAShapeLayer()
foreground.frame = circleFrame
foreground.fillColor = UIColor.green.cgColor
foreground.path = buildPiePath(frame: circleFrame, percentage: initialPercentage).cgPath
container.layer.addSublayer(foreground)
// Filling animation
let fillAnimation = CAKeyframeAnimation(keyPath: "path")
fillAnimation.values = buildPiePathList(frame: circleFrame,
startPercentage: initialPercentage,
finalPercentage: finalPercentage)
fillAnimation.duration = 3
fillAnimation.calculationMode = kCAAnimationDiscrete
foreground.add(fillAnimation, forKey:nil)

Resources