Adding different objects to CAEmitterLayer with forEach with SwiftUI - ios

When I add more than one object, I have to write one by one as below. When I want to add 2 or more objects, I always have to copy and paste. I don't want that and I tried doing it with forLoop, but I only see whatever object was added last.
ForLoop
I wrote a function. ForLoop works as much as the number of images in the array.
func prepareParticeCell(particles: [String]) -> CAEmitterCell {
let particleCell = CAEmitterCell()
for particleItem in particles {
let particle = UIImage(named: particleItem)?.cgImage
particleCell.contents = particle
particleCell.name = "Square"
particleCell.birthRate = 5
particleCell.lifetime = 74.5
particleCell.velocityRange = 0.0
particleCell.velocity = 79.0
particleCell.xAcceleration = 0.0
particleCell.yAcceleration = 0.0
particleCell.emissionLatitude = 1*6.0 * (.pi / 180)
particleCell.emissionLongitude = -105.0 * (.pi / 180)
particleCell.emissionRange = 360.0 * (.pi / 180.0)
particleCell.spin = -65.6 * (.pi / 180.0)
particleCell.spinRange = 314.2 * (.pi / 180.0)
particleCell.scale = 0.043
particleCell.scaleRange = 0.7
particleCell.scaleSpeed = 0.02
particleCell.alphaRange = 0.0
particleCell.alphaSpeed = 0.47
particleCell.color = UIColor(red: 255.0/255.0, green: 255.0/255.0, blue: 255.0/255.0, alpha: 1.0).cgColor
}
return particleCell
}
Using Function
let host = UIView(frame: CGRect(x: 0.0, y: 0.0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
let particlesLayer = CAEmitterLayer()
particlesLayer.frame = CGRect(x: 0.0, y: 0.0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
host.layer.insertSublayer(particlesLayer, at: 0)
host.layer.masksToBounds = true
host.insetsLayoutMarginsFromSafeArea = false
particlesLayer.backgroundColor = .none
particlesLayer.emitterShape = .circle
particlesLayer.emitterPosition = CGPoint(x: 509.4, y: 707.7)
particlesLayer.emitterSize = CGSize(width: 1648.0, height: 1112.0)
particlesLayer.emitterMode = .outline
particlesLayer.renderMode = .backToFront
particlesLayer.emitterCells = [prepareParticeCell(particles: particleImages)] // here
return host

You have to return an array, I gave you 2 way for this issue, one with for another with map. Both working the same thing.
func prepareParticeCellV1(particles: [String]) -> [CAEmitterCell] {
var arrayParticleCell: [CAEmitterCell] = [CAEmitterCell]() // <<: Here
for particleItem in particles {
let particleCell: CAEmitterCell = CAEmitterCell()
particleCell.contents = UIImage(named: particleItem)?.cgImage
particleCell.name = "Square"
particleCell.birthRate = 5
particleCell.lifetime = 74.5
particleCell.velocityRange = 0.0
particleCell.velocity = 79.0
particleCell.xAcceleration = 0.0
particleCell.yAcceleration = 0.0
particleCell.emissionLatitude = 1*6.0 * (.pi / 180)
particleCell.emissionLongitude = -105.0 * (.pi / 180)
particleCell.emissionRange = 360.0 * (.pi / 180.0)
particleCell.spin = -65.6 * (.pi / 180.0)
particleCell.spinRange = 314.2 * (.pi / 180.0)
particleCell.scale = 0.043
particleCell.scaleRange = 0.7
particleCell.scaleSpeed = 0.02
particleCell.alphaRange = 0.0
particleCell.alphaSpeed = 0.47
particleCell.color = UIColor(red: 255.0/255.0, green: 255.0/255.0, blue: 255.0/255.0, alpha: 1.0).cgColor
arrayParticleCell.append(particleCell)
}
return arrayParticleCell
}
func prepareParticeCellV2(particles: [String]) -> [CAEmitterCell] {
return particles.map({ item in
let particleCell: CAEmitterCell = CAEmitterCell()
particleCell.contents = UIImage(named: item)?.cgImage
particleCell.name = "Square"
particleCell.birthRate = 5
particleCell.lifetime = 74.5
particleCell.velocityRange = 0.0
particleCell.velocity = 79.0
particleCell.xAcceleration = 0.0
particleCell.yAcceleration = 0.0
particleCell.emissionLatitude = 1*6.0 * (.pi / 180)
particleCell.emissionLongitude = -105.0 * (.pi / 180)
particleCell.emissionRange = 360.0 * (.pi / 180.0)
particleCell.spin = -65.6 * (.pi / 180.0)
particleCell.spinRange = 314.2 * (.pi / 180.0)
particleCell.scale = 0.043
particleCell.scaleRange = 0.7
particleCell.scaleSpeed = 0.02
particleCell.alphaRange = 0.0
particleCell.alphaSpeed = 0.47
particleCell.color = UIColor(red: 255.0/255.0, green: 255.0/255.0, blue: 255.0/255.0, alpha: 1.0).cgColor
return particleCell
})
}

Related

How to fill stroke colours on based of percentages in circle in swift

I am creating a circle and divided into four parts. stroke colours should be filled by based on given percentage.
func showCircle() {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+0.2) { [weak self] in
guard let `self` = self else { return }
self.layer.cornerRadius = self.frame.size.width / 2
self.backgroundColor = .clear
let total: Int = 4
let width: CGFloat = 10.0
let reducer: CGFloat = 0.0 // add gape b/w segments if reducer > 0
let circlePath = UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width / 2, y: self.frame.size.height / 2),
radius: self.frame.size.width / 2,
startAngle: CGFloat(-0.5 * Double.pi),
endAngle: CGFloat(1.5 * Double.pi),
clockwise: true)
// these are the percentages
let percentages = [0.20, 0.15, 0.35, 0.30]
for i in 1...total {
let percentage = CGFloat(percentages[i - 1] )
let circleShape = CAShapeLayer()
circleShape.path = circlePath.cgPath
switch i-1 {
case 0:
circleShape.strokeColor = #colorLiteral(red: 0.431372549, green: 0.231372549, blue: 0.631372549, alpha: 1)
case 1:
circleShape.strokeColor = #colorLiteral(red: 0.6784313725, green: 0.568627451, blue: 0.7882352941, alpha: 1)
case 2:
circleShape.strokeColor = #colorLiteral(red: 0.8470588235, green: 0.7960784314, blue: 0.8980392157, alpha: 1)
case 3:
circleShape.strokeColor = #colorLiteral(red: 0.9490196078, green: 0.937254902, blue: 0.9568627451, alpha: 1)
default:
circleShape.strokeColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)
}
circleShape.fillColor = UIColor.clear.cgColor
circleShape.lineWidth = width
circleShape.strokeStart = CGFloat(CGFloat(i - 1) * percentage) + reducer
circleShape.strokeEnd = CGFloat(CGFloat(i) * percentage) - reducer
self.layer.addSublayer(circleShape)
}
}
}
Wrong result:- these colours should be filled based on percentage.
Expected result:- we can see in this picture colours are filled properly based on percentages.
Your mistake is that the strokeStart and strokeEnd should be from 0 to 1 and increase as the index increases.
This is gonna work:
// these are the percentages
let percentages = [0.20, 0.15, 0.35, 0.30]
for i in 1...total {
let prevPercentage: Double = percentages.prefix(i - 1).reduce(0, +)
let percentage = percentages[i - 1]
let circleShape = CAShapeLayer()
circleShape.path = circlePath.cgPath
switch i-1 {
case 0:
circleShape.strokeColor = UIColor.red.cgColor
case 1:
circleShape.strokeColor = UIColor.green.cgColor
case 2:
circleShape.strokeColor = UIColor.blue.cgColor
case 3:
circleShape.strokeColor = UIColor.orange.cgColor
default:
circleShape.strokeColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)
}
circleShape.fillColor = UIColor.clear.cgColor
circleShape.lineWidth = width
circleShape.strokeStart = CGFloat(prevPercentage)
circleShape.strokeEnd = CGFloat(prevPercentage + percentage)
view.layer.addSublayer(circleShape)
}
Result:

CATextLayer position and Transformation issue

I am trying to build a control like attached circle image with multiple segment having equal space for each part. Number of segments can change depend upon provided array.
I have developed this so far using CAShapeLayer and UIBezierPath. Also added text in the center of each shape Layer.
I have added my code so far for generating this control.
let circleLayer = CALayer()
func createCircle(_ titleArray:[String]) {
update(bounds: bounds, titleArray: titleArray)
containerView.layer.addSublayer(circleRenderer.circleLayer)
}
func update(bounds: CGRect, titleArray: [String]) {
let position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
circleLayer.position = position
circleLayer.bounds = bounds
update(titleArray: titles)
}
func update(titleArray: [String]) {
let center = CGPoint(x: circleLayer.bounds.size.width / 2.0, y: circleLayer.bounds.size.height / 2.0)
let radius:CGFloat = min(circleLayer.bounds.size.width, circleLayer.bounds.size.height) / 2
let segmentSize = CGFloat((Double.pi*2) / Double(titleArray.count))
for i in 0..<titleArray.count {
let startAngle = segmentSize*CGFloat(i) - segmentSize/2
let endAngle = segmentSize*CGFloat(i+1) - segmentSize/2
let midAngle = (startAngle+endAngle)/2
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.random.cgColor
let bezierPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
bezierPath.addLine(to: center)
shapeLayer.path = bezierPath.cgPath
let height = titleArray[i].height(withConstrainedWidth: radius-20, font: UIFont.systemFont(ofSize: 15))
let frame = shapeLayer.path?.boundingBoxOfPath ?? CGRect.zero
let textLayer = TextLayer()
textLayer.frame = CGRect(x: 0, y: 0, width: 70, height: height)
textLayer.position = CGPoint(x: frame.center.x, y: frame.center.y)
textLayer.fontSize = 15
textLayer.contentsScale = UIScreen.main.scale
textLayer.alignmentMode = .center
textLayer.string = titleArray[i]
textLayer.isWrapped = true
textLayer.backgroundColor = UIColor.black.cgColor
textLayer.foregroundColor = UIColor.white.cgColor
textLayer.transform = CATransform3DMakeRotation(midAngle, 0.0, 0.0, 1.0)
shapeLayer.addSublayer(textLayer)
circleLayer.addSublayer(shapeLayer)
}
}
circleLayer is added as superlayer, containing full area of UIView.
My requirement is to add text centered vertically and horizontally within shape with angle. I am facing issue with centring text within shape while angle is fine.
Thanks
Edit:
If I remove textLayer rotation code, then It look like this image.
Your labels are not "centered" because you're using the geometric center of the wedge bounding-box:
What you need to do is calculate an "inner" circle, with 1/2 of the full radius, and then find the points on that circle to place your labels.
So, first we calculate the circle:
Then bisect each angle and find the point on the circle:
Then calculate the bounding-box for the label (I used max-width of radius * 0.6), put the center of that frame on the point on the circle, and then rotate the text layer:
And the result, without the "guides":
Note: For these images, I used radius * 0.55 - or just slightly further out than exactly 1/2 of the radius - for the "inner circle". This gave me just slightly better appearance, due to the wedges narrowing as we get to the center of the circle. Changing that to radius * 0.6 might even look better.
Here is the code to generate this view:
struct Wedge {
var color: UIColor = .cyan
var label: String = ""
}
class WedgeView: UIView {
var wedges: [Wedge] = []
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
}
override func layoutSubviews() {
super.layoutSubviews()
layer.sublayers?.forEach { $0.removeFromSuperlayer() }
setup()
}
func setup() -> Void {
// initialize local variables
var startAngle: CGFloat = 0
var outerRadius: CGFloat = 0.0
var halfRadius: CGFloat = 0.0
// initialize local constants
let viewCenter: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
let diameter = bounds.width
let fontHeight: CGFloat = ceil(12.0 * (bounds.height / 300.0))
let textLayerFont = UIFont.systemFont(ofSize: fontHeight, weight: .light)
outerRadius = diameter * 0.5
halfRadius = outerRadius * 0.55
let labelMaxWidth:CGFloat = outerRadius * 0.6
startAngle = -(.pi * (1.0 / CGFloat(wedges.count)))
for i in 0..<wedges.count {
let endAngle = startAngle + 2 * .pi * (1.0 / CGFloat(wedges.count))
let shape = CAShapeLayer()
let path: UIBezierPath = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: outerRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addLine(to: viewCenter)
path.close()
shape.path = path.cgPath
shape.fillColor = wedges[i].color.cgColor
shape.strokeColor = UIColor.black.cgColor
self.layer.addSublayer(shape)
let textLayer = CATextLayer()
textLayer.font = textLayerFont
textLayer.fontSize = fontHeight
let string = wedges[i].label
textLayer.string = string
textLayer.foregroundColor = UIColor.white.cgColor
textLayer.backgroundColor = UIColor.black.cgColor
textLayer.isWrapped = true
textLayer.alignmentMode = CATextLayerAlignmentMode.center
textLayer.contentsScale = UIScreen.main.scale
let bisectAngle = startAngle + ((endAngle - startAngle) * 0.5)
let p = CGPoint.pointOnCircle(center: viewCenter, radius: halfRadius, angle: bisectAngle)
var textLayerframe = CGRect(x: 0, y: 0, width: labelMaxWidth, height: 0)
let h = string.getLableHeight(labelMaxWidth, usingFont: textLayerFont)
textLayerframe.size.height = h
textLayerframe.origin.x = p.x - (textLayerframe.size.width * 0.5)
textLayerframe.origin.y = p.y - (textLayerframe.size.height * 0.5)
textLayer.frame = textLayerframe
self.layer.addSublayer(textLayer)
textLayer.transform = CATransform3DMakeRotation(bisectAngle, 0.0, 0.0, 1.0)
// uncomment this block to show the dashed-lines
/*
let biLayer = CAShapeLayer()
let dash = UIBezierPath()
dash.move(to: viewCenter)
dash.addLine(to: p)
biLayer.strokeColor = UIColor.yellow.cgColor
biLayer.lineDashPattern = [4, 4]
biLayer.path = dash.cgPath
self.layer.addSublayer(biLayer)
*/
startAngle = endAngle
}
// uncomment this block to show the half-radius circle
/*
let tempLayer: CAShapeLayer = CAShapeLayer()
tempLayer.path = UIBezierPath(ovalIn: bounds.insetBy(dx: outerRadius - halfRadius, dy: outerRadius - halfRadius)).cgPath
tempLayer.fillColor = UIColor.clear.cgColor
tempLayer.strokeColor = UIColor.green.cgColor
tempLayer.lineWidth = 1.0
self.layer.addSublayer(tempLayer)
*/
}
}
class WedgesWithRotatedLabelsViewController: UIViewController {
let wedgeView: WedgeView = WedgeView()
var wedges: [Wedge] = []
let colors: [UIColor] = [
UIColor(red: 1.00, green: 0.00, blue: 0.00, alpha: 1.0),
UIColor(red: 0.00, green: 0.50, blue: 0.00, alpha: 1.0),
UIColor(red: 0.00, green: 0.00, blue: 1.00, alpha: 1.0),
UIColor(red: 1.00, green: 0.50, blue: 0.50, alpha: 1.0),
UIColor(red: 0.00, green: 0.75, blue: 0.00, alpha: 1.0),
UIColor(red: 0.50, green: 0.50, blue: 1.00, alpha: 1.0),
]
let labels: [String] = [
"This is long text for Label 1",
"Label 2",
"Longer Label 3",
"Label 4",
"Label 5",
"Label 6",
]
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
for (c, s) in zip(colors, labels) {
wedges.append(Wedge(color: c, label: s))
}
wedgeView.wedges = wedges
view.addSubview(wedgeView)
wedgeView.translatesAutoresizingMaskIntoConstraints = false
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
wedgeView.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.8),
wedgeView.heightAnchor.constraint(equalTo: wedgeView.widthAnchor),
wedgeView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
wedgeView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
}
}
Couple of "helper" extensions used in the above code:
// get a random color
extension UIColor {
static var random: UIColor {
return UIColor(red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1),
alpha: 1.0)
}
}
// get the point on a circle at specific radian
extension CGPoint {
static func pointOnCircle(center: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint {
let x = center.x + radius * cos(angle)
let y = center.y + radius * sin(angle)
return CGPoint(x: x, y: y)
}
}
// get height of word-wrapping string with max-width
extension String {
func getLableHeight(_ forWidth: CGFloat, usingFont: UIFont) -> CGFloat {
let constraintRect = CGSize(width: forWidth, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: usingFont], context: nil)
return ceil(boundingBox.height)
}
}

How can I make animation with CAEmitterLayer on SwiftUI?

How can I convert this code to SwiftUI. It's a snow effect. I used CAEmitterLayer but I don't know how to use it in SwfitUI. There is no addSublayer in SwiftUI. Is it possible to run this code without using UIHostingController ?
let size = CGSize(width: 824.0, height: 1112.0)
let host = UIView(frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height))
self.view.addSubview(host)
let particlesLayer = CAEmitterLayer()
particlesLayer.frame = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)
host.layer.addSublayer(particlesLayer)
host.layer.masksToBounds = true
particlesLayer.backgroundColor = UIColor(red: 0.0/255.0, green: 0.0/255.0, blue: 0.0/255.0, alpha: 1.0).cgColor
particlesLayer.emitterShape = .circle
particlesLayer.emitterPosition = CGPoint(x: 509.4, y: 707.7)
particlesLayer.emitterSize = CGSize(width: 1648.0, height: 1112.0)
particlesLayer.emitterMode = .surface
particlesLayer.renderMode = .oldestLast
let image1 = UIImage(named: "logo")?.cgImage
let cell1 = CAEmitterCell()
cell1.contents = image1
cell1.name = "Snow"
cell1.birthRate = 92.0
cell1.lifetime = 20.0
cell1.velocity = 59.0
cell1.velocityRange = -15.0
cell1.xAcceleration = 5.0
cell1.yAcceleration = 40.0
cell1.emissionRange = 180.0 * (.pi / 180.0)
cell1.spin = -28.6 * (.pi / 180.0)
cell1.spinRange = 57.2 * (.pi / 180.0)
cell1.scale = 0.06
cell1.scaleRange = 0.3
cell1.color = UIColor(red: 255.0/255.0, green: 255.0/255.0, blue: 255.0/255.0, alpha: 1.0).cgColor
particlesLayer.emitterCells = [cell1]
Here is a solution. Tested with Xcode 11.4 / iOS 13.4
struct EmitterView: UIViewRepresentable {
func makeUIView(context: Context) -> UIView {
let size = CGSize(width: 824.0, height: 1112.0)
let host = UIView(frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height))
let particlesLayer = CAEmitterLayer()
particlesLayer.frame = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)
host.layer.addSublayer(particlesLayer)
host.layer.masksToBounds = true
particlesLayer.backgroundColor = UIColor(red: 0.0/255.0, green: 0.0/255.0, blue: 0.0/255.0, alpha: 1.0).cgColor
particlesLayer.emitterShape = .circle
particlesLayer.emitterPosition = CGPoint(x: 509.4, y: 707.7)
particlesLayer.emitterSize = CGSize(width: 1648.0, height: 1112.0)
particlesLayer.emitterMode = .surface
particlesLayer.renderMode = .oldestLast
let image1 = UIImage(named: "logo")?.cgImage
let cell1 = CAEmitterCell()
cell1.contents = image1
cell1.name = "Snow"
cell1.birthRate = 92.0
cell1.lifetime = 20.0
cell1.velocity = 59.0
cell1.velocityRange = -15.0
cell1.xAcceleration = 5.0
cell1.yAcceleration = 40.0
cell1.emissionRange = 180.0 * (.pi / 180.0)
cell1.spin = -28.6 * (.pi / 180.0)
cell1.spinRange = 57.2 * (.pi / 180.0)
cell1.scale = 0.06
cell1.scaleRange = 0.3
cell1.color = UIColor(red: 255.0/255.0, green: 255.0/255.0, blue: 255.0/255.0, alpha: 1.0).cgColor
particlesLayer.emitterCells = [cell1]
return host
}
func updateUIView(_ uiView: UIView, context: Context) {
}
typealias UIViewType = UIView
}
struct TestEmitterLayer: View {
var body: some View {
EmitterView() // << usage !!
}
}

How do I get the coordinates from CAShapeLayer

So I am trying to make a progress bar. So I have made circular path, but I want the dot to be at the end of the progress bar, but how do I get the position of the dot to be att the end of the current progress?
private func simpleShape() {
let width: CGFloat = 10
createCircle()
//make circle transparant in middle
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.strokeColor = UIColor.blue.cgColor
progressLayer.lineCap = CAShapeLayerLineCap.round
progressLayer.lineWidth = width
progressLayer.strokeStart = 0
progressLayer.strokeEnd = 0
//unfilled
backLayer.lineWidth = width
backLayer.strokeColor = #colorLiteral(red: 0.1411764706, green: 0.1725490196, blue: 0.2431372549, alpha: 1).cgColor
backLayer.strokeEnd = 1
self.layer.addSublayer(gradientLayer)
}
private func createCircle() {
//create circle
let circle = UIView(frame: bounds)
circle.layoutIfNeeded()
let centerPoint = CGPoint (x: circle.bounds.width / 2, y: circle.bounds.width / 2)
let circleRadius: CGFloat = circle.bounds.width / 2 * 0.83
let circlePath = UIBezierPath(arcCenter: centerPoint, radius: circleRadius, startAngle: CGFloat(-0.475 * Double.pi), endAngle: CGFloat(1.525 * Double.pi), clockwise: true)
//add layers
progressLayer.path = circlePath.cgPath
backLayer.path = circlePath.cgPath
circle.layer.addSublayer(backLayer)
circle.layer.addSublayer(progressLayer)
addSubview(circle)
circle.layer.addSublayer(dotLayer)
}
let dotLayer = CAShapeLayer()
public func setProgress(_ progress: CGFloat) {
progressLayer.strokeEnd = CGFloat(progress)
if let progressEndpoint = progressLayer.path?.currentPoint {
dotLayer.position = progressEndpoint
}
}
This is what I'm getting
This is what I want
You’re going to have to calculate it yourself. So figure out the angle from the start and end angles for your arcs:
let angle = (endAngle - startAngle) * progress + startAngle
And then use basic trigonometry to determine where that point falls:
let point = CGPoint(x: centerPoint.x + radius * cos(angle),
y: centerPoint.y + radius * sin(angle))
dotLayer.position = point
By the way, I’d suggest separating the adding of the sublayers (which is part of the initial configuration process) from the updating paths (which is part of the view layout process, which may be called again if the frame of the view changes, constraints are applied, etc). Thus, perhaps:
#IBDesignable
class ProgressView: UIView {
var progress: CGFloat = 0 { didSet { updateProgress() } }
private var centerPoint: CGPoint = .zero
private var radius: CGFloat = 0
private let startAngle: CGFloat = -0.475 * .pi
private let endAngle: CGFloat = 1.525 * .pi
private let lineWidth: CGFloat = 10
private lazy var progressLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.lineCap = .round
shapeLayer.lineWidth = lineWidth
shapeLayer.strokeStart = 0
shapeLayer.strokeEnd = progress
return shapeLayer
}()
private lazy var backLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = lineWidth
shapeLayer.strokeColor = #colorLiteral(red: 0.1411764706, green: 0.1725490196, blue: 0.2431372549, alpha: 1).cgColor
return shapeLayer
}()
private lazy var dotLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.path = UIBezierPath(arcCenter: .zero, radius: lineWidth / 2 * 1.75, startAngle: 0, endAngle: 2 * .pi, clockwise: true).cgPath
shapeLayer.fillColor = UIColor.white.withAlphaComponent(0.5).cgColor
return shapeLayer
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSublayers()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
addSublayers()
}
override func layoutSubviews() {
super.layoutSubviews()
updatePaths()
updateProgress()
}
}
private extension ProgressView {
func addSublayers() {
layer.addSublayer(backLayer)
layer.addSublayer(progressLayer)
layer.addSublayer(dotLayer)
}
func updatePaths() {
centerPoint = CGPoint(x: bounds.midX, y: bounds.midY)
radius = min(bounds.width, bounds.height) / 2 * 0.83
let circlePath = UIBezierPath(arcCenter: centerPoint, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
progressLayer.path = circlePath.cgPath
backLayer.path = circlePath.cgPath
}
func updateProgress() {
progressLayer.strokeEnd = progress
let angle = (endAngle - startAngle) * progress + startAngle
let point = CGPoint(x: centerPoint.x + radius * cos(angle),
y: centerPoint.y + radius * sin(angle))
dotLayer.position = point
}
}
What you need is rotation Animation
let progressLayer = CAShapeLayer()
let backLayer = CAShapeLayer()
private func simpleShape() {
let width: CGFloat = 15
createCircle()
//make circle transparant in middle
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.strokeColor = #colorLiteral(red: 0.888897419, green: 0.5411034822, blue: 0.04008810222, alpha: 1)
progressLayer.lineCap = CAShapeLayerLineCap.round
progressLayer.lineWidth = width
progressLayer.strokeStart = 0
progressLayer.strokeEnd = 0
//unfilled
backLayer.lineWidth = width
backLayer.strokeColor = #colorLiteral(red: 0.1411764706, green: 0.1725490196, blue: 0.2431372549, alpha: 1)
backLayer.strokeEnd = 1
// self.layer.addSublayer(gradientLayer)
}
private func createCircle() {
//create circle
let circle = UIView(frame: bounds)
let centerPoint = CGPoint (x: circle.bounds.width / 2, y: circle.bounds.width / 2)
let circleRadius: CGFloat = circle.bounds.width / 2 * 0.83
let distance = circle.bounds.width / 2 * 0.17
let circlePath = UIBezierPath(arcCenter: centerPoint, radius: circleRadius, startAngle: CGFloat(-0.475 * Double.pi), endAngle: CGFloat(1.525 * Double.pi), clockwise: true)
//add layers
progressLayer.path = circlePath.cgPath
backLayer.path = circlePath.cgPath
circle.layer.addSublayer(backLayer)
circle.layer.addSublayer(progressLayer)
addSubview(circle)
let circleCenter = CGPoint(x:centerPoint.x - distance,y:centerPoint.y - circleRadius )
let dotCircle = UIBezierPath()
dotCircle.addArc(withCenter:circleCenter, radius: 3, startAngle: CGFloat(-90).deg2rad(), endAngle: CGFloat(270).deg2rad(), clockwise: true)
dotLayer.path = dotCircle.cgPath
dotLayer.position = CGPoint(x:centerPoint.x,y:centerPoint.y )
dotLayer.strokeColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0.6496753961)
dotLayer.lineWidth = 10
dotLayer.fillColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
dotLayer.isHidden = true
circle.layer.addSublayer(dotLayer)
}
let dotLayer = CAShapeLayer()
public func setProgress(_ progress: CGFloat) {
print(progress)
// progressLayer.strokeEnd = progress
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.beginTime = CACurrentMediaTime() + 0.5;
animation.fromValue = 0
animation.toValue = progress
animation.duration = 2
animation.autoreverses = false
animation.repeatCount = .nan
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
progressLayer.add(animation, forKey: "line")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.dotLayer.isHidden = false
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
// rotateAnimation.beginTime = CACurrentMediaTime() + 0.5;
rotateAnimation.fromValue = (CGFloat( -90)).deg2rad()
rotateAnimation.toValue = (360*progress - 98).deg2rad()
rotateAnimation.duration = 2
rotateAnimation.fillMode = .forwards
rotateAnimation.isRemovedOnCompletion = false
self.dotLayer.add(rotateAnimation, forKey: nil)
}
}

Display an array of CAShapeLayers

I have created an array of CAShapeLayers in order to draw different portions of an arc in different colors on different layers, this is my code :
import UIKit
class ViewController: UIViewController {
var level = 0.0
var old_level = 0.75
var progressLayer: [CAShapeLayer] = []
var circle = UIView()
override func viewDidLoad() {
circle = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height))
view.addSubview(circle)
update_curve(value: 10)
update_curve(value: 20)
update_curve(value: 10)
}
func update_curve(value: Double){
level = value*(1.5/100)+0.75
progressLayer.append(CAShapeLayer())
var progressPath = UIBezierPath(arcCenter: CGPoint(x: progressLayer[layerindex].frame.size.width/2, y: progressLayer[layerindex].frame.size.height/2), radius: CGFloat(100), startAngle: CGFloat(M_PI*old_level), endAngle:CGFloat(level*M_PI), clockwise: true)
circle.layer.addSublayer(progressLayer[layerindex])
progressLayer[layerindex].frame = view.bounds
progressLayer[layerindex].path = progressPath.cgPath
progressLayer[layerindex].fillColor = UIColor.clear.cgColor
progressLayer[layerindex].strokeColor = generateRandomColor()
progressLayer[layerindex].lineWidth = 20.0
let animation2 = CABasicAnimation(keyPath: "strokeEnd")
animation2.fromValue = 0.0
animation2.toValue = 1.0
animation2.duration = 1
animation2.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
progressLayer[layerindex].add(animation2, forKey: "drawLineAnimation")
layerindex += 1
old_level = level
}
func generateRandomColor() -> UIColor {
let hue : CGFloat = CGFloat(arc4random() % 256) / 256 // use 256 to get full range from 0.0 to 1.0
let saturation : CGFloat = CGFloat(arc4random() % 128) / 256 + 0.5 // from 0.5 to 1.0 to stay away from white
let brightness : CGFloat = CGFloat(arc4random() % 128) / 256 + 0.5 // from 0.5 to 1.0 to stay away from black
return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1)
}
}
But when I run this code, nothing appears on the screen of the iphone, but I don't get an error. What am I doing wrong?
I have just realised that the frame of the progressLayer was being defined after the defining the progressPath. The code for the update_curve function should be :
func update_curve(value: Double){
level = value*(1.5/100)+0.75
progressLayer.append(CAShapeLayer())
progressLayer[layerindex].frame = view.bounds
var progressPath = UIBezierPath(arcCenter: CGPoint(x: progressLayer[layerindex].frame.size.width/2, y: progressLayer[layerindex].frame.size.height/2), radius: CGFloat(100), startAngle: CGFloat(M_PI*old_level), endAngle:CGFloat(level*M_PI), clockwise: true)
circle.layer.addSublayer(progressLayer[layerindex])
progressLayer[layerindex].path = progressPath.cgPath
progressLayer[layerindex].fillColor = UIColor.clear.cgColor
progressLayer[layerindex].strokeColor = generateRandomColor()
progressLayer[layerindex].lineWidth = 20.0
let animation2 = CABasicAnimation(keyPath: "strokeEnd")
animation2.fromValue = 0.0
animation2.toValue = 1.0
animation2.duration = 1
animation2.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
progressLayer[layerindex].add(animation2, forKey: "drawLineAnimation")
layerindex += 1
old_level = level
}

Resources