How to provide maintain spacing between different CALayers - ios

Heading ##I'm trying to learn the charts and having trouble
Adding consistent space between the slices.
Start the animation in sequence.
The reason, I didn't want the separator as a separate arch is to have both the edges rounded. Adding a separator as another layer overlaps the rounded corners.
Any help or pointers is highly appreciated.
import UIKit
import PlaygroundSupport
var str = "Hello, playground"
struct dataItem {
var color: UIColor
var percentage: CGFloat
}
typealias pieAngle = (start: CGFloat, end: CGFloat, color: UIColor)
let pieDataToDisplay = [dataItem(color: .red, percentage: 10),
dataItem(color: .blue, percentage: 20),
dataItem(color: .green, percentage: 25),
dataItem(color: .yellow, percentage: 25),
dataItem(color: .orange, percentage: 10)]
class USBCircleChart: UIView {
private var piesToDisplay: [dataItem] = [] { didSet { setNeedsLayout() } }
private var seperatorSpace: Double = 2.0 { didSet { setNeedsLayout() } }
func fillDataForChart(with items: [dataItem] ) {
self.piesToDisplay.append(contentsOf: items)
print("getting data \(self.piesToDisplay)")
layoutIfNeeded()
}
override func layoutSubviews() {
super.layoutSubviews()
guard piesToDisplay.count > 0 else { return }
print("laying out data")
let angles = calcualteStartAndEndAngle(items: piesToDisplay)
for i in angles {
var dataItem = i
addSpace(data: &dataItem)
addShapeToCircle(data: dataItem)
}
}
func addSpace(data:inout pieAngle) -> pieAngle {
// If space is not added, then its collated at the end, we have to scatter it between each item.
//data.end -= CGFloat(seperatorSpace)
return data
}
func addShapeToCircle(data : pieAngle, percent: CGFloat) {
let center = CGPoint(x: bounds.origin.x + bounds.size.width / 2, y: bounds.origin.y + bounds.size.height / 2)
var shapeLayer = CAShapeLayer()
// radians = degrees * pi / 180
// x*2 + y*2 = r*2
//cos teta = x/r --> x = r * cos teta
// sinn teta = y/ r --> y = r * sin teta
// let x = 100 * cos(data.start)
// let y = 100 * sin(data.end)
let radius = (bounds.origin.x + bounds.size.width / 2 - (sliceThickness)) / 2
//This is the circle path drawn.
let circularPath = UIBezierPath(arcCenter: .zero, radius: self.frame.width / 2, startAngle: data.start, endAngle: data.end, clockwise: true) //2*CGFloat.pi
shapeLayer.path = circularPath.cgPath
//Provide a bounding box for the shape layer to handle events
//Removing the below line works but will not handle touch events :(
shapeLayer.bounds = circularPath.cgPath.boundingBox
//Start the angle from anyplace you need { + - of Pi} // {0, 0.5 pi, 1 pi, 1.5pi}
// shapeLayer.transform = CATransform3DMakeRotation(-CGFloat.pi / 2 , 0, 0, 1)
// color of the stroke
shapeLayer.strokeColor = data.color.cgColor
//Width of stoke
shapeLayer.lineWidth = sliceThickness
//Starts from the center of the view
shapeLayer.position = center
//To provide a rounded cap on the stroke
shapeLayer.lineCap = .round
//Fills the entire circle with this color
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeEnd = 0
basicAnim(shapeLayer: &shapeLayer, percentage: percent)
layer.addSublayer(shapeLayer)
}
func basicAnim(shapeLayer: inout CAShapeLayer) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = 10
//Forwards will hold the layer after completion
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "shapeLayerAniamtion")
}
// //Calucate percentage based on given values
// public func calculateAngle(percentageVal:Double)-> CGFloat {
// return CGFloat((percentageVal / 100) * 360)
// let val = CGFloat (percentageVal / 100.0)
// return val * 2 * CGFloat.pi
// }
private func calcualteStartAndEndAngle(items : [dataItem])-> [pieAngle] {
var angle: pieAngle
var angleToStart: CGFloat = 0.0
//Add the total separator space to the circle so we can accurately measure the start point with space.
var totalSeperatorSpace = Double(items.count) * separatorSpace
var totalSum = items.reduce(CGFloat(totalSeperatorSpace)) { return $0 + $1.percentage }
var angleList: [pieAngle] = []
for item in items {
//Find the end angle based on the percentage in the total circle
let endAngle = (item.percentage / totalSum * 2 * .pi) + angleToStart
angle.0 = angleToStart
angle.1 = endAngle
angle.2 = item.color
angleList.append(angle)
angleToStart = endAngle
//print(angle)
}
return angleList
}
}
let container = UIView()
container.frame.size = CGSize(width: 360, height: 360)
container.backgroundColor = .white
PlaygroundPage.current.liveView = container
PlaygroundPage.current.needsIndefiniteExecution = true
let m = USBCircleChart(frame: CGRect(x: 0, y: 0, width: 215, height: 215))
m.center = CGPoint(x: container.bounds.size.width / 2, y: container.bounds.size.height / 2)
m.fillDataForChart(with: pieDataToDisplay)
container.addSubview(m)
UPDATED :
Updated the code to include proper spacing irrespective of single/multiple items on the chart with equal distribution of total spacing, based on a suggestion from #jaferAli
Open Issue: Handling tap gesture on the layer so I can perform custom actions based on the category selected.
Screen 2
UPDATED CODE:
import UIKit
import PlaygroundSupport
var str = "Hello, playground"
struct dataItem {
var color: UIColor
var percentage: CGFloat
}
func hexStringToUIColor (hex:String) -> UIColor {
var cString:String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
if (cString.hasPrefix("#")) {
cString.remove(at: cString.startIndex)
}
if ((cString.count) != 6) {
return UIColor.gray
}
var rgbValue:UInt64 = 0
Scanner(string: cString).scanHexInt64(&rgbValue)
return UIColor(
red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
alpha: CGFloat(1.0)
)
}
typealias pieAngle = (start: CGFloat, end: CGFloat, color: UIColor, percent: CGFloat)
let pieDataToDisplay = [
dataItem(color: hexStringToUIColor(hex: "#E61628"), percentage: 10),
dataItem(color: hexStringToUIColor(hex: "#50B7FB"), percentage: 20),
dataItem(color: hexStringToUIColor(hex: "#38BE72"), percentage: 25),
dataItem(color: hexStringToUIColor(hex: "#FFAA4C"), percentage: 15),
dataItem(color: hexStringToUIColor(hex: "#B6BE33"), percentage: 30)
]
let pieDataToDisplayWhite = [dataItem(color: .white, percentage: 10),
dataItem(color: .white, percentage: 20),
dataItem(color: .white, percentage: 25),
dataItem(color: .white, percentage: 25),
dataItem(color: .orange, percentage: 10)]
class USBCircleChart: UIView {
private var piesToDisplay: [dataItem] = [] { didSet { setNeedsLayout() } }
private var seperatorSpace: Double = 5.0 { didSet { setNeedsLayout() } }
private var sliceThickness: CGFloat = 10.0 { didSet { setNeedsLayout() } }
func fillDataForChart(with items: [dataItem] ) {
self.piesToDisplay.append(contentsOf: items)
print("getting data \(self.piesToDisplay)")
layoutIfNeeded()
}
override func layoutSubviews() {
super.layoutSubviews()
guard piesToDisplay.count > 0 else { return }
print("laying out data")
let angles = calcualteStartAndEndAngle(items: piesToDisplay)
for i in angles {
var dataItem = i
addSpace(data: &dataItem)
addShapeToCircle(data: dataItem, percent:i.percent)
}
}
func addSpace(data:inout pieAngle) -> pieAngle {
// If space is not added, then its collated at the end, we have to scatter it between each item.
//data.end -= CGFloat(seperatorSpace)
return data
}
func addShapeToCircle(data : pieAngle, percent: CGFloat) {
let center = CGPoint(x: bounds.origin.x + bounds.size.width / 2, y: bounds.origin.y + bounds.size.height / 2)
var shapeLayer = CAShapeLayer()
// radians = degrees * pi / 180
// x*2 + y*2 = r*2
//cos teta = x/r --> x = r * cos teta
// sinn teta = y/ r --> y = r * sin teta
// let x = 100 * cos(data.start)
// let y = 100 * sin(data.end)
let radius = (bounds.origin.x + bounds.size.width / 2 - (sliceThickness)) / 2
//This is the circle path drawn.
let circularPath = UIBezierPath(arcCenter: .zero, radius: self.frame.width / 2, startAngle: data.start, endAngle: data.end, clockwise: true) //2*CGFloat.pi
shapeLayer.path = circularPath.cgPath
//Provide a bounding box for the shape layer to handle events
//shapeLayer.bounds = circularPath.cgPath.boundingBox
//Start the angle from anyplace you need { + - of Pi} // {0, 0.5 pi, 1 pi, 1.5pi}
// shapeLayer.transform = CATransform3DMakeRotation(-CGFloat.pi / 2 , 0, 0, 1)
// color of the stroke
shapeLayer.strokeColor = data.color.cgColor
//Width of stoke
shapeLayer.lineWidth = sliceThickness
//Starts from the center of the view
shapeLayer.position = center
//To provide a rounded cap on the stroke
shapeLayer.lineCap = .round
//Fills the entire circle with this color
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeEnd = 0
basicAnim(shapeLayer: &shapeLayer, percentage: percent)
layer.addSublayer(shapeLayer)
}
func basicAnim(shapeLayer: inout CAShapeLayer) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = 10
//Forwards will hold the layer after completion
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "shapeLayerAniamtion")
}
private var timeOffset:CFTimeInterval = 0
func basicAnim(shapeLayer: inout CAShapeLayer, percentage:CGFloat) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = CFTimeInterval(percentage / 50)
basicAnimation.beginTime = CACurrentMediaTime() + timeOffset
print("timeOffset:\(timeOffset),")
//Forwards will hold the layer after completion
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "shapeLayerAniamtion")
timeOffset += CFTimeInterval(percentage / 50)
}
private func calcualteStartAndEndAngle(items : [dataItem])-> [pieAngle] {
var angle: pieAngle
var angleToStart: CGFloat = 0.0
//Add the total separator space to the circle so we can accurately measure the start point with space.
let totalSeperatorSpace = Double(items.count)
let totalSum = items.reduce(CGFloat(seperatorSpace)) { return $0 + $1.percentage }
let spacing = CGFloat(seperatorSpace ) / CGFloat (totalSum)
print("total Sum:\(spacing)")
var angleList: [pieAngle] = []
for item in items {
//Find the end angle based on the percentage in the total circle
let endAngle = (item.percentage / totalSum * 2 * CGFloat.pi) + angleToStart
print("start:\(angleToStart) end:\(endAngle)")
angle.0 = angleToStart + spacing
angle.1 = endAngle - spacing
angle.2 = item.color
angle.3 = item.percentage
angleList.append(angle)
angleToStart = endAngle + spacing
//print(angle)
}
return angleList
}
}
extension USBCircleChart {
#objc func handleTap() {
print("getting tap action")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
guard let loca = touch?.location(in: self) else { return }
let point = self.convert(loca, from: nil)
guard let sublayers = self.layer.sublayers as? [CAShapeLayer] else { return }
for layer in sublayers {
print("checking paths \(point) \(loca) \(layer.path) \n")
if let path = layer.path, path.contains(point) {
print(layer)
}
}
}
}
let container = UIView()
container.frame.size = CGSize(width: 300, height: 300)
container.backgroundColor = .white
PlaygroundPage.current.liveView = container
PlaygroundPage.current.needsIndefiniteExecution = true
let m = USBCircleChart(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
//m.center = CGPoint(x: container.bounds.size.width / 2, y: container.bounds.size.height / 2)
m.center = container.center
m.fillDataForChart(with: pieDataToDisplay)
container.addSubview(m)

You need to calculate spacing and then add and Subtract it from start and end angle .. so update your calcualteStartAndEndAngle Method with this one
private func calcualteStartAndEndAngle(items : [dataItem])-> [pieAngle] {
var angle: pieAngle
var angleToStart: CGFloat = 0.0
//Add the total separator space to the circle so we can accurately measure the start point with space.
let totalSeperatorSpace = Double(items.count)
let totalSum = items.reduce(CGFloat(totalSeperatorSpace)) { return $0 + $1.percentage }
let spacing = CGFloat( totalSeperatorSpace + 1 ) / totalSum
print("total Sum:\(spacing)")
var angleList: [pieAngle] = []
for item in items {
//Find the end angle based on the percentage in the total circle
let endAngle = (item.percentage / totalSum * 2 * .pi) + angleToStart
print("start:\(angleToStart) end:\(endAngle)")
angle.0 = angleToStart + spacing
angle.1 = endAngle - spacing
angle.2 = item.color
angleList.append(angle)
angleToStart = endAngle + spacing
//print(angle)
}
return angleList
}
It will Result this Animation
and if you want linear Animation then change your animation method
private var timeOffset:CFTimeInterval = 0
func basicAnim(shapeLayer: inout CAShapeLayer, percentage:CGFloat) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = CFTimeInterval(percentage)
basicAnimation.beginTime = CACurrentMediaTime() + timeOffset
print("timeOffset:\(timeOffset),")
//Forwards will hold the layer after completion
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "shapeLayerAniamtion")
timeOffset += CFTimeInterval(percentage)
}
And if you want to learn more you can see this framework RingPieChart

The problem was
1. Re - Calculate the percentages by keeping the spacing percentage.
that is,
//This is to recalculate the percentage by adding the total spacing percentage.
/// Example : The percenatge of each category is recalculated - for instance , lets assume Apple - 60 %,
/// Android - 40 %, now we add Samsung as 10 %, which equates to 110%, To correct this
/// Apple 60 * (100- Samsung) / 100 = 54 %, Android = 36 %, which totals to 100 %.
///
/// - Parameter buffer: total spacing between the splices.
func updatedPercentage(with buffer: CGFloat ) -> CGFloat {
return percentage * (100 - buffer) / 100
}
Once this is done, the total categories + spacings will equate to 100 %.
The only problem left is, for very smaller percentage categories (lesser than spacing percentage), the start angle will be greater than end angle. This is because we are subtracting the spacing from end angle.
there are two options to correct,
a. flip the angles.
if angle.start > angle.end {
let start = angle.start
angle.start = angle.end
angle.end = start
}
b. draw it anti clock wise in Beizer path , only for that slice.
let circularPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: angle.start, endAngle: angle.end, clockwise: **angle.start < angle.end**)
this should solve all the problems, i will upload my findings on a GIT repo and publish the link here.

Related

How to create Circle with step progress (gaps in it) and animate it?

I need to create a progressive with gaps in it and Animate the layers. I have achieved it. But the problem is it is starting (0) from Right centre. But the requirement is it should start from top centre. In image You can see that it is started from right side.
I have attached my code sample along with Image for your understanding. Can somebody help me where I'm doing wrong or how should I make it from top.
extension ViewController {
func sampleProgress() {
let totalSteps = 6
let frame = CGRect(x: 50, y: 50, width: 120, height: 120)
let circlePath = UIBezierPath(ovalIn: frame)
let gapSize: CGFloat = 0.0125
let segmentAngle: CGFloat = 0.167 // (1/totalSteps)
var startAngle = 0.0
let lineWidth = 8.0
for index in 0 ... totalSteps {
// Background layer
let backgroundLayer = CAShapeLayer()
backgroundLayer.strokeStart = startAngle
backgroundLayer.strokeEnd = backgroundLayer.strokeStart + segmentAngle - gapSize
backgroundLayer.path = circlePath.cgPath
backgroundLayer.name = String(index)
backgroundLayer.strokeColor = UIColor.lightGray.cgColor
backgroundLayer.lineWidth = lineWidth
backgroundLayer.lineCap = CAShapeLayerLineCap.butt
backgroundLayer.fillColor = UIColor.clear.cgColor
self.view.layer.addSublayer(backgroundLayer)
// Foreground layer
let foregroundLayer = CAShapeLayer()
foregroundLayer.strokeStart = startAngle
foregroundLayer.strokeEnd = backgroundLayer.strokeStart + segmentAngle - gapSize
foregroundLayer.isHidden = true
foregroundLayer.name = String(index) + String(index)
foregroundLayer.path = circlePath.cgPath
foregroundLayer.strokeColor = UIColor.green.cgColor
foregroundLayer.lineWidth = lineWidth
foregroundLayer.lineCap = CAShapeLayerLineCap.butt
foregroundLayer.fillColor = UIColor.clear.cgColor
self.view.layer.addSublayer(foregroundLayer)
print("Start angle: \(startAngle)")
startAngle = startAngle + segmentAngle
}
}
func animateLayer(isAnimate: Bool, stepsToAnimate: Int) {
let segmentAngle: CGFloat = (360 * 0.166) / 360
let gapSize: CGFloat = 0.0125
var startAngle = 0.0
for index in 0 ... stepsToAnimate {
if let foregroundLayers = self.view.layer.sublayers {
for animateLayer in foregroundLayers {
if animateLayer.name == String(index) + String(index) {
if index == stepsToAnimate && isAnimate {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = startAngle
animation.toValue = startAngle + segmentAngle - gapSize
animation.duration = 1.0
animateLayer.add(animation, forKey: "foregroundAnimation")
animateLayer.isHidden = false
} else {
animateLayer.isHidden = false
}
startAngle = startAngle + segmentAngle
}
}
}
}
}
}
You can "move the start" to the top by rotating the layer(s) minus 90-degrees:
let tr = CATransform3DMakeRotation(-(.pi * 0.5), 0, 0, 1)
I would assume this would be wrapped into a UIView subclass, but to get your example (adding sublayers to the main view's layer) to work right, we'll want to use a Zero-based origin for the path rect:
// use 0,0 for the origin of the PATH frame
let frame = CGRect(x: 0, y: 0, width: 120, height: 120)
let circlePath = UIBezierPath(ovalIn: frame)
and then an offset rect for the position:
let layerFrame = frame.offsetBy(dx: 50, dy: 50)
and we set the .anchorPoint of the layers to the center of that rect -- so it will rotate around its center:
// set the layer's frame
backgroundLayer.frame = layerFrame
// set the layer's anchor point
backgroundLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
// apply the rotation transform
backgroundLayer.transform = tr
// set the layer's frame
foregroundLayer.frame = layerFrame
// set the layer's anchor point
foregroundLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
// apply the rotation transform
foregroundLayer.transform = tr
So, slight modifications to your code:
extension ViewController {
func sampleProgress() {
let totalSteps = 6
// use 0,0 for the origin of the PATH frame
let frame = CGRect(x: 0, y: 0, width: 120, height: 120)
let circlePath = UIBezierPath(ovalIn: frame)
// use this for the POSITION of the path
let layerFrame = frame.offsetBy(dx: 50, dy: 50)
let gapSize: CGFloat = 0.0125
let segmentAngle: CGFloat = 0.167 // (1/totalSteps)
var startAngle = 0.0
let lineWidth = 8.0
// we want to rotate the layer by -90 degrees
let tr = CATransform3DMakeRotation(-(.pi * 0.5), 0, 0, 1)
for index in 0 ... totalSteps {
// Background layer
let backgroundLayer = CAShapeLayer()
backgroundLayer.strokeStart = startAngle
backgroundLayer.strokeEnd = backgroundLayer.strokeStart + segmentAngle - gapSize
backgroundLayer.path = circlePath.cgPath
backgroundLayer.name = String(index)
backgroundLayer.strokeColor = UIColor.lightGray.cgColor
backgroundLayer.lineWidth = lineWidth
backgroundLayer.lineCap = CAShapeLayerLineCap.butt
backgroundLayer.fillColor = UIColor.clear.cgColor
self.view.layer.addSublayer(backgroundLayer)
// set the layer's frame
backgroundLayer.frame = layerFrame
// set the layer's anchor point
backgroundLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
// apply the rotation transform
backgroundLayer.transform = tr
// Foreground layer
let foregroundLayer = CAShapeLayer()
foregroundLayer.strokeStart = startAngle
foregroundLayer.strokeEnd = backgroundLayer.strokeStart + segmentAngle - gapSize
foregroundLayer.isHidden = true
foregroundLayer.name = String(index) + String(index)
foregroundLayer.path = circlePath.cgPath
foregroundLayer.strokeColor = UIColor.green.cgColor
foregroundLayer.lineWidth = lineWidth
foregroundLayer.lineCap = CAShapeLayerLineCap.butt
foregroundLayer.fillColor = UIColor.clear.cgColor
self.view.layer.addSublayer(foregroundLayer)
// set the layer's frame
foregroundLayer.frame = layerFrame
// set the layer's anchor point
foregroundLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
// apply the rotation transform
foregroundLayer.transform = tr
print("Start angle: \(startAngle)")
startAngle = startAngle + segmentAngle
}
}
func animateLayer(isAnimate: Bool, stepsToAnimate: Int) {
let segmentAngle: CGFloat = (360 * 0.166) / 360
let gapSize: CGFloat = 0.0125
var startAngle = 0.0
for index in 0 ... stepsToAnimate {
if let foregroundLayers = self.view.layer.sublayers {
for animateLayer in foregroundLayers {
if animateLayer.name == String(index) + String(index) {
if index == stepsToAnimate && isAnimate {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = startAngle
animation.toValue = startAngle + segmentAngle - gapSize
animation.duration = 1.0
animateLayer.add(animation, forKey: "foregroundAnimation")
animateLayer.isHidden = false
} else {
animateLayer.isHidden = false
}
startAngle = startAngle + segmentAngle
}
}
}
}
}
}
and an example controller - each tap anywhere animates the next step:
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
sampleProgress()
}
var p: Int = 0
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
animateLayer(isAnimate: true, stepsToAnimate: p)
p += 1
}
}

swift How to add view into layer correctly

I have a Gauge view. Here minimized code:
class GaugeView: UIView {
// MARK: PROPERTIES
private let labelFactory = LabelFactory()
/// Minimum value.
var minValue: Double = 0
/// Maximum value.
var maxValue: Double = 100
/// The thickness of the ring.
private var ringThickness: CGFloat = 30
private var startAngle: CGFloat = .pi * 3/4 + .pi/20
private var endAngle: CGFloat = .pi/4 + .pi * 2
private let points = 24
private lazy var ellipseLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.contentsScale = UIScreen.main.scale
layer.fillColor = UIColor.clear.cgColor
layer.lineCap = CAShapeLayerLineCap.butt
layer.lineJoin = CAShapeLayerLineJoin.bevel
layer.strokeEnd = 0
return layer
}()
// MARK: DRAWING
override open func draw(_ rect: CGRect) {
addLayer(ellipseLayer)
drawBorderedLayer(&ellipseLayer)
let subView = buildCircleView(text: "1", size: 50)
addSubview(subView)
subView.snp.makeConstraints {
$0.size.equalTo(50)
}
}
private func drawBorderedLayer(_ subLayer: inout CAShapeLayer) {
let thickness: CGFloat = 30
let center = CGPoint(x: bounds.width/2, y: bounds.height / 2)
let radius: CGFloat = min(bounds.width, bounds.height) / 2 + 40
subLayer.strokeEnd = CGFloat((30 - minValue)/(maxValue - minValue))
subLayer.strokeColor = UIColor.white.cgColor
subLayer.frame = CGRect(x: center.x - radius - thickness / 2,
y: center.y - radius - thickness / 2,
width: (radius + thickness / 2 ) * 2,
height: (radius + thickness / 2) * 2)
subLayer.bounds = subLayer.frame
let smoothedPath = UIBezierPath(arcCenter: subLayer.position,
radius: radius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
subLayer.path = smoothedPath.cgPath
subLayer.lineWidth = CGFloat(thickness)
}
private func addLayer(_ subLayer: CAShapeLayer) {
if subLayer.superlayer == nil {
layer.addSublayer(subLayer)
}
}
private func addView(_ subView: UIView) {
if subView.superview == nil {
addSubview(subView)
}
}
private func buildCircleView(
text: String,
size: CGFloat) -> UIView {
let containerView = UIView()
let circleView = UIView(frame: .init(x: 0, y: 0, width: size, height: size))
circleView.layer.cornerRadius = size / 2
circleView.layer.borderWidth = 2
circleView.layer.borderColor = UIColor.white.cgColor
circleView.backgroundColor = Color.grayBackground
let label = labelFactory.make(withStyle: .headingH2,
text: text,
textColor: .white,
textAlignment: .center)
circleView.addSubview(label)
label.snp.makeConstraints { $0.center.equalToSuperview() }
containerView.addSubview(circleView)
return containerView
}
}
Image:
And I want to add circle view into the ellipse layer:
...
let subView = buildCircleView(text: "1", size: 50)
ellipseLayer.addSublayer(subView.layer)
subView.snp.makeConstraints {
$0.size.equalTo(50)
}
...
On screen displays just result:
How to add circle view correct way into ellipse. Like here:
Here my view controller:
class ViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
private let gaugeView = GaugeView()
gaugeView.layer.masksToBounds = false
gaugeView.setNeedsDisplay()
[gaugeView].forEach(view.addSubview(_:))
gaugeView.snp.makeConstraints {
$0.size.equalTo(300)
$0.center.equalToSuperview()
}
}
}

iOS Radar Chart with 3D Effect

I would like to replicate this graph in my app.
I tried to search online but I found only pods, specifically Charts.
I tried to customize it but I was unable to give it the 3d effect by assigning each "triangle" a different color shade.
How can I replicate it?
Uibezierpath or something else?
Just have fun.
class GraphView: UIView {
let cirleSegnaposto:CGFloat = 20.0
let labelSize:Double = 50
let spacingGraphLabel:Double = 0
let widthOfZero:Double = 30
let labels = ["Label 1", "Label 2", "Label 3", "Label 4", "Label 5"]
let firstColors:[UIColor] = [.darkGray, .black, .darkGray, .lightGray, .white]
let secondColors:[UIColor] = [.orange, .brown, .orange, .yellow, .red]
var values: [Int]? = nil
var secondValues: [Int]? = nil
override func draw(_ rect: CGRect) {
for i in 0 ..< 4 {
let cirleLayer = CAShapeLayer()
let delta = Double(15 * i) + labelSize
let path = UIBezierPath(ovalIn: CGRect(x: delta,
y: delta,
width: Double(rect.width) - delta * 2,
height: Double(rect.width) - delta * 2))
cirleLayer.path = path.cgPath
cirleLayer.lineWidth = 1
cirleLayer.strokeColor = UIColor.lightGray.cgColor
cirleLayer.fillColor = UIColor.clear.cgColor
self.layer.addSublayer(cirleLayer)
}
let radius:Double = Double(rect.width/2) - (labelSize - spacingGraphLabel)
let labelRadius:Double = Double(rect.width/2) + (spacingGraphLabel)
let origin = CGPoint(x: rect.width/2, y: rect.height/2)
for i in 0..<5 {
let cirleLayer = CAShapeLayer()
let angle:Double = Double(i)/5.0 * (2 * .pi)
let centerX = Double(origin.x) + radius * cos(angle)
let centerY = Double(origin.y) - radius * sin(angle)
let path = UIBezierPath(ovalIn: CGRect(x: CGFloat(centerX) - cirleSegnaposto/2,
y: CGFloat(centerY) - cirleSegnaposto/2,
width: cirleSegnaposto,
height: cirleSegnaposto))
cirleLayer.path = path.cgPath
cirleLayer.fillColor = UIColor.lightGray.cgColor
cirleLayer.lineWidth = 0.5
cirleLayer.strokeColor = UIColor.black.cgColor
self.layer.addSublayer(cirleLayer)
let label = UILabel(frame: .zero)
label.font = UIFont.systemFont(ofSize: 12)
label.text = labels[i]
label.frame.size = CGSize(width: labelSize, height: labelSize/2)
let labelCenterX = Double(origin.x) + labelRadius * cos(angle)
let labelCenterY = Double(origin.y) - labelRadius * sin(angle)
label.center = CGPoint(x: labelCenterX, y: labelCenterY)
label.transform = label.transform.rotated(by: .pi/2)
self.addSubview(label)
}
if let values = secondValues {
drawGraph(values: values, center: origin, maxValue: radius, colors: secondColors.map({$0.cgColor}))
}
if let values = values {
drawGraph(values: values, center: origin, maxValue: radius, colors: firstColors.map({$0.cgColor}))
}
}
func drawGraph(values: [Int], center: CGPoint, maxValue: Double, colors: [CGColor]) {
var points = [CGPoint]()
for i in 0 ..< values.count {
let radius = Double(values[i])/10.0 * (maxValue - widthOfZero) + widthOfZero
let angle:Double = Double(i)/5.0 * (2 * .pi)
let x = Double(center.x) + radius * cos(angle)
let y = Double(center.y) - radius * sin(angle)
let point = CGPoint(x: x, y: y)
points.append(point)
}
for (i, point) in points.enumerated() {
let secondPoint = point == points.last ? points[0] : points[i+1]
let path = UIBezierPath()
path.move(to: center)
path.addLine(to: point)
path.addLine(to: secondPoint)
path.close()
let layer = CAShapeLayer()
layer.path = path.cgPath
layer.fillColor = colors[i]
layer.lineWidth = 1
layer.lineJoin = .round
layer.strokeColor = UIColor.black.cgColor
self.layer.addSublayer(layer)
}
}
}

Adding labels to a circle using CATextLayers

I have created a circle using a CAShapeLayer. Now I want to add text to the control but I am not quite sure on how to do so (so it looks good).
I have the following code:
import Foundation
import UIKit
class Gauge : UIView
{
private var shapeLayer = CAShapeLayer()
private var maskingLayer = CAShapeLayer()
private var gradientLayer = CAGradientLayer()
private var textLayers: [CATextLayer] = []
private var mValue: CGFloat = 0.0
private var mSegments = 9
private let textHeight: CGFloat = 24.0
// MARK: Properties
var lineWidth: CGFloat = 32.0
var min: CGFloat = 0.0
var max: CGFloat = 100.0
var segments: Int
{
get { return self.mSegments - 1 }
set
{
self.mSegments = newValue + 1
self.commonInit()
}
}
var progress: CGFloat
{
get
{
let diff = abs(self.min) + self.max
return self.value / diff
}
}
var segmentSize: CGFloat = 270.0
{
didSet
{
self.value = 0.0
self.commonInit()
}
}
var value: CGFloat
{
get { return self.mValue }
set
{
if self.mValue == newValue { return }
if newValue < 0.0
{
self.mValue = 0.0
}
else if newValue > self.max
{
self.mValue = self.max
}
else
{
self.mValue = newValue
}
self.maskingLayer.strokeStart = 0.0
self.maskingLayer.strokeEnd = 0.5
}
}
override init(frame: CGRect)
{
super.init(frame: frame)
self.commonInit()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
self.commonInit()
}
fileprivate func commonInit()
{
self.value = 50
self.determineLineWidth()
self.initLayers()
self.initDataLayers()
self.initTextLayers()
}
override func layoutSubviews()
{
super.layoutSubviews()
self.commonInit()
}
fileprivate func initTextLayers()
{
for textLayer in self.textLayers
{
textLayer.removeFromSuperlayer()
}
let fontSize: CGFloat = self.getFontSize()
for i in 0 ... self.segments
{
let orientation = CGFloat(i) * (1.0 / CGFloat(self.segments))
let span = self.max + abs(self.min)
let step = span / CGFloat(self.segments)
let value = CGFloat(i) * step
let font = UIFont.systemFont(ofSize: fontSize, weight: .bold)
let width = Utilities.measure(Int(value).description, .zero, font)
let point = self.getLabelPosition(orientation, width)
let layer = CATextLayer()
layer.contentsScale = UIScreen.main.scale
layer.font = font
layer.foregroundColor = UIColor.black.cgColor
layer.fontSize = fontSize
layer.string = Int(value).description
layer.alignmentMode = .center
layer.frame = CGRect(origin: point, size: .init(width: 48.0, height: self.textHeight))
self.textLayers.append(layer)
self.layer.addSublayer(layer)
}
}
fileprivate func gaugeFont() -> UIFont
{
let valueFontSize = self.getFontSize()
return UIFont.boldSystemFont(ofSize: valueFontSize)
}
fileprivate func getFontSize() -> CGFloat
{
if self.bounds.height < 128.0
{
return 10.0
}
else if self.bounds.height < 256.0
{
return 14.0
}
else
{
return 18.0
}
}
fileprivate func initDataLayers()
{
self.maskingLayer.removeFromSuperlayer()
let fillPath = self.createPath()
self.maskingLayer.frame = self.bounds
self.maskingLayer.path = fillPath.cgPath
self.maskingLayer.lineCap = .round
self.maskingLayer.fillColor = UIColor.clear.cgColor
self.maskingLayer.strokeColor = UIColor.black.cgColor
self.maskingLayer.lineWidth = self.lineWidth / 2.0
self.maskingLayer.position = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
self.layer.addSublayer(self.maskingLayer)
}
fileprivate func calculateAngle(_ value: CGFloat) -> CGFloat
{
let diff = abs(self.min) + self.max
return value / diff
}
fileprivate func getLabelPosition(_ progress: CGFloat, _ width: CGFloat) -> CGPoint
{
let size = Swift.min(self.bounds.width - self.lineWidth, self.bounds.height - self.lineWidth)
let center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
let alpha = (180.0 - self.segmentSize) / 2.0
let radius = size / 2.0 - self.lineWidth - width
let cx = center.x
let cy = center.y
let angle = self.segmentSize * progress
let x2 = self.deg2rad(180.0 + alpha + angle)
let outerX = cx + (radius + self.lineWidth / 2.0) * CGFloat(cos(x2))
let outerY = cy + (radius + self.lineWidth / 2.0) * CGFloat(sin(x2))
return CGPoint(x: outerX, y: outerY)
}
fileprivate func initLayers()
{
self.shapeLayer.removeFromSuperlayer()
let path = self.createPath()
self.shapeLayer = CAShapeLayer()
self.shapeLayer.frame = self.bounds
self.shapeLayer.path = path.cgPath
self.shapeLayer.strokeColor = UIColor.lightGray.cgColor
self.shapeLayer.fillColor = nil
self.shapeLayer.lineWidth = self.lineWidth / 2.0
self.shapeLayer.lineCap = .round
self.layer.addSublayer(self.shapeLayer)
}
fileprivate func createPath() -> UIBezierPath
{
let size = Swift.min(self.frame.width - self.lineWidth / 2, self.frame.height - self.lineWidth)
let center = CGPoint(x: self.frame.width / 2.0, y: self.frame.height / 2.0)
let alpha = (180.0 - self.segmentSize) / 2.0
let path = UIBezierPath(arcCenter: center, radius: size / 2.0, startAngle: self.deg2rad(180.0 + alpha), endAngle: self.deg2rad(360.0 - alpha), clockwise: true)
return path
}
fileprivate func determineLineWidth()
{
if self.bounds.height < 192.0
{
self.lineWidth = 20.0
}
else if self.bounds.height < 320
{
self.lineWidth = 32.0
}
else
{
self.lineWidth = 40.0
}
}
fileprivate func deg2rad(_ number: CGFloat) -> CGFloat
{
return number * .pi / 180
}
}
The result looks like this:
But I want the text to be positioned perfectly like this:
I tried adding various offsets manually, but when the control gets resized, it started to look bad again. Is there some kind of formula which I can use to calculate the exact position?
It looks like getLabelPosition returns a point that should be used as the centre of the text but you're passing it to the frame so it's used as the upper left point.
You need to offset the point by hals the size of the label to get the origin.
let size = CGSize(width: 48.0, height: self.textHeight)
var origin = point
origin.x -= size.width / 2
origin.y -= size.height / 2
layer.frame = CGRect(origin: origin, size: size)

UIView horizontal bar animation in swift

I am working on this animation where a number will be received every second and progress bar has to fill or go down based on the double value.
I have created the views and have added all the views in the UIStackView. Also made the outlet collection for all the views. (sorting them by the tag and making them round rect).
I can loop the views and change their background color but trying to see if there is a better way to do it. Any suggestions?
Thanks
So how you are doing it is fine. Here would be two different ways. The first with Core Graphics. You may want to update methods and even make the color gradient in the sublayer.
1st Way
import UIKit
class Indicator: UIView {
var padding : CGFloat = 5.0
var minimumSpace : CGFloat = 4.0
var indicators : CGFloat = 40
var indicatorColor : UIColor = UIColor.lightGray
var filledIndicatorColor = UIColor.blue
var currentProgress = 0.0
var radiusFactor : CGFloat = 0.25
var fillReversed = false
override init(frame: CGRect) {
super.init(frame: frame)
setUp(animated: false)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setUp(animated: false)
backgroundColor = UIColor.green
}
func updateProgress(progress:Double, animated:Bool) {
currentProgress = progress
setUp(animated: animated)
}
private func setUp(animated:Bool){
// internal space
let neededPadding = (indicators - 1) * minimumSpace
//calculate height and width minus padding and space since vertical
let height = (bounds.height - neededPadding - (padding * 2.0)) / indicators
let width = bounds.width - padding * 2.0
if animated == true{
let trans = CATransition()
trans.type = kCATransitionFade
trans.duration = 0.5
self.layer.add(trans, forKey: nil)
}
layer.sublayers?.removeAll()
for i in 0...Int(indicators - 1.0){
let indicatorLayer = CALayer()
indicatorLayer.frame = CGRect(x: padding, y: CGFloat(i) * height + padding + (minimumSpace * CGFloat(i)), width: width, height: height)
//haha i don't understand my logic below but it works hahaha
// i know it has to go backwards
if fillReversed{
if CGFloat(1 - currentProgress) * self.bounds.height < indicatorLayer.frame.origin.y{
indicatorLayer.backgroundColor = filledIndicatorColor.cgColor
}else{
indicatorLayer.backgroundColor = indicatorColor.cgColor
}
}else{
if CGFloat(currentProgress) * self.bounds.height > indicatorLayer.frame.origin.y{
indicatorLayer.backgroundColor = indicatorColor.cgColor
}else{
indicatorLayer.backgroundColor = filledIndicatorColor.cgColor
}
}
indicatorLayer.cornerRadius = indicatorLayer.frame.height * radiusFactor
indicatorLayer.masksToBounds = true
self.layer.addSublayer(indicatorLayer)
}
}
//handle rotation
override func layoutSubviews() {
super.layoutSubviews()
setUp(animated: false)
}
}
The second way is using CAShapeLayer and the benefit is that the progress will get a natural animation.
import UIKit
class Indicator: UIView {
var padding : CGFloat = 5.0
var minimumSpace : CGFloat = 4.0
var indicators : CGFloat = 40
var indicatorColor : UIColor = UIColor.lightGray
var filledIndicatorColor = UIColor.blue
var currentProgress = 0.0
var radiusFactor : CGFloat = 0.25
private var progressLayer : CALayer?
private var shapeHoles : CAShapeLayer?
override init(frame: CGRect) {
super.init(frame: frame)
transparentDotsAndProgress()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
transparentDotsAndProgress()
}
func updateProgress(progress:Double) {
if progress <= 1 && progress >= 0{
currentProgress = progress
transparentDotsAndProgress()
}
}
//handle rotation
override func layoutSubviews() {
super.layoutSubviews()
transparentDotsAndProgress()
}
func transparentDotsAndProgress(){
self.layer.masksToBounds = true
let neededPadding = (indicators - 1) * minimumSpace
//calculate height and width minus padding and space since vertical
let height = (bounds.height - neededPadding - (padding * 2.0)) / indicators
let width = bounds.width - padding * 2.0
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: self.bounds.width, height: self.bounds.height), cornerRadius: 0)
path.usesEvenOddFillRule = true
for i in 0...Int(indicators - 1.0){
let circlePath = UIBezierPath(roundedRect: CGRect(x: padding, y: CGFloat(i) * height + padding + (minimumSpace * CGFloat(i)), width: width, height: height), cornerRadius: height * radiusFactor)
path.append(circlePath)
}
if progressLayer == nil{
progressLayer = CALayer()
progressLayer?.backgroundColor = filledIndicatorColor.cgColor
self.layer.addSublayer(progressLayer!)
}
progressLayer?.frame = CGRect(x: 0, y: -self.bounds.height - padding + CGFloat(currentProgress) * self.bounds.height, width: bounds.width, height: bounds.height)
self.shapeHoles?.removeFromSuperlayer()
shapeHoles = CAShapeLayer()
shapeHoles?.path = path.cgPath
shapeHoles?.fillRule = kCAFillRuleEvenOdd
shapeHoles?.fillColor = UIColor.white.cgColor
shapeHoles?.strokeColor = UIColor.clear.cgColor
self.layer.backgroundColor = indicatorColor.cgColor
self.layer.addSublayer(shapeHoles!)
}
}
Both of these ways should work but the advantage of the CAShapeLayer is you get a natural animation.
I'm a firm believer in learning through solving organic problems and slowly building my global knowledge on a subject. So I'm afraid I don't have any good tutorials for you.
Here is an example that will jump start you, though.
// For participating in Simulator's "slow animations"
#_silgen_name("UIAnimationDragCoefficient") func UIAnimationDragCoefficient() -> Float
import UIKit
#IBDesignable
class VerticalProgessView: UIControl {
#IBInspectable
var numberOfSegments: UInt = 0
#IBInspectable
var verticalSegmentGap: CGFloat = 4.0
#IBInspectable
var outerColor: UIColor = UIColor(red: 33, green: 133, blue: 109)
#IBInspectable
var unfilledColor: UIColor = UIColor(red: 61, green: 202, blue: 169)
#IBInspectable
var filledColor: UIColor = UIColor.white
private var _progress: Float = 0.25
#IBInspectable
open var progress: Float {
get {
return _progress
}
set {
self.setProgress(newValue, animated: false)
}
}
let progressLayer = CALayer()
let maskLayer = CAShapeLayer()
var skipLayoutSubviews = false
open func setProgress(_ progress: Float, animated: Bool) {
if progress < 0 {
_progress = 0
} else if progress > 1.0 {
_progress = 1
} else {
// Clamp the percentage to discreet values
let discreetPercentageDistance: Float = 1.0 / 28.0
let nearestProgress = discreetPercentageDistance * round(progress/discreetPercentageDistance)
_progress = nearestProgress
}
CATransaction.begin()
CATransaction.setCompletionBlock { [weak self] in
self?.skipLayoutSubviews = false
}
if !animated {
CATransaction.setDisableActions(true)
} else {
CATransaction.setAnimationDuration(0.25 * Double(UIAnimationDragCoefficient()))
}
let properties = progressLayerProperties()
progressLayer.bounds = properties.bounds
progressLayer.position = properties.position
skipLayoutSubviews = true
CATransaction.commit() // This triggers layoutSubviews
}
override func prepareForInterfaceBuilder() {
awakeFromNib()
}
override func awakeFromNib() {
super.awakeFromNib()
self.backgroundColor = UIColor.clear
self.layer.backgroundColor = unfilledColor.cgColor
// Initialize and add the progressLayer
let properties = progressLayerProperties()
progressLayer.bounds = properties.bounds
progressLayer.position = properties.position
progressLayer.backgroundColor = filledColor.cgColor
self.layer.addSublayer(progressLayer)
// Initialize and add the maskLayer (it has the holes)
maskLayer.frame = self.layer.bounds
maskLayer.fillColor = outerColor.cgColor
maskLayer.fillRule = kCAFillRuleEvenOdd
maskLayer.path = maskPath(for: maskLayer.bounds)
self.layer.addSublayer(maskLayer)
// Layer hierarchy
// self.maskLayer
// self.progressLayer
// self.layer
}
override func layoutSubviews() {
super.layoutSubviews()
if skipLayoutSubviews {
// Crude but effective, not fool proof though
skipLayoutSubviews = false
return
}
let timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
// Doesn't work for 180° rotations
let duration = UIApplication.shared.statusBarOrientationAnimationDuration * Double(UIAnimationDragCoefficient())
CATransaction.begin()
CATransaction.setAnimationTimingFunction(timingFunction)
CATransaction.setAnimationDuration(duration)
let properties = progressLayerProperties()
progressLayer.bounds = properties.bounds
progressLayer.position = properties.position
let size = self.bounds.size
let anchorPoint = CGPoint(x: 0.5, y: 1.0)
maskLayer.anchorPoint = anchorPoint
maskLayer.bounds = CGRect(origin: CGPoint.zero, size: size)
maskLayer.position = CGPoint(x: size.width * anchorPoint.x, y: size.height * anchorPoint.y)
// Animate the segments
let pathChangeAnimation = CAKeyframeAnimation(keyPath: "path")
let finalPath = maskPath(for: maskLayer.bounds)
pathChangeAnimation.values = [maskLayer.path!, finalPath]
pathChangeAnimation.keyTimes = [0, 1]
pathChangeAnimation.timingFunction = timingFunction
pathChangeAnimation.duration = duration
pathChangeAnimation.isRemovedOnCompletion = true
maskLayer.add(pathChangeAnimation, forKey: "pathAnimation")
CATransaction.setCompletionBlock {
// CAAnimation's don't actually change the value
self.maskLayer.path = finalPath
}
CATransaction.commit()
}
// Provides a path that will mask out all the holes to show self.layer and the progressLayer behind
private func maskPath(for rect: CGRect) -> CGPath {
let horizontalSegmentGap: CGFloat = 5.0
let segmentWidth = rect.width - horizontalSegmentGap * 2
let segmentHeight = rect.height/CGFloat(numberOfSegments) - verticalSegmentGap + verticalSegmentGap/CGFloat(numberOfSegments)
let segmentSize = CGSize(width: segmentWidth, height: segmentHeight)
let segmentRect = CGRect(x: horizontalSegmentGap, y: 0, width: segmentSize.width, height: segmentSize.height)
let path = CGMutablePath()
for i in 0..<numberOfSegments {
// Literally, just move it down by the y value here
// this simplifies the math of calculating the starting points and what not
let transform = CGAffineTransform.identity.translatedBy(x: 0, y: (segmentSize.height + verticalSegmentGap) * CGFloat(i))
let segmentPath = UIBezierPath(roundedRect: segmentRect, cornerRadius: segmentSize.height / 2)
segmentPath.apply(transform)
path.addPath(segmentPath.cgPath)
}
// Without the outerPath, we'll end up with a bunch of squircles instead of a bunch of holes
let outerPath = CGPath(rect: rect, transform: nil)
path.addPath(outerPath)
return path
}
/// Provides the current and correct bounds and position for the progressLayer
private func progressLayerProperties() -> (bounds: CGRect, position: CGPoint) {
let frame = self.bounds
let height = frame.height * CGFloat(progress)
let y = frame.height * CGFloat(1 - progress)
let width = frame.width
let anchorPoint = maskLayer.anchorPoint
let bounds = CGRect(x: 0, y: 0, width: width, height: height)
let position = CGPoint(x: 0 + width * anchorPoint.x, y: y + height * anchorPoint.x)
return (bounds: bounds, position: position)
}
// TODO: Implement functions to further mimic UIProgressView
}
extension UIColor {
convenience init(red: Int, green: Int, blue: Int) {
self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1)
}
}
Using in a storyboard
Enjoy the magic

Resources