DrawRect overriding properties of view - ios

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

Related

iOS - CALayer protruding bounds on orientation change

I have a simple CustomView. I have a CAGradientLayer that I am adding to the layer.
#IBDesignable
class CustomView: UIView {
override init (frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func layoutSubviews() {
super.layoutSubviews()
setupView()
}
private func setupView() {
let gradient = CAGradientLayer()
gradient.frame = bounds
gradient.colors = [UIColor.blue.cgColor, UIColor.yellow.cgColor]
layer.insertSublayer(gradient, at: 0)
}
}
Below is the Storyboard setup
Below is the result in portrait on iPad, which is fine.
When the device orientation is changed, the layer of the CustomView is protruded from its bounds.
When I checked the view hierarchy in Xcode, the frame of the CustomView is adjusted accordingly, but, it is the layer which is protruding. To cross check, I removed all codes in setupView and set only a background colour, on orientation change, CustomView is adjusted as expected. But, why does it happen for layers?
You are inserting a new layer each time when layoutSubviews method call. That means it shows the line of the first layer.
Just update the frame from layoutSubviews and call setup method from awakeFromNib.
class CustomView: UIView {
private let gradient = CAGradientLayer()
override init (frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func layoutSubviews() {
super.layoutSubviews()
gradient.frame = bounds
}
override func awakeFromNib() {
super.awakeFromNib()
setupView()
}
private func setupView() {
gradient.colors = [UIColor.blue.cgColor, UIColor.yellow.cgColor]
layer.insertSublayer(gradient, at: 0)
}
}

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

How to fit a thumb to custom size switch?

I'm working on the custom UISwitch. I have changed size using this:
self.transform = CGAffineTransform(scaleX: 1.25, y: 1.16).
And now I have one problem, the thumb is still default size.
How can I fit it with uiswitch?
class CustomSwitch:UISwitch {
override init(frame: CGRect) {
super.init(frame: frame)
self.viewDidLoad()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.viewDidLoad()
}
func viewDidLoad() {
self.transform = CGAffineTransform(scaleX: 1.25, y: 1.16)
self.setupAppearance()
self.setColors()
self.addTarget(self, action: #selector(toggleState), for: .valueChanged)
}
func setupAppearance() {
self.layer.borderColor = UIColor.HavelockBlue.cgColor
self.layer.borderWidth = 1.0
self.layer.cornerRadius = self.bounds.height / 2
}
func setColors() {
self.backgroundColor = .white
self.subviews.first?.subviews.first?.backgroundColor = .clear
self.onTintColor = .white
self.thumbTintColor = .HavelockBlue
}
#objc func toggleState() {
if self.isOn {
print("Dark mode is on")
} else {
print("Dark mode is off")
}
}
}
Your problem is, you settings constrained width and height for your custom UISwitch, and after then you are trying to transform this object, but what actually happen.
Inside this override init(frame: CGRect) and required init?(coder: NSCoder) methods if you using auto layout you don't have actually final size of your UIView, the size is taken from IB. But you are setting self.layer.cornerRadius = self.bounds.height / 2. If you will print values of frame.size and bounds.size you will see.
Simple solution is to remove constrained sizes from IB and just transform to your desire scale.
Example:
required init?(coder: NSCoder) {
super.init(coder: coder)
changeSwitchSize()
}
override init(frame: CGRect) {
super.init(frame: frame)
changeSwitchSize()
}
private func changeSwitchSize() {
print("Before transform switch frame size: \(frame.size), bounds size: \(bounds.size)")
self.transform = CGAffineTransform(scaleX: 1.25, y: 1.16)
print("After transform switch frame size: \(frame.size), bounds size: \(bounds.size)")
}
/// Before transform switch frame: (51.0, 31.0), (51.0, 31.0)
/// After transform switch frame: (63.75, 35.95999999999998), (51.0, 31.0)
But be aware than CGAffineTransform change view's frame relative to its superview
More about it: https://stackoverflow.com/a/11288488/6057764

Updating context fill color on didSet

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

Why I got this strange behavior for UIView with gradient background?

I am trying to add a gradient layer to a UIView (ContentView) that contains a UILabel.
My UIView implementation:
class ContentView: UIView {
override init() {
super.init()
styleIt()
}
override init(frame: CGRect) {
super.init(frame: frame)
styleIt()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
styleIt()
}
private func styleIt() {
var gradient = CAGradientLayer()
gradient.colors = [Utils.Colors.gray_ee.CGColor, Utils.Colors.gray_dd.CGColor]
gradient.frame = bounds
layer.insertSublayer(gradient, atIndex: 0)
layer.borderColor = Utils.Colors.gray_aa.CGColor
layer.borderWidth = 1
layer.cornerRadius = 7.0
}
}
But here is what I got as a result shown in the iPhone 6 iOS Simulator
Only the UIView with cornerRadius is having a strange behavior all other elements in this screenshot are not UIView elements.
I have tried to comment out border-related code and radius code, but still same issue.
Edit: Here is another screenshot with green background for the container (UIScrollView) and all other blah blah labels are within a ContentView element to show the issue in a bigger context.
Try something like this
private var gradient: CAGradientLayer! // Make it a private variable
override init(frame: CGRect) {
// Only init(frame:) and init(coder:) are designated initializers. You don't need to override init(). Internally init() will call init(frame:)
super.init(frame: frame)
commonInit()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
// Create & add the layer
gradient = CAGradientLayer()
layer.addSublayer(gradient)
// You need to set locations, startPoint, endPoint apart from colors
// See Apple documentation for more info
gradient.locations = [0.0, 1.0]
gradient.startPoint = CGPointMake(0, 0)
gradient.endPoint = CGPointMake(0, 1)
gradient.colors = [UIColor.whiteColor().CGColor, UIColor.greenColor().CGColor];
}
override func layoutSubviews() {
super.layoutSubviews()
// bounds may not be correct in init(), better set the layer's frame here
gradient.frame = bounds
}

Resources