Converting a CAShapeLayer to use in an NSImageView - swift

I'm trying to port some iOS code for a Mac app. My code is as follows:
func innerRing() {
let innerRing = CAShapeLayer()
let circleRadius: CGFloat = 105.0
innerRing.frame = InnerRingView.bounds
func circleFrame() -> CGRect {
var circleFrame = CGRect(x: 0, y: 0, width: 2*circleRadius, height: 2*circleRadius)
circleFrame.origin.x = CGRectGetMidX(InnerRingView.bounds) - CGRectGetMidX(circleFrame)
circleFrame.origin.y = CGRectGetMidY(InnerRingView.bounds) - CGRectGetMidY(circleFrame)
return circleFrame
}
innerRing.path = UIBezierPath(ovalInRect: circleFrame()).CGPath
innerRing.lineWidth = 3.0
innerRing.strokeStart = 0.0
innerRing.strokeEnd = 1.0
innerRing.fillColor = UIColor.clearColor().CGColor
innerRing.strokeColor = UIColor(red: 147.0/255.0, green: 184.0/255.0, blue: 255.0/255.0, alpha: 1.0).CGColor
InnerRingView.layer.addSublayer(innerRing)
}
This code works very well, especially for adjusting the fill color, stroke color, and stroke start/end.
In my Mac app, I am effectively trying to use the same code, but apply it to an NSImageView (I want it to be able to appear on each row of a table, and I will adjust certain parameters (such as color) based on what that row details.
Could anyone assist with guidance on adding this simple circle to an NSImageView?

Why do you want to use an NSImageView? NSImageView is for displaying images (icons, pictures, etc).
Make yourself a custom NSView instead. Just remember that, unlike UIKit's UIView, NSView doesn't get a layer by default, so you need to tell It to by setting wantsLayer to true.
Like so:
class CircleView: NSView {
lazy var innerRing: CAShapeLayer = {
let innerRing = CAShapeLayer()
let circleRadius: CGFloat = 105.0
innerRing.frame = self.bounds
var circleFrame = CGRect(x: 0, y: 0, width: circleRadius, height: circleRadius)
circleFrame.origin.x = CGRectGetMidX(self.bounds) - CGRectGetMidX(circleFrame)
circleFrame.origin.y = CGRectGetMidY(self.bounds) - CGRectGetMidY(circleFrame)
innerRing.path = CGPathCreateWithEllipseInRect(circleFrame, nil)
innerRing.lineWidth = 3.0
innerRing.strokeStart = 0.0
innerRing.strokeEnd = 1.0
innerRing.fillColor = NSColor.clearColor().CGColor
innerRing.strokeColor = NSColor(red: 147.0/255.0, green: 184.0/255.0, blue: 255.0/255.0, alpha: 1.0).CGColor
return innerRing
}()
override func awakeFromNib() {
super.awakeFromNib()
wantsLayer = true
layer = CALayer()
layer?.addSublayer(innerRing)
}
}

Related

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.

Issue with CABasicAnimation

So basically I am trying to create dialogView. This dialogView will contain a loading spinner and some text which will be updated depending on the state. I followed two guides to use a CAShapeLayer to draw a circle and then animate a line around it.
Link to code/guides I followed
https://www.youtube.com/watch?v=O3ltwjDJaMk
https://github.com/vinayjn/Spinner/blob/master/Spinner.swift
Despite following most of these tutorials to the tee. My animation will not work. At the moment it just looks like this and will not spin.
class LoginDialogView: UIViewController {
lazy var loaderView: UIView = {
let loaderView = UIView()
loaderView.backgroundColor = .yellow
return loaderView
}()
lazy var dialogTitle : UILabel = {
let dialogTitle = UILabel()
dialogTitle.text = "Apples"
dialogTitle.font = UIFont.systemFont(ofSize: 24.0)
dialogTitle.textAlignment = .left
dialogTitle.numberOfLines = 1
return dialogTitle
}()
private var shouldAddSublayer: Bool {
/*
check if:
1. we have any sublayers at all, if we don't then its safe to add a new, so return true
2. if there are sublayers, see if "our" layer is there, if it is not, return true
*/
guard let sublayers = loaderView.layer.sublayers else { return true }
return sublayers.filter({ $0.name == "progress"}).count == 0
}
override func viewDidLoad() {
super.viewDidLoad()
setupView()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if shouldAddSublayer {
setupCircleLayers()
}
}
fileprivate func setupView() {
view.backgroundColor = .red
view.layer.cornerRadius = 20
view.addSubview(loaderView)
loaderView.snp.makeConstraints { (make) in
make.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(10)
make.centerX.equalTo(view.safeAreaLayoutGuide.snp.centerX)
make.height.width.equalTo(100)
}
}
private func setupCircleLayers() {
let trackLayer = createCircleShapeLayer(strokeColor: UIColor.init(red: 56/255, green: 25/255, blue: 49/255, alpha: 1), fillColor: #colorLiteral(red: 0.9686274529, green: 0.78039217, blue: 0.3450980484, alpha: 1))
loaderView.layer.addSublayer(trackLayer)
startAnimating()
}
private func startAnimating(){
animateRing()
}
private func animateRing(){
let rotationAnimation = CABasicAnimation(keyPath: "strokeEnd")
rotationAnimation.fromValue = 0 * (CGFloat.pi / 180)
rotationAnimation.toValue = 360 * (CGFloat.pi / 180)
rotationAnimation.duration = 1.6
rotationAnimation.repeatCount = HUGE
loaderView.layer.add(rotationAnimation, forKey: "strokeEnd")
}
private func createCircleShapeLayer(strokeColor: UIColor, fillColor: UIColor) -> CAShapeLayer {
let centerpoint = CGPoint.zero
let circularPath = UIBezierPath(arcCenter: centerpoint, radius: 30, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
let layer = CAShapeLayer()
layer.path = circularPath.cgPath
layer.fillColor = fillColor.cgColor
layer.strokeColor = strokeColor.cgColor
layer.strokeStart = 0
layer.strokeEnd = 0.5
layer.lineCap = .round
layer.lineWidth = 5
layer.position = CGPoint(x: loaderView.frame.size.width / 2, y: loaderView.frame.size.height / 2)
layer.name = "progress"
return layer
}
}
I have looked over and compared the code a couple times and I can't see what I did that was different. So my question is can anyone look over this code and figure out what I did wrong? and Can anyone recommend a safe way to stop this animation that wouldn't cause any potential memory leaks?
Im using ios 13 and xcode 11.3.1
You are not adding the animation to the shape layer; you added it to the container view's layer. Keep a reference to your trackLayer and add the animation to that layer.
class LoginDialogView: UIViewController {
lazy var loaderView: UIView = {
let loaderView = UIView()
loaderView.backgroundColor = .yellow
return loaderView
}()
var trackLayer: CAShapeLayer?
...
...
...
private func setupCircleLayers() {
trackLayer = createCircleShapeLayer(strokeColor: UIColor(red: 56/255,
green: 25/255,
blue: 49/255,
alpha: 1),
fillColor: #colorLiteral(red: 0.9686274529, green: 0.78039217, blue: 0.3450980484, alpha: 1))
loaderView.layer.addSublayer(trackLayer!)
startAnimating()
}
...
...
...
private func animateRing(){
let rotationAnimation = CABasicAnimation(keyPath: "strokeEnd")
rotationAnimation.fromValue = 0 * (CGFloat.pi / 180)
rotationAnimation.toValue = 360 * (CGFloat.pi / 180)
rotationAnimation.duration = 1.6
rotationAnimation.repeatCount = HUGE
trackLayer.add(rotationAnimation, forKey: "strokeEnd")
}
...
...
...
}
Note: ... in the above code refers to the truncated portion of your code.

Color animation

I've written simple animations for drawing rectangles in lines, we can treat them as a bars.
Each bar is one shape layer which has a path which animates ( size change and fill color change ).
#IBDesignable final class BarView: UIView {
lazy var pathAnimation: CABasicAnimation = {
let animation = CABasicAnimation(keyPath: "path")
animation.duration = 1
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animation.fillMode = kCAFillModeBoth
animation.isRemovedOnCompletion = false
return animation
}()
let red = UIColor(red: 249/255, green: 26/255, blue: 26/255, alpha: 1)
let orange = UIColor(red: 1, green: 167/255, blue: 463/255, alpha: 1)
let green = UIColor(red: 106/255, green: 239/255, blue: 47/255, alpha: 1)
lazy var backgroundColorAnimation: CABasicAnimation = {
let animation = CABasicAnimation(keyPath: "fillColor")
animation.duration = 1
animation.fromValue = red.cgColor
animation.byValue = orange.cgColor
animation.toValue = green.cgColor
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animation.fillMode = kCAFillModeBoth
animation.isRemovedOnCompletion = false
return animation
}()
#IBInspectable var spaceBetweenBars: CGFloat = 10
var numberOfBars: Int = 5
let data: [CGFloat] = [5.5, 9.0, 9.5, 3.0, 8.0]
override func awakeFromNib() {
super.awakeFromNib()
initSublayers()
}
override func layoutSubviews() {
super.layoutSubviews()
setupLayers()
}
func setupLayers() {
let width = bounds.width - (spaceBetweenBars * CGFloat(numberOfBars + 1)) // There is n + 1 spaces between bars.
let barWidth: CGFloat = width / CGFloat(numberOfBars)
let scalePoint: CGFloat = bounds.height / 10.0 // 10.0 - 10 points is max
guard let sublayers = layer.sublayers as? [CAShapeLayer] else { return }
for i in 0...numberOfBars - 1 {
let barHeight: CGFloat = scalePoint * data[i]
let shapeLayer = CAShapeLayer()
layer.addSublayer(shapeLayer)
var xPos: CGFloat!
if i == 0 {
xPos = spaceBetweenBars
} else if i == numberOfBars - 1 {
xPos = bounds.width - (barWidth + spaceBetweenBars)
} else {
xPos = barWidth * CGFloat(i) + spaceBetweenBars * CGFloat(i) + spaceBetweenBars
}
let startPath = UIBezierPath(rect: CGRect(x: xPos, y: bounds.height, width: barWidth, height: 0)).cgPath
let endPath = UIBezierPath(rect: CGRect(x: xPos, y: bounds.height, width: barWidth, height: -barHeight)).cgPath
sublayers[i].path = startPath
pathAnimation.toValue = endPath
sublayers[i].removeAllAnimations()
sublayers[i].add(pathAnimation, forKey: "path")
sublayers[i].add(backgroundColorAnimation, forKey: "backgroundColor")
}
}
func initSublayers() {
for _ in 1...numberOfBars {
let shapeLayer = CAShapeLayer()
layer.addSublayer(shapeLayer)
}
}
}
The size ( height ) of bar depends of the data array, each sublayers has a different height. Based on this data I've crated a scale.
PathAnimation is changing height of the bars.
BackgroundColorAnimation is changing the collors of the path. It starts from red one, goes through the orange and finish at green.
My goal is to connect backgroundColorAnimation with data array as well as it's connected with pathAnimation.
Ex. When in data array is going to be value 1.0 then the bar going to be animate only to the red color which is a derivated from a base red color which is declared as a global variable. If the value in the data array going to be ex. 4.5 then the color animation will stop close to the delcared orange color, the 5.0 limit going to be this orange color or color close to this. Value closer to 10 going to be green.
How could I connect these conditions with animation properties fromValue, byValue, toValue. Is it an algorithm for that ? Any ideas ?
You have several problems.
You're setting fillMode and isRemovedOnCompletion. This tells me, to be blunt, that you don't understand Core Animation. You need to watch WWDC 2011 Session 421: Core Animation Essentials.
You're adding more layers every time layoutSubviews is called, but not doing anything with them.
You're adding animation every time layoutSubviews runs. Do you really want to re-animate the bars when the double-height “in-call” status bar appears or disappears, or on an interface rotation? It's probably better to have a separate animateBars() method, and call it from your view controller's viewDidAppear method.
You seem to think byValue means “go through this value on the way from fromValue to toValue”, but that's not what it means. byValue is ignored in your case, because you're setting fromValue and toValue. The effects of byValue are explained in Setting Interpolation Values.
If you want to interpolate between colors, it's best to use a hue-based color space, but I believe Core Animation uses an RGB color space. So you should use a keyframe animation to specify intermediate colors that you calculate by interpolating in a hue-based color space.
Here's a rewrite of BarView that fixes all these problems:
#IBDesignable final class BarView: UIView {
#IBInspectable var spaceBetweenBars: CGFloat = 10
var data: [CGFloat] = [5.5, 9.0, 9.5, 3.0, 8.0]
var maxDatum = CGFloat(10)
func animateBars() {
guard window != nil else { return }
let bounds = self.bounds
var flatteningTransform = CGAffineTransform.identity.translatedBy(x: 0, y: bounds.size.height).scaledBy(x: 1, y: 0.001)
let duration: CFTimeInterval = 1
let frames = Int((duration * 60.0).rounded(.awayFromZero))
for (datum, barLayer) in zip(data, barLayers) {
let t = datum / maxDatum
if let path = barLayer.path {
let path0 = path.copy(using: &flatteningTransform)
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.duration = 1
pathAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
pathAnimation.fromValue = path0
barLayer.add(pathAnimation, forKey: pathAnimation.keyPath)
let colors = gradient.colors(from: 0, to: t, count: frames).map({ $0.cgColor })
let colorAnimation = CAKeyframeAnimation(keyPath: "fillColor")
colorAnimation.timingFunction = pathAnimation.timingFunction
colorAnimation.duration = duration
colorAnimation.values = colors
barLayer.add(colorAnimation, forKey: colorAnimation.keyPath)
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
createOrDestroyBarLayers()
let bounds = self.bounds
let barSpacing = (bounds.size.width - spaceBetweenBars) / CGFloat(data.count)
let barWidth = barSpacing - spaceBetweenBars
for ((offset: i, element: datum), barLayer) in zip(data.enumerated(), barLayers) {
let t = datum / maxDatum
let barHeight = t * bounds.size.height
barLayer.frame = bounds
let rect = CGRect(x: spaceBetweenBars + CGFloat(i) * barSpacing, y: bounds.size.height, width: barWidth, height: -barHeight)
barLayer.path = CGPath(rect: rect, transform: nil)
barLayer.fillColor = gradient.color(at: t).cgColor
}
}
private let gradient = Gradient(startColor: .red, endColor: .green)
private var barLayers = [CAShapeLayer]()
private func createOrDestroyBarLayers() {
while barLayers.count < data.count {
barLayers.append(CAShapeLayer())
layer.addSublayer(barLayers.last!)
}
while barLayers.count > data.count {
barLayers.removeLast().removeFromSuperlayer()
}
}
}
private extension UIColor {
var hsba: [CGFloat] {
var hue: CGFloat = 0
var saturation: CGFloat = 0
var brightness: CGFloat = 0
var alpha: CGFloat = 0
getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
return [hue, saturation, brightness, alpha]
}
}
private struct Gradient {
init(startColor: UIColor, endColor: UIColor) {
self.startColor = startColor
self.startHsba = startColor.hsba
self.endColor = endColor
self.endHsba = endColor.hsba
}
let startColor: UIColor
let endColor: UIColor
let startHsba: [CGFloat]
let endHsba: [CGFloat]
func color(at t: CGFloat) -> UIColor {
let out = zip(startHsba, endHsba).map { $0 * (1.0 - t) + $1 * t }
return UIColor(hue: out[0], saturation: out[1], brightness: out[2], alpha: out[3])
}
func colors(from t0: CGFloat, to t1: CGFloat, count: Int) -> [UIColor] {
var colors = [UIColor]()
colors.reserveCapacity(count)
for i in 0 ..< count {
let s = CGFloat(i) / CGFloat(count - 1)
let t = t0 * (1 - s) + t1 * s
colors.append(color(at: t))
}
return colors
}
}
Result:

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()
}
}

UIButton Borders Function Only Gives Back White Borders

I'm trying to create a Button Borders Function in Swift to help style my UI. However whatever RGB values I pass in/initialize the function only creates white borders.
Here is my function:
func buttonsWithBorders(button: UIButton, borderWidth: CGFloat, redcolour: CGFloat , greencolour: CGFloat, bluecolour: CGFloat, alpha: CGFloat?) {
let redcolour : CGFloat = 7.0
var greencolour : CGFloat = 3.0
var bluecolour : CGFloat = 2.0
var alpha: CGFloat = 1.0
var widthOfBorder: CGFloat = borderWidth
var theButtonWithBorders: UIButton
var buttonBorderColour : UIColor = UIColor(red: redcolour, green: greencolour, blue: bluecolour, alpha: alpha)
button.layer.borderWidth = widthOfBorder
return button.layer.borderColor = buttonBorderColour.CGColor
}
And I call it using:
buttonsWithBorders(learnHomeButton, 2.0,2.0, 5.0, 5.0, 1.0)
Also I know that passing in values and initializing them is incorrect but Xcode complaines that I am not initializing before using them otherwise
Any help would be very much appreciated, Cheers
You aren't initializing them. You're declaring entirely new variables with the same names as the parameters you're passing in. Whenever you use let or var you are introducing a brand new variable.
When a new variable is introduced with the same name as another currently in scope, this is known as variable shadowing, and what you have here is an almost textbook case.
A better, more concise implementation of your function might look like this:
func addButtonBorder(button: UIButton, width: CGFloat, red: CGFloat, blue: CGFloat, green: CGFloat, alpha: CGFloat = 1.0) {
button.layer.borderColor = UIColor(red: red, green: green, blue: blue, alpha: alpha).CGColor
button.layer.borderWidth = width
}
I used a different name because buttonsWithBorders implies that one or more buttons will be returned from this function. That does not appear to be your intent. Since you are passing one button in, you could only ever get one out, but "buttons" implies more than one.
If I were going to initialize a lot of buttons with borders, I might do something like this:
extension UIButton {
convenience init(frame: CGRect, borderColor: UIColor, borderWidth: CGFloat = 1.0) {
self.init(frame: frame)
setBorder(borderColor, borderWidth: borderWidth)
}
func setBorder(borderColor: UIColor, borderWidth: CGFloat = 1.0) {
layer.borderWidth = borderWidth
layer.borderColor = borderColor.CGColor
}
}
Then you could say UIButton(frame: frame, borderColor: borderColor, borderWidth: 2.0) to initialize a new button or button.setBorder(borderColor, borderWidth: 2.0) to set the border on an existing button.
UIColor takes a float between 0 and 1. So you want to divide your RGB Values by 255.0
Here is the code I used, that works on playground :
import Foundation
import UIKit
func buttonsWithBorders(button: UIButton, borderWidth: CGFloat,
redcolour:CGFloat, greencolour:CGFloat, bluecolour:CGFloat,
alpha:CGFloat) {
let buttonBorderColour : UIColor = UIColor(red: redcolour, green: greencolour, blue: bluecolour, alpha: alpha)
button.layer.borderWidth = borderWidth
return button.layer.borderColor = buttonBorderColour.CGColor
}
let learnHomeButton = UIButton(frame: CGRect(x: 0, y: 0, width: 50,
height: 50))
buttonsWithBorders(learnHomeButton, 2.0, 177/255.0, 177/255.0,
177/255.0, 1.0)
I edited the code so you can pass the colors to the function as parameters. Hope it helps.
The colour values need to be between 0.0 and 1.0 so you should define them as:
let redcolour : CGFloat = 7.0 / 255.0
var greencolour : CGFloat = 3.0 / 255.0
var bluecolour : CGFloat = 2.0 / 255.0