How can I centre a circular progress bar in a UIView using Swift? - swift

I'm having quite a bit of difficulty simply ensuring a circular progress bar is always in the centre of the UIView it is associated to.
This is what is happening:
Ignore the grey region, this is simply the UIView on a placeholder card. The red is the UIView I have added as an outlet to the UIViewController.
Below is the code for the class that I have made:
class CircleProgress: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
var progressLyr = CAShapeLayer()
var trackLyr = CAShapeLayer()
var progressClr = UIColor.red {
didSet {
progressLyr.strokeColor = progressClr.cgColor
}
}
var trackClr = UIColor.black {
didSet {
trackLyr.strokeColor = trackClr.cgColor
}
}
private func setupView() {
self.backgroundColor = UIColor.red
let centre = CGPoint(x: frame.size.width/2, y: frame.size.height/2)
let circlePath = UIBezierPath(arcCenter: centre,
radius: 10,
startAngle: 0,
endAngle: 2 * CGFloat.pi,
clockwise: true)
trackLyr.path = circlePath.cgPath
trackLyr.fillColor = UIColor.clear.cgColor
trackLyr.strokeColor = trackClr.cgColor
trackLyr.lineWidth = 5.0
trackLyr.strokeEnd = 1.0
layer.addSublayer(trackLyr)
}
}
The aim is simply to have all 4 edges of the black circle touching the edges of the red square.
Any help is hugely appreciated. I'm thinking it must be too obvious but this has cost me too many hours tonight. :)

The problem is that there is no frame being passed when creating the object. No need for changing anything to your code. For sure you have to change the width and the height to whatever you want.
Here is an example...
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
// Add circleProgress
addCircleProgress()
}
private func addCircleProgress() {
let circleProgress = CircleProgress(frame: CGRect(x: self.view.center.x - 50, y: self.view.center.x - 50, width: 100, height: 100))
self.view.addSubview(circleProgress)
}
}
class CircleProgress: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var progressLyr = CAShapeLayer()
var trackLyr = CAShapeLayer()
var progressClr = UIColor.red {
didSet {
progressLyr.strokeColor = progressClr.cgColor
}
}
var trackClr = UIColor.black {
didSet {
trackLyr.strokeColor = trackClr.cgColor
}
}
private func setupView() {
self.backgroundColor = UIColor.red
let centre = CGPoint(x: frame.size.width/2, y: frame.size.height/2)
let circlePath = UIBezierPath(arcCenter: centre,
radius: 50,
startAngle: 0,
endAngle: 2 * CGFloat.pi,
clockwise: true)
trackLyr.path = circlePath.cgPath
trackLyr.fillColor = UIColor.clear.cgColor
trackLyr.strokeColor = trackClr.cgColor
trackLyr.lineWidth = 5.0
trackLyr.strokeEnd = 1.0
layer.addSublayer(trackLyr)
}
}

A few suggestions:
I’d suggest making this CircularProgress take up the whole view. If you want this to be inset within the view, then, fine, add a property for that. In my example below, I created a property called inset to capture this value.
Make sure to update your path in layoutSubviews. Especially if you use constraints, you want this to respond to size changes. So add these layers from init, but update the path in layoutSubviews.
Don’t reference frame (which is the location within the superview coordinate system). Use bounds. And inset it by half the line width, so the circle doesn’t exceed the bounds of the view.
You created a progress color and progress layer, but didn’t use either one. I’ve guessed you wanted that to show the progress within the track.
You are stroking your path from 0 to 2π. People tend to expect these circular progress views to start from -π/2 (12 o’clock) and progress to 3π/2. So I’ve updated the path to use those values. But use whatever you want.
If you want, you can make it #IBDesignable if you want to see this rendered in IB.
Thus, pulling that together, you get:
#IBDesignable
public class CircleProgress: UIView {
#IBInspectable
public var lineWidth: CGFloat = 5 { didSet { updatePath() } }
#IBInspectable
public var strokeEnd: CGFloat = 1 { didSet { progressLayer.strokeEnd = strokeEnd } }
#IBInspectable
public var trackColor: UIColor = .black { didSet { trackLayer.strokeColor = trackColor.cgColor } }
#IBInspectable
public var progressColor: UIColor = .red { didSet { progressLayer.strokeColor = progressColor.cgColor } }
#IBInspectable
public var inset: CGFloat = 0 { didSet { updatePath() } }
private lazy var trackLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.fillColor = UIColor.clear.cgColor
layer.strokeColor = trackColor.cgColor
layer.lineWidth = lineWidth
return layer
}()
private lazy var progressLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.fillColor = UIColor.clear.cgColor
layer.strokeColor = progressColor.cgColor
layer.lineWidth = lineWidth
return layer
}()
override init(frame: CGRect = .zero) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
public override func layoutSubviews() {
super.layoutSubviews()
updatePath()
}
}
private extension CircleProgress {
func setupView() {
layer.addSublayer(trackLayer)
layer.addSublayer(progressLayer)
}
func updatePath() {
let rect = bounds.insetBy(dx: lineWidth / 2 + inset, dy: lineWidth / 2 + inset)
let centre = CGPoint(x: rect.midX, y: rect.midY)
let radius = min(rect.width, rect.height) / 2
let path = UIBezierPath(arcCenter: centre,
radius: radius,
startAngle: -.pi / 2,
endAngle: 3 * .pi / 2,
clockwise: true)
trackLayer.path = path.cgPath
trackLayer.lineWidth = lineWidth
progressLayer.path = path.cgPath
progressLayer.lineWidth = lineWidth
}
}
That yields:

Update this function
private func setupView() {
self.backgroundColor = UIColor.red
let centre = CGPoint(x: bounds.midX, y: bounds.midY)
let circlePath = UIBezierPath(arcCenter: centre,
radius: bounds.maxX/2,
startAngle: 0,
endAngle: 2 * CGFloat.pi,
clockwise: true)
trackLyr.path = circlePath.cgPath
trackLyr.fillColor = UIColor.clear.cgColor
trackLyr.strokeColor = trackClr.cgColor
trackLyr.lineWidth = 5.0
trackLyr.strokeEnd = 1.0
layer.addSublayer(trackLyr)
}

Related

Animate a CALayer frame change

I'm trying to create a Custom Progress Bar View and the idea is to have a timer that will update every second. However, the transition from the progress is not so smooth.
If I reduce the timer interval to something like 0.01 instead of 0.1 the "animation" works nicely; however, I'm trying to avoid it.
There's some way to make this animation smooth using 1 second on the Timer interval?
class CustomProgressBar: UIView {
private let progressLayer = CALayer()
var progress: CGFloat = 1.0 {
didSet {
setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
addSublayers()
startTimer()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
addSublayers()
startTimer()
}
private func startTimer() {
Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateProgressBar), userInfo: nil, repeats: true)
}
override func draw(_ rect: CGRect) {
let backgroundMask = CAShapeLayer()
backgroundMask.path = UIBezierPath(roundedRect: rect, cornerRadius: rect.height * 0.5).cgPath
layer.mask = backgroundMask
updateLayer(rect: rect, color: .white)
backgroundColor = .black
progressLayer.backgroundColor = UIColor.white.cgColor
}
private func addSublayers() {
layer.addSublayer(progressLayer)
}
#objc func updateProgressBar() {
progress -= 0.1
}
private func updateLayer(rect: CGRect, color: UIColor?) {
let progressRectBorderSize = rect.height * 0.2
let width: CGFloat
width = max(rect.width * progress - (progressRectBorderSize * 2), 0)
let progressRect = CGRect(origin: CGPoint(x: progressRectBorderSize, y: progressRectBorderSize), size: CGSize(width: width, height: rect.height - (progressRectBorderSize * 2)))
let progressBackgroundMask = CAShapeLayer()
let progressBackgroundMaskRoundedRect = CGRect(x: 0, y: 0, width: progressRect.width, height: progressRect.height)
progressBackgroundMask.path = UIBezierPath(roundedRect: progressBackgroundMaskRoundedRect, cornerRadius: progressRect.height * 0.5).cgPath
progressLayer.mask = progressBackgroundMask
progressLayer.frame = progressRect
}
}
Here is how I create CustomProgressBar for you according to your needs.
I gave it a run, it works smoothly. You can customize it according to your Design needs.
class CustomProgressBar: UIView {
private var progressLayerBackground = CAShapeLayer()
private var progressLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func draw(_ rect: CGRect) {
progressLayerBackground = createBarShapeLayer(strokeColor: .black, rect: rect, margin: 10)
layer.addSublayer(progressLayerBackground)
progressLayer = createBarShapeLayer(strokeColor: .white, rect: rect, margin: 10)
progressLayer.lineWidth = 15
progressLayer.strokeEnd = 0 // This define how much will be width of bar at start.
layer.addSublayer(progressLayer)
animateBar()
backgroundColor = .white
progressLayerBackground.backgroundColor = UIColor.white.cgColor
}
private func addSublayers() {
layer.addSublayer(progressLayerBackground)
}
private func createBarShapeLayer(strokeColor: UIColor, rect: CGRect, margin: CGFloat) -> CAShapeLayer {
let layer = CAShapeLayer()
let linePath = UIBezierPath()
linePath.move(to: CGPoint(x: rect.width - margin, y: rect.height / 2))
linePath.addLine(to: CGPoint(x: rect.minX + margin , y: rect.height / 2))
layer.path = linePath.cgPath
layer.strokeColor = strokeColor.cgColor
layer.lineWidth = 20
layer.strokeEnd = 1
layer.lineCap = .round
return layer
}
private func animateBar() {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = 2 // This defines how much will be duration of animation.
basicAnimation.fillMode = .forwards
basicAnimation.beginTime = CACurrentMediaTime() + 0.5
basicAnimation.isRemovedOnCompletion = false
progressLayer.add(basicAnimation, forKey: "moveHorizontally")
}
}
Demo: https://drive.google.com/file/d/1iGwcPPe7uQROa9s276t4SIsHY42fTJ1D/view?usp=sharing

The background mask cuts off part of the foreground layer

Recently ran into a problem in one of my tasks. I tried to solve this problem for two days. Finally I came here.
I have a custom progress view. Don't look at the colors, they are so awful just for the debugging process to see better.
[the look I have now] 1
[the look I want to have] 2
As you can see, there is a red dot at the end of the progress layer, but it is cut at the top and bottom ...
And it shouldn't be like that ...
Also I will leave here all the code I have, maybe someone
can help me.
Thank you all in advance for your time.
import UIKit
class PlainProgressBar: UIView {
//MARK: - Private
//colour of progress layer : default -> white
private var color: UIColor = .white {
didSet { setNeedsDisplay() }
}
// progress numerical value
private var progress: CGFloat = 0.0 {
didSet { setNeedsDisplay() }
}
private var height : CGFloat = 0.0 {
didSet{ setNeedsDisplay() }
}
private let progressLayer = CALayer()
private let dotLayer = CALayer()
private let backgroundMask = CAShapeLayer()
private func setupLayers() {
layer.addSublayer(progressLayer)
layer.addSublayer(dotLayer)
}
//MARK:- Public
// set color for progress layer
func setColor(progressLayer color: UIColor){
self.color = color
}
func set(progress: CGFloat){
if progress > 1.0{
self.progress = 1.0
}
if progress < 0.0{
self.progress = 0.0
}
if progress >= 0.0 && progress <= 1.0{
self.progress = progress
}
}
func set(height: CGFloat){
self.height = height
}
override init(frame: CGRect) {
super.init(frame: frame)
setupLayers()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupLayers()
}
//Main draw function for view
override func draw(_ rect: CGRect) {
backgroundMask.path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: CGSize(width: rect.width, height: height)), cornerRadius: rect.height * 0.25).cgPath
layer.mask = backgroundMask
let dotOnProgressLayer = CGRect(origin: .zero, size: CGSize(width: 10.0, height: 10.0))
let progressRect = CGRect(origin: .zero, size: CGSize(width: rect.width * progress, height: height))
progressLayer.frame = progressRect
progressLayer.backgroundColor = color.cgColor
dotLayer.frame = dotOnProgressLayer
dotLayer.cornerRadius = dotLayer.bounds.width * 0.5
dotLayer.backgroundColor = #colorLiteral(red: 0.9254902005, green: 0.2352941185, blue: 0.1019607857, alpha: 1).cgColor
dotLayer.position = CGPoint(x: progressLayer.frame.width ,y: progressLayer.frame.height / 2)
}
}
progressView.set(height: 4.5)
progressView.setColor(progressLayer: UIColor(ciColor: .green))
You are masking the views layer with
layer.mask = backgroundMask
So anything outside the mask will no be drawn.

How to generate a custom UIView that is above a UIImageView that has a circular hole punched in the middle that can see through to the UIImageView?

I am trying to punch a circular hole through a UIView that is above a UIImageView, whereby the hole can see through to the image below (I would like to interact with this image through the hole with a GestureRecognizer later). I have 2 problems, I cannot get the circular hole to centre to the middle of the UIImageView (it is currently centred to the top left of the screen), and that the effect that I am getting is the opposite to what i am trying to achieve (Everything outside of the circle is visible). Below is my code. Please can someone advise?
Result:
class UploadProfileImageViewController: UIViewController {
var scrollView: ReadyToUseScrollView!
let container: UIView = {
let view = UIView()
view.backgroundColor = UIColor.black
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let imgView: UIImageView = {
let imgView = UIImageView()
imgView.backgroundColor = UIColor.black
imgView.contentMode = .scaleAspectFit
imgView.image = UIImage.init(named: "soldier")!
imgView.translatesAutoresizingMaskIntoConstraints = false
return imgView
}()
var overlay: UIView!
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
private func setup(){
view.backgroundColor = UIColor.white
setupViews()
}
override func viewDidLayoutSubviews() {
overlay.center = imgView.center
print("imgView.center: \(imgView.center)")
overlay.layer.layoutIfNeeded() // I have also tried view.layoutIfNeeded()
}
private func setupViews(){
let s = view.safeAreaLayoutGuide
view.addSubview(imgView)
imgView.topAnchor.constraint(equalTo: s.topAnchor).isActive = true
imgView.leadingAnchor.constraint(equalTo: s.leadingAnchor).isActive = true
imgView.trailingAnchor.constraint(equalTo: s.trailingAnchor).isActive = true
imgView.heightAnchor.constraint(equalTo: s.heightAnchor, multiplier: 0.7).isActive = true
overlay = Overlay.init(frame: .zero, center: imgView.center)
print("setup.imgView.center: \(imgView.center)")
view.addSubview(overlay)
overlay.translatesAutoresizingMaskIntoConstraints = false
overlay.topAnchor.constraint(equalTo: s.topAnchor).isActive = true
overlay.leadingAnchor.constraint(equalTo: s.leadingAnchor).isActive = true
overlay.trailingAnchor.constraint(equalTo: s.trailingAnchor).isActive = true
overlay.bottomAnchor.constraint(equalTo: imgView.bottomAnchor).isActive = true
}
private func deg2rad( number: Double) -> CGFloat{
let rad = number * .pi / 180
return CGFloat.init(rad)
}
}
class Overlay: UIView{
var path: UIBezierPath!
var viewCenter: CGPoint?
init(frame: CGRect, center: CGPoint) {
super.init(frame: frame)
self.viewCenter = center
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup(){
backgroundColor = UIColor.black.withAlphaComponent(0.8)
guard let path = createCirclePath() else {return}
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillRule = CAShapeLayerFillRule.evenOdd
self.layer.mask = shapeLayer
}
private func createCirclePath() -> UIBezierPath?{
guard let center = self.viewCenter else{return nil}
let circlePath = UIBezierPath()
circlePath.addArc(withCenter: center, radius: 200, startAngle: 0, endAngle: deg2rad(number: 360), clockwise: true)
return circlePath
}
private func deg2rad( number: Double) -> CGFloat{
let rad = number * .pi / 180
return CGFloat.init(rad)
}
}
CONSOLE:
setup.imgView.center: (0.0, 0.0)
imgView.center: (207.0, 359.0)
Try getting rid of the overlay you have and instead add the below UIView. It's basically a circular UIView with a giant black border, but it takes up the whole screen so the user can't tell. FYI, you need to use .frame to position items on the screen. The below puts the circle in the center of the screen. If you want the center of the image, replace self.view.frame with self. imgView.frame... Play around with circleSize and borderSize until you get the circle size you want.
let circle = UIView()
let circleSize: CGFloat = self.view.frame.height * 2 //must be bigger than the screen
let x = (self.view.frame.width / 2) - (circleSize / 2)
let y = (self.view.frame.height / 2) - (circleSize / 2)
circle.frame = CGRect(x: x, y: y, width: circleSize, height: circleSize)
let borderSize = (circleSize / 2) * 0.9 //the size of the inner circle will be circleSize - borderSize
circle.backgroundColor = .clear
circle.layer.cornerRadius = circle.frame.height / 2
circle.layer.borderColor = UIColor.black.cgColor
circle.layer.borderWidth = borderSize
view.addSubview(circle)

Change color gradient around donut view

I am trying to make an animated donut view that when given a value between 0 and 100 it will animate round the view up to that number. I have this working fine but want to fade the color from one to another, then another on the way around. Currently, when I add my gradient it goes from left to right and not around the circumference of the donut view.
class CircleScoreView: UIView {
private let outerCircleLayer = CAShapeLayer()
private let outerCircleGradientLayer = CAGradientLayer()
private let outerCircleLineWidth: CGFloat = 5
override init(frame: CGRect) {
super.init(frame: .zero)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
buildLayers()
}
/// Value must be within 0...100 range
func setScore(_ value: Int, animated: Bool = false) {
if value != 0 {
let clampedValue: CGFloat = CGFloat(value.clamped(to: 0...100)) / 100
if !animated {
outerCircleLayer.strokeEnd = clampedValue
} else {
let outerCircleAnimation = CABasicAnimation(keyPath: "strokeEnd")
outerCircleAnimation.duration = 1.0
outerCircleAnimation.fromValue = 0
outerCircleAnimation.toValue = clampedValue
outerCircleAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
outerCircleLayer.strokeEnd = clampedValue
outerCircleLayer.add(outerCircleAnimation, forKey: "outerCircleAnimation")
}
outerCircleGradientLayer.colors = [Constant.Palette.CircleScoreView.startValue.cgColor,
Constant.Palette.CircleScoreView.middleValue.cgColor,
Constant.Palette.CircleScoreView.endValue.cgColor]
}
}
private func buildLayers() {
// Outer background circle
let arcCenter = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
let startAngle = CGFloat(-0.5 * Double.pi)
let endAngle = CGFloat(1.5 * Double.pi)
let circlePath = UIBezierPath(arcCenter: arcCenter,
radius: (frame.size.width - outerCircleLineWidth) / 2,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
// Outer circle
setupOuterCircle(outerCirclePath: circlePath)
}
private func setupOuterCircle(outerCirclePath: UIBezierPath) {
outerCircleLayer.path = outerCirclePath.cgPath
outerCircleLayer.fillColor = UIColor.clear.cgColor
outerCircleLayer.strokeColor = UIColor.black.cgColor
outerCircleLayer.lineWidth = outerCircleLineWidth
outerCircleLayer.lineCap = CAShapeLayerLineCap.round
outerCircleGradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
outerCircleGradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
outerCircleGradientLayer.frame = bounds
outerCircleGradientLayer.mask = outerCircleLayer
layer.addSublayer(outerCircleGradientLayer)
}
}
I am going for something like this but the color isn't one block but gradients around the donut view from one color to the next.
If you imported AngleGradientLayer into your project then all you should need to do is change:
private let outerCircleGradientLayer = CAGradientLayer() to
private let outerCircleGradientLayer = AngleGradientLayer()

Why doesn't UIView.animateWithDuration affect this custom view?

I designed a custom header view that masks an image and draws a border on the bottom edge, which is an arc. It looks like this:
Here's the code for the class:
class HeaderView: UIView
{
private let imageView = UIImageView()
private let dimmerView = UIView()
private let arcShape = CAShapeLayer()
private let maskShape = CAShapeLayer() // Masks the image and the dimmer
private let titleLabel = UILabel()
#IBInspectable var image: UIImage? { didSet { self.imageView.image = self.image } }
#IBInspectable var title: String? { didSet {self.titleLabel.text = self.title} }
#IBInspectable var arcHeight: CGFloat? { didSet {self.setupLayers()} }
// MARK: Initialization
override init(frame: CGRect)
{
super.init(frame:frame)
initMyStuff()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder:aDecoder)
initMyStuff()
}
override func prepareForInterfaceBuilder()
{
backgroundColor = UIColor.clear()
}
internal func initMyStuff()
{
backgroundColor = UIColor.clear()
titleLabel.font = Font.AvenirNext_Bold(24)
titleLabel.text = "TITLE"
titleLabel.textColor = UIColor.white()
titleLabel.layer.shadowColor = UIColor.black().cgColor
titleLabel.layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
titleLabel.layer.shadowRadius = 0.0;
titleLabel.layer.shadowOpacity = 1.0;
titleLabel.layer.masksToBounds = false
titleLabel.layer.shouldRasterize = true
imageView.contentMode = UIViewContentMode.scaleAspectFill
addSubview(imageView)
dimmerView.frame = self.bounds
dimmerView.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
addSubview(dimmerView)
addSubview(titleLabel)
// Add the shapes
self.layer.addSublayer(arcShape)
self.layer.addSublayer(maskShape)
self.layer.masksToBounds = true // This seems to be unneeded...test more
// Set constraints
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView .autoPinEdgesToSuperviewEdges()
titleLabel.autoCenterInSuperview()
}
func setupLayers()
{
let aHeight = arcHeight ?? 10
// Create the arc shape
arcShape.path = AppocalypseUI.createHorizontalArcPath(CGPoint(x: 0, y: bounds.size.height), width: bounds.size.width, arcHeight: aHeight)
arcShape.strokeColor = UIColor.white().cgColor
arcShape.lineWidth = 1.0
arcShape.fillColor = UIColor.clear().cgColor
// Create the mask shape
let maskPath = AppocalypseUI.createHorizontalArcPath(CGPoint(x: 0, y: bounds.size.height), width: bounds.size.width, arcHeight: aHeight, closed: true)
maskPath.moveTo(nil, x: bounds.size.width, y: bounds.size.height)
maskPath.addLineTo(nil, x: bounds.size.width, y: 0)
maskPath.addLineTo(nil, x: 0, y: 0)
maskPath.addLineTo(nil, x: 0, y: bounds.size.height)
//let current = CGPathGetCurrentPoint(maskPath);
//print(current)
let mask_Dimmer = CAShapeLayer()
mask_Dimmer.path = maskPath.copy()
maskShape.fillColor = UIColor(red: 0, green: 0, blue: 0, alpha: 1.0).cgColor
maskShape.path = maskPath
// Apply the masks
imageView.layer.mask = maskShape
dimmerView.layer.mask = mask_Dimmer
}
override func layoutSubviews()
{
super.layoutSubviews()
// Let's go old school here...
imageView.frame = self.bounds
dimmerView.frame = self.bounds
setupLayers()
}
}
Something like this will cause it to just snap to the new size without gradually changing its frame:
UIView.animate(withDuration: 1.0)
{
self.headerView.arcHeight = self.new_headerView_arcHeight
self.headerView.frame = self.new_headerView_frame
}
I figure it must have something to do with the fact that I'm using CALayers, but I don't really know enough about what's going on behind the scenes.
EDIT:
Here's the function I use to create the arc path:
class func createHorizontalArcPath(_ startPoint:CGPoint, width:CGFloat, arcHeight:CGFloat, closed:Bool = false) -> CGMutablePath
{
// http://www.raywenderlich.com/33193/core-graphics-tutorial-arcs-and-paths
let arcRect = CGRect(x: startPoint.x, y: startPoint.y-arcHeight, width: width, height: arcHeight)
let arcRadius = (arcRect.size.height/2) + (pow(arcRect.size.width, 2) / (8*arcRect.size.height));
let arcCenter = CGPoint(x: arcRect.origin.x + arcRect.size.width/2, y: arcRect.origin.y + arcRadius);
let angle = acos(arcRect.size.width / (2*arcRadius));
let startAngle = CGFloat(M_PI)+angle // (180 degrees + angle)
let endAngle = CGFloat(M_PI*2)-angle // (360 degrees - angle)
// let startAngle = radians(180) + angle;
// let endAngle = radians(360) - angle;
let path = CGMutablePath();
path.addArc(nil, x: arcCenter.x, y: arcCenter.y, radius: arcRadius, startAngle: startAngle, endAngle: endAngle, clockwise: false);
if(closed == true)
{path.addLineTo(nil, x: startPoint.x, y: startPoint.y);}
return path;
}
BONUS:
Setting the arcHeight property to 0 results in no white line being drawn. Why?
The Path property can't be animated. You have to approach the problem differently. You can draw an arc 'instantly', any arc, so that tells us that we need to handle the animation manually. If you expect the entire draw process to take say 3 seconds, then you might want to split the process to 1000 parts, and call the arc drawing function 1000 times every 0.3 miliseconds to draw the arc again from the beginning to the current point.
self.headerView.arcHeight is not a animatable property. It is only UIView own properties are animatable
you can do something like this
let displayLink = CADisplayLink(target: self, selector: #selector(update))
displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSDefaultRunLoopMode
let expectedFramesPerSecond = 60
var diff : CGFloat = 0
func update() {
let diffUpdated = self.headerView.arcHeight - self.new_headerView_arcHeight
let done = (fabs(diffUpdated) < 0.1)
if(!done){
self.headerView.arcHeight -= diffUpdated/(expectedFramesPerSecond*0.5)
self.setNeedsDisplay()
}
}