CAShapeLayer is off center - ios

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

Related

CAShapeLayer clipped by image view clip to bounds

I am trying to customize UIImage view to include a red/green circle to show online status. My current code is like so:
import UIKit
class AvatarImageView: UIImageView {
enum AvatarImageViewOnlineStatus {
case online
case offline
case hidden
}
var onlineStatus: AvatarImageViewOnlineStatus = .hidden
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.cornerRadius = frame.size.width / 2.0
let circleShapeLayer: CAShapeLayer = CAShapeLayer()
circleShapeLayer.path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 30, height: 30)).cgPath
circleShapeLayer.fillColor = UIColor.red.cgColor
layer.addSublayer(circleShapeLayer)
}
}
However the layer is also clipped:
maskToBounds on the layer is by default set to false the docs state:
A Boolean indicating whether sublayers are clipped to the layer’s bounds. Animatable.
However it is still clipped. What am I doing wrong here?

Button not rendering

class SwiftyRecordButton: SwiftyCamButton {
private var circleBorder: CALayer!
private var innerCircle: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
drawButton()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
drawButton()
}
private func drawButton() {
self.backgroundColor = UIColor.white
circleBorder = CALayer()
circleBorder.backgroundColor = UIColor.red.cgColor
circleBorder.borderWidth = 6.0
circleBorder.borderColor = UIColor.blue.cgColor
circleBorder.bounds = self.bounds
circleBorder.position = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
circleBorder.cornerRadius = self.frame.size.width / 2
layer.insertSublayer(circleBorder, at: 0)
}
}
Im having an issue with the rendering of this button. It is somewhat of a special button that I use in place of the standard one that comes from swifty cams library. The issue comes with the drawButton function. Even though I clearly called it I can't see the snapchat style button with a border that should exist there. Can anyone see where I went wrong?

CAShapeLayer position incorrect

I'm having issues drawing a circle using CAShapeLayer. I have a custom view MainLogoAnimationView code as follows:
import UIKit
class MainLogoAnimationView: UIView {
#IBOutlet var customView: UIView!
var redCircle: AnimationCircleView!
// MARK: Object Lifecycle
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
_ = Bundle.main.loadNibNamed("MainLogoAnimationView", owner: self, options: nil)?[0] as! UIView
self.addSubview(customView)
customView.frame = self.bounds
let circleWidth = frame.size.width*0.25
let yPos = frame.size.height/2 - circleWidth/2
redCircle = AnimationCircleView(frame: CGRect(x: 10, y: yPos, width: circleWidth, height: circleWidth))
redCircle.backgroundColor = UIColor.yellow
addSubview(redCircle)
}
override func layoutSubviews() {
super.layoutSubviews()
}
}
In interface builder I created a light blue UIView and selected MainLogoAnimationView as the subview.
The AnimationCircleView is like so:
import UIKit
class AnimationCircleView: UIView {
// MARK: Object lifecycle
var circleColor = UIColor.red.withAlphaComponent(0.5).cgColor {
didSet {
shapeLayer.fillColor = circleColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
let shapeLayer = CAShapeLayer()
func setup() {
let circlePath = UIBezierPath(ovalIn: frame)
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = circleColor
shapeLayer.frame = self.frame
shapeLayer.backgroundColor = UIColor.gray.cgColor
layer.addSublayer(shapeLayer)
}
}
Screenshot:
Is seems the CAShapeLayer frame is incorrect and also the red circle is in the wrong position. It should be inside the yellow square. What am I doing wrong here? Any pointers would be really appreciated! Thanks!
UPDATE:
In your AnimationCircleView inside setup change
shapeLayer.frame = self.frame
to
shapeLayer.frame = self.bounds
This should ensure the gray colour is drawn inside the yellow rectangle.
Edit:
Similar to the answer above, changing
let circlePath = UIBezierPath(ovalIn: frame)
to
let circlePath = UIBezierPath(ovalIn: bounds)
will solve your problem and draw circle inside the gray area.
CAShapeLayer frame is calculated relative to the position of the view it is added on, soo setting it equal to the frame, adds the CAShapeLayer at a x and y position offset from the origin by the value of the x and y passed in self.frame.

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.

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