CAShapeLayer position incorrect - ios

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.

Related

issue adding layer around a circular view's border

I am adding a circular layer around my circular view and setting its position to be the center of the view but the layer gets added at a different position. The following is the code.
class CustomView: UIView {
let outerLayer = CAShapeLayer()
required init?(coder: NSCoder) {
super.init(coder: coder)
self.layer.addSublayer(outerLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
self.backgroundColor = UIColor.systemBlue
self.layer.cornerRadius = self.bounds.height/2
self.layer.borderWidth = 2.0
self.layer.borderColor = UIColor.white.cgColor
let outerLayerFrame = self.bounds.insetBy(dx: -5.0, dy: -5.0)
outerLayer.frame = outerLayerFrame
let path = UIBezierPath(ovalIn: outerLayerFrame)
outerLayer.path = path.cgPath
outerLayer.position = self.center
outerLayer.strokeColor = UIColor.systemBlue.cgColor
outerLayer.fillColor = UIColor.clear.cgColor
outerLayer.lineWidth = 3
}
}
Can anyone please tell me what is wrong here.
You want to set your layer(s) properties when the view is instantiated / initialized, but its size can (and usually does) change between then and when auto-layout adjusts it to the current device dimensions.
Unlike the UIView itself, layers do not auto-resize.
So, you want to set framing and layer paths in layoutSubviews():
class CustomView: UIView {
let outerLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
// layoutSubviews is where you want to set your size
// and corner radius values
override func layoutSubviews() {
super.layoutSubviews()
// make self "round"
self.layer.cornerRadius = self.bounds.height/2
// add a round path for outer layer
let path = UIBezierPath(ovalIn: bounds)
outerLayer.path = path.cgPath
}
private func configure() {
self.backgroundColor = UIColor.systemBlue
// setup layer properties here
self.layer.borderWidth = 2.0
self.layer.borderColor = UIColor.white.cgColor
outerLayer.strokeColor = UIColor.systemBlue.cgColor
outerLayer.fillColor = UIColor.clear.cgColor
outerLayer.lineWidth = 3
// add the sublayer here
self.layer.addSublayer(outerLayer)
}
}
Result (with the view set to 120 x 120 pts):
So, I realized, it's the position that is making the layer not aligned in center.
Here is the solution, I figured-
class CustomView: UIView {
let outerLayer = CAShapeLayer()
required init?(coder: NSCoder) {
super.init(coder: coder)
self.layer.addSublayer(outerLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
self.backgroundColor = UIColor.systemBlue
self.layer.cornerRadius = self.bounds.height/2
self.layer.borderWidth = 2.0
self.layer.borderColor = UIColor.white.cgColor
let outerLayerBounds = self.bounds.insetBy(dx: -5.0, dy: -5.0)
outerLayer.bounds = outerLayerBounds // should be bounds not frame
let path = UIBezierPath(ovalIn: outerLayerBounds)
outerLayer.path = path.cgPath
outerLayer.position = CGPoint(x: outerLayerBounds.midX , y: outerLayerBounds.midY) //self.center doesn't center the layer, had to calculate the center of the layer manually
outerLayer.strokeColor = UIColor.systemBlue.cgColor
outerLayer.fillColor = UIColor.clear.cgColor
outerLayer.lineWidth = 3
}
}

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

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?

CAShapeLayer not visible in XIB swift iOS

I'm building a custom view using an XIB file. However, I am facing a problem where the layers I add to the view (trackLayer) are not shown on the xib (circleLayer is an animation and I don't expect it to render in xib which is not possible to my knowledge). The code of the owner class for the XIB is shown as follows:
#IBDesignable
class SpinningView: UIView {
#IBOutlet var contentView: SpinningView!
//MARK: Properties
let circleLayer = CAShapeLayer()
let trackLayer = CAShapeLayer()
let circularAnimation: CABasicAnimation = {
let animation = CABasicAnimation()
animation.fromValue = 0
animation.toValue = 1
animation.duration = 2
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
return animation
}()
//MARK: IB Inspectables
#IBInspectable var strokeColor: UIColor = UIColor.red {
...
}
#IBInspectable var trackColor: UIColor = UIColor.lightGray {
...
}
#IBInspectable var lineWidth: CGFloat = 5 {
...
}
#IBInspectable var fillColor: UIColor = UIColor.clear {
...
}
#IBInspectable var isAnimated: Bool = true {
...
}
//MARK: Initialization
override init(frame: CGRect) {
super.init(frame: frame)
initSubviews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
initSubviews()
}
//MARK: Private functions
private func setup() {
setupTrack()
setupAnimationOnTrack()
}
private func setupTrack(){
trackLayer.lineWidth = lineWidth
trackLayer.fillColor = fillColor.cgColor
trackLayer.strokeColor = trackColor.cgColor
layer.addSublayer(trackLayer)
}
private func setupAnimationOnTrack(){
circleLayer.lineWidth = lineWidth
circleLayer.fillColor = fillColor.cgColor
circleLayer.strokeColor = strokeColor.cgColor
circleLayer.lineCap = kCALineCapRound
layer.addSublayer(circleLayer)
updateAnimation()
}
private func updateAnimation() {
if isAnimated {
circleLayer.add(circularAnimation, forKey: "strokeEnd")
}
else {
circleLayer.removeAnimation(forKey: "strokeEnd")
}
}
//MARK: Layout contraints
override func layoutSubviews() {
super.layoutSubviews()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width / 2, bounds.height / 2) - circleLayer.lineWidth / 2
let startAngle = -CGFloat.pi / 2
let endAngle = 3 * CGFloat.pi / 2
let path = UIBezierPath(arcCenter: .zero, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
trackLayer.position = center
trackLayer.path = path.cgPath
circleLayer.position = center
circleLayer.path = path.cgPath
}
private func initSubviews() {
let bundle = Bundle(for: SpinningView.self)
let nib = UINib(nibName: "SpinningView", bundle: bundle)
nib.instantiate(withOwner: self, options: nil)
contentView.frame = bounds
addSubview(contentView)
}
}
When a view is subclassed in the Main.storyboard, I can see the image and the tracklayer as follows IB UIView but when I go over to the XIB, it does not have the trackLayer(circle around the image) XIB Image. While one can argue that it is working and why I am bothering with this, I think it is important that I design the XIB properly since another person might just see it as an view with only an image (no idea of the animating feature)
When you are designing your XIB, it is not an instance of your #IBDesignable SpinningView. You are, effectively, looking at the source of your SpinningView.
So, the code behind it is not running at that point - it only runs when you add an instance of SpinningView to another view.
Take a look at this simple example:
#IBDesignable
class SpinningView: UIView {
#IBOutlet var contentView: UIView!
#IBOutlet var theLabel: UILabel!
//MARK: Initialization
override init(frame: CGRect) {
super.init(frame: frame)
initSubviews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initSubviews()
}
private func initSubviews() {
let bundle = Bundle(for: SpinningView.self)
let nib = UINib(nibName: "SpinningView", bundle: bundle)
nib.instantiate(withOwner: self, options: nil)
contentView.frame = bounds
addSubview(contentView)
// this will change the label's textColor in Storyboard
// when a UIView's class is set to SpinningView
theLabel.textColor = .red
}
}
When I am designing the XIB, this is what I see:
But when I add a UIView to another view, and assign its class as SpinningView, this is what I see:
The label's text color get's changed to red, because the code is now running.
Another way to think about it... you have trackColor, fillColor, etc vars defined as #IBInspectable. When you're designing your XIB, you don't see those properties in Xcode's Attributes Inspector panel - you only see them when you've selected an instance of SpinningView that has been added elsewhere.
Hope that makes sense.

Custom UIButton background after orientation change

Learning Swift.
I have an UIButton class. Here is it:
class CustomBtn: UIButton {
var shadowLayer: CAShapeLayer!
var shadowAdded: Bool = false
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
override func layoutSubviews() {
super.layoutSubviews()
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
super.drawRect(rect)
shadowLayer = CAShapeLayer()
shadowLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 5).CGPath
shadowLayer.fillColor = UIColor(netHex: Colors.Red1.value).CGColor
shadowLayer.shadowColor = UIColor.darkGrayColor().CGColor
shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowOffset = CGSize(width: 2.0, height: 2.0)
shadowLayer.shadowOpacity = 0.8
shadowLayer.shadowRadius = 2
layer.insertSublayer(shadowLayer, atIndex: 0)
}
}
And this is my output:
But when I change orientation:
What would you do to get the button background (red color and the shadow) fill the width in any orientation?
You don't really need to override drawRect for this, so is best to heed the advice in the comment above it. But, the problem is you are setting your layers path based on your button's bounds, but those bounds change when you rotate the device, but you're not updating your layer.
Also many of the effects you're trying to achieve can be done by setting properties on your button's layer itself. I would change your class to this:
class CustomBtn: UIButton {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
self.commonInit()
}
override init(frame:CGRect){
super.init(frame: frame)
self.commonInit()
}
func commonInit(){
self.layer.backgroundColor = UIColor(netHex: Colors.Red1.value).CGColor
self.layer.cornerRadius = 5
self.layer.shadowColor = UIColor.darkGrayColor().CGColor
self.layer.shadowOffset = CGSize(width: 2.0, height: 2.0)
self.layer.shadowOpacity = 0.8
self.layer.shadowRadius = 2
}
}
You can get your class to be slightly more performant by adding to commonInit:
self.layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: 5).CGPath
But if you do that you'll also want to add:
override var bounds: CGRect{
didSet{
self.commonInit()
}
}
So anytime the bounds change your shadow path will be updated.

Resources