Updating context fill color on didSet - ios

I made a custom UIView which is basically a colored circle. However, when I change the circle's color property, it's not updating. How can I do this?
Code
class CircleView : UIView {
var color = UIColor.blue {
didSet {
// WHAT DO I DO!?
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {return}
context.addEllipse(in: rect)
context.setFillColor(color.cgColor)
context.fillPath()
}
}

Try the following one
var color = UIColor.blue {
didSet {
setNeedsDisplay()
}
}

Related

CAShapeLayer is off center

I have been trying to create a a custom "slider" or knob with UIControl for my app. I found this tutorial and have been using it for some inspiration, but since it does not really accomplish what I want to do I am using it as more of a reference than a tutorial. Anyways, I wrote the code below and found that my CAShapeLayer was not in the center of the UIView that I set to be an instance of my custom class CircularSelector.
Here is my code:
class CircularSelector: UIControl {
private let renderer = CircularSelectorRenderer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
renderer.updateBounds(bounds)
//layer.borderWidth = 4
// layer.borderColor = UIColor.red.cgColor
layer.addSublayer(renderer.selectorLayer)
}
}
class CircularSelectorRenderer {
let selectorLayer = CAShapeLayer()
init() {
selectorLayer.fillColor = UIColor.clear.cgColor
selectorLayer.strokeColor = UIColor.white.cgColor
//Testing
//selectorLayer.borderColor = UIColor.white.cgColor
// selectorLayer.borderWidth = 4
}
private func updateSelectorLayerPath() {
let bounds = selectorLayer.bounds
let arcCenter = CGPoint(x: bounds.midX, y: bounds.maxY)
let radius = 125
var ring = UIBezierPath(arcCenter: arcCenter, radius: CGFloat(radius), startAngle: 0.degreesToRadians, endAngle: 180.degreesToRadians, clockwise: false)
selectorLayer.lineWidth = 10
selectorLayer.path = ring.cgPath
}
func updateBounds(_ bounds: CGRect) {
selectorLayer.bounds = bounds
selectorLayer.position = CGPoint(x: bounds.midX, y: bounds.midY)
updateSelectorLayerPath()
}
}
This is what I get:
and when I uncomment the code under the //Testing line in the init() of the CircularSelectorRenderer I get this:
The grey UIView is of the class CircularSelector. I am confused as to why my CAShapeLayer is wider than the grey view itself and why it is not in the center of the grey view. Why is this happening and how can I fix it.
The problem is that you are updating the shape in the wrong place. Views can (and will) change sizes for different device sizes, superview sizes, device rotation, etc.
You want to update your shape layer when your view is told to layout its subviews:
class CircularSelector: UIControl {
private let renderer = CircularSelectorRenderer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
// no need to call this here
//renderer.updateBounds(bounds)
//layer.borderWidth = 4
// layer.borderColor = UIColor.red.cgColor
layer.addSublayer(renderer.selectorLayer)
}
// add this func
override func layoutSubviews() {
super.layoutSubviews()
// here is where you want to update the layer
renderer.updateBounds(bounds)
}
}

Swift UIButton Subclass and change color based on variable

I'm using a subclass for my UIButton and it has a variable called isActive. I need to change the button border color based on that variable. This variable will change programmatically. Please help me with this.
#IBDesignable
class buttonCTAOutlineDark: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override func prepareForInterfaceBuilder() {
commonInit()
}
#IBInspectable var isActive: Bool {
get {
return self.isActive
}
set (active) {
if active {
commonInit(isActive: active)
}
}
}
func commonInit(isActive: Bool = false) {
self.backgroundColor = .clear
self.layer.cornerRadius = 4
self.layer.borderWidth = 1
if (isActive) {
self.tintColor = ACTIVE_COLOR
self.layer.borderColor = ACTIVE_COLOR.cgColor
} else {
self.tintColor = nil
self.layer.borderColor = UIColor(red:0.69, green:0.72, blue:0.77, alpha:1.0).cgColor
}
}
}
You should be observing didSet to update the view. In Swift, type names should start with Uppercase e.g ButtonCTAOutlineDark. Please see the fixed class,
#IBDesignable
class ButtonCTAOutlineDark: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
#IBInspectable var isActive: Bool = false {
didSet {
self.commonInit(isActive: self.isActive)
}
}
func commonInit(isActive: Bool = false) {
self.backgroundColor = .clear
self.layer.cornerRadius = 4
self.layer.borderWidth = 1
if (isActive) {
self.tintColor = ACTIVE_COLOR
self.layer.borderColor = ACTIVE_COLOR.cgColor
} else {
self.tintColor = nil
self.layer.borderColor = UIColor(red:0.69, green:0.72, blue:0.77, alpha:1.0).cgColor
}
}
}
Your isActive property is written incorrectly. It should not be a computed property in the first place. Currently, the getter will just cause infinite recursion and the setter doesn't actually set anything.
The isActive property should be a stored property with a didSet property observer:
#IBInspectable
var isActive: Bool {
didSet {
}
}
Inside didSet, you can just put the last part of commonInit. The first part of commonInit doesn't need to be run every time isActive changes. I recommend you to extract that as a method called updateBorder:
func updateBorder(isActive: Bool) {
if (isActive) {
self.tintColor = ACTIVE_COLOR
self.layer.borderColor = ACTIVE_COLOR.cgColor
} else {
self.tintColor = nil
self.layer.borderColor = UIColor(red:0.69, green:0.72, blue:0.77, alpha:1.0).cgColor
}
}
And then in didSet, you can just call that:
updateBorder(isActive: isActive)

Animate setFillColor color change in custom UIView

I have a custom UIView called CircleView which is essentially a colored ellipse. The color property I'm using to color the ellipse is rendered using setFillColor on the graphics context. I was wondering if there was a way to animate the color change, because when I run through the animate / transition the color changes immediately instead of being animated.
Example Setup
let c = CircleView()
c.frame = CGRect(x: 20, y: 20, width: 100, height: 100)
c.color = UIColor.blue
c.backgroundColor = UIColor.clear
self.view.addSubview(c)
UIView.transition(with: c, duration: 5, options: .transitionCrossDissolve, animations: {
c.color = UIColor.red // Not animated
})
UIView.animate(withDuration: 5) {
c.color = UIColor.yellow // Not animated
}
Circle View
class CircleView : UIView {
var color = UIColor.blue {
didSet {
setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {return}
context.addEllipse(in: rect)
context.setFillColor(color.cgColor)
context.fillPath()
}
}
You can use the built in animation support for the layer's backgroundColor.
While the easiest way to make a circle is to make your view a square (using aspect ratio constraints, for instance) and then set the cornerRadius to half the width or height, I assume you want something a bit more advanced, and that is why you used a path.
My solution to this would be something like:
class CircleView : UIView {
var color = UIColor.blue {
didSet {
layer.backgroundColor = color.cgColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
// Setup the view, by setting a mask and setting the initial color
private func setup(){
layer.mask = shape
layer.backgroundColor = color.cgColor
}
// Change the path in case our view changes it's size
override func layoutSubviews() {
super.layoutSubviews()
let path = CGMutablePath()
// add an elipse, or what ever path/shapes you want
path.addEllipse(in: bounds)
// Created an inverted path to use as a mask on the view's layer
shape.path = UIBezierPath(cgPath: path).reversing().cgPath
}
// this is our shape
private var shape = CAShapeLayer()
}
Or if you really need a simple circle, just something like:
class CircleView : UIView {
var color = UIColor.blue {
didSet {
layer.backgroundColor = color.cgColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup(){
clipsToBounds = true
layer.backgroundColor = color.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.height / 2
}
}
Either way, this will animate nicely:
UIView.animate(withDuration: 5) {
self.circle.color = .red
}
Strange things happens!
Your code is ok, you just need to call your animation in another method and asyncronusly
As you can see, with
let c = CircleView()
override func viewDidLoad() {
super.viewDidLoad()
c.frame = CGRect(x: 20, y: 20, width: 100, height: 100)
c.color = UIColor.blue
c.backgroundColor = UIColor.clear
self.view.addSubview(c)
changeColor()
}
func changeColor(){
DispatchQueue.main.async
{
UIView.transition(with: self.c, duration: 5, options: .transitionCrossDissolve, animations: {
self.c.color = UIColor.red // Not animated
})
UIView.animate(withDuration: 5) {
self.c.color = UIColor.yellow // Not animated
}
}
}
Work as charm.
Even if you add a button that trigger the color change, when you press the button the animation will work.
I encourage you to set this method in the definition of the CircleView
func changeColor(){
DispatchQueue.main.async
{
UIView.transition(with: self, duration: 5, options: .transitionCrossDissolve, animations: {
self.color = UIColor.red
})
UIView.animate(withDuration: 5) {
self.color = UIColor.yellow
}
}
}
and call it where you want in your ViewController, simply with
c.changeColor()

UIButton backgroundRect(forBounds:) not being called

I have a custom hexagon shaped button and I want to disable the background color so it wont create a square shape around the hexagon itself.
I am using the backgroundRect(forBounds:) method to override the background's rect.
Here is the full implementation:
class HexaButton : UIButton {
var shape : CAShapeLayer!
func setup() {
if shape == nil {
shape = CAShapeLayer()
shape.fillColor = self.backgroundColor?.cgColor
shape.strokeColor = UIColor.cyan.cgColor
shape.lineWidth = 5.0
self.backgroundColor = UIColor.yellow
self.layer.addSublayer(shape)
}
let side : CGFloat = 6
let r = frame.height/side
shape.path = ShapeDrawer.polygonPath(x: frame.width/2, y: frame.height/2, radius: r, sides: Int(side), pointyness: side/2.0)
}
override func backgroundRect(forBounds bounds: CGRect) -> CGRect {
return .zero
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override func layoutSubviews() {
super.layoutSubviews()
setup()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if shape?.path?.contains(point) == true {
return self
} else {
return nil
}
}
}
For some unknown reason, the backgroundRect(forBounds:) isn't being called.
Any Ideas?
You need to set the backgroundImage property of your UIButton and backgroundRect method will be called.

DrawRect overriding properties of view

I'm creating a custom view which will basically just be a checkmark inside a cirlce. I've created a circle using layer.cornerRadius = 0.5 * frame.width but when I override drawRect, it causes that line to be ignored and my view becomes a square.
Why is this? And how can I fix this so I can create the circle with a checkmark inside it?
Here is my current code:
class CheckMarkView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
layer.cornerRadius = 0.5 * frame.width
backgroundColor = .whiteColor()
hidden = true
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Simply add clipsToBounds to true, and takes all the frame that is available to it.
class CheckMarkView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
layer.cornerRadius = 0.5 * frame.width
backgroundColor = UIColor.whiteColor()
clipsToBounds = true
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func drawRect(rect: CGRect) {
super.drawRect(rect)
}
}

Resources