I am trying to add a CAShapeLayer to a view. But when I add the customView to ViewCOntroller's View, the shapeLayer doesn't show up. Capturing hierarchy shows that the layer doesn't even get inserted to the View. Here is the code:
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
...
override func didMoveToSuperview() {
let diameter = min(self.frame.width, self.frame.height)
let path = UIBezierPath(arcCenter: self.center, radius: diameter/2, startAngle: 0, endAngle: CGFloat.pi*2, clockwise: true)
let sLayer = CAShapeLayer()
sLayer.frame = bounds
sLayer.path = path.cgPath
sLayer.fillMode = .backwards
sLayer.fillColor = UIColor.blue.cgColor//bubbleColor?.cgColor
layer.addSublayer(sLayer)
}
And in ViewController:
override func viewDidLoad() {
super.viewDidLoad()
let node = Node(frame: CGRect(origin: self.view.center, size: CGSize(width: 250, height: 250)))
self.view.addSubview(node)
...
}
I really do not understand what is happening. Please help!
I'd suggest:
adding your sublayer in init (so it's added once and only once);
configure its frame and path in layoutSubviews (so if it's resized, whether by constraints or some other mechanism, the layer will update accordingly);
when adding a subview/sublayer whose dimensions is based upon the size of the current view, reference the view’s bounds (the coordinate system within the current view), not the frame nor center (both of which are in the coordinate system of the superview); and
either use addSublayer approach or layerClass approach, but not both.
Thus, adding CAShapeLayer as sublayer:
#IBDesignable
class CircleView: UIView {
#IBInspectable var bubbleColor: UIColor = .blue { didSet { shapeLayer.fillColor = bubbleColor.cgColor } }
private lazy var shapeLayer: CAShapeLayer = {
let sLayer = CAShapeLayer()
sLayer.fillColor = bubbleColor.cgColor
return sLayer
}()
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
override func layoutSubviews() {
super.layoutSubviews()
shapeLayer.frame = bounds
updatePath()
}
}
// MARK: - Utility methods
private extension CircleView {
func configure() {
layer.addSublayer(shapeLayer)
}
func updatePath() {
let diameter = min(bounds.width, bounds.height)
let arcCenter = CGPoint(x: bounds.midX, y: bounds.midY)
shapeLayer.path = UIBezierPath(arcCenter: arcCenter, radius: diameter / 2, startAngle: 0, endAngle: .pi * 2, clockwise: true).cgPath
}
}
Yielding:
Or, if you want to use layerClass approach:
#IBDesignable
class CircleView: UIView {
#IBInspectable var bubbleColor: UIColor = .blue { didSet { shapeLayer.fillColor = bubbleColor.cgColor } }
override class var layerClass: AnyClass { return CAShapeLayer.self }
private var shapeLayer: CAShapeLayer { return layer as! CAShapeLayer}
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
override func layoutSubviews() {
super.layoutSubviews()
updatePath()
}
}
// MARK: - Utility methods
private extension CircleView {
func configure() {
shapeLayer.fillColor = bubbleColor.cgColor
}
func updatePath() {
let diameter = min(bounds.width, bounds.height)
let arcCenter = CGPoint(x: bounds.midX, y: bounds.midY)
shapeLayer.path = UIBezierPath(arcCenter: arcCenter, radius: diameter / 2, startAngle: 0, endAngle: .pi * 2, clockwise: true).cgPath
}
}
Related
I am currently working on a round progress bar, to do so I created a customView. However the shapeLayer is not being displayed. I tried adding a frame to the shapeLayer as well as background colour, however that only showed a rectangle rather than the circularPath I am expecting to see. Any ideas as to what I am doing wrong :thinking:
import UIKit
class CircularProgressBar: UIView {
override func draw(_ rect: CGRect) {
setupProgressView()
}
private func setupProgressView() {
let shapeLayer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: center,
radius: 100,
startAngle: 0,
endAngle: 2*CGFloat.pi,
clockwise: true)
shapeLayer.path = circularPath.cgPath
shapeLayer.fillColor = UIColor.blue.cgColor
shapeLayer.strokeColor = UIColor.yellow.cgColor
shapeLayer.lineWidth = 20
self.layer.addSublayer(shapeLayer)
}
}
Set shapeLayer Frame
shapeLayer.frame = bounds
And better approach is to not use draw method
import UIKit
#IBDesignable
class CircularProgressBar: UIView {
private lazy var shapeLayer: CAShapeLayer = {
let shape = CAShapeLayer()
shape.fillColor = UIColor.blue.cgColor
shape.strokeColor = UIColor.yellow.cgColor
shape.lineWidth = 20
return shape
}()
override init(frame: CGRect) {
super.init(frame: frame)
addLayers()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
addLayers()
}
private func addLayers() {
layer.addSublayer(shapeLayer)
}
override func layoutSubviews() {
updatePath()
}
private func updatePath() {
let circularPath = UIBezierPath(arcCenter: CGPoint(x: bounds.midX, y: bounds.midY),
radius: min(bounds.midX, bounds.midY) - shapeLayer.lineWidth/2,
startAngle: 0,
endAngle: 2*CGFloat.pi,
clockwise: true)
shapeLayer.frame = bounds
shapeLayer.path = circularPath.cgPath
}
}
I am trying to create a custom view by extending the class UIView which can show a circle at the centre of the custom view.
In order to add custom drawing, I override draw(_ rect: CGRect) method as below.
public override func draw(_ rect: CGRect) {
super.draw(rect)
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
let minLength = bounds.width <= bounds.height ? bounds.width : bounds.height
let circlePath = UIBezierPath (arcCenter: center, radius: CGFloat(minLength / 2), startAngle: 0, endAngle: CGFloat(Float(360).degreesToRadians) , clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = circleColor.cgColor
layer.addSublayer(shapeLayer)
}
But when I try to add the same custom component in storyboard and added a breakpoint in draw method, it was not getting called. To my curiosity, I added a breakpoint to init?(coder aDecoder: NSCoder) method which was getting called. Also when I try to override the method as below, it worked fine.
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
drawCircle()
}
fileprivate func drawCircle() {
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
let minLength = bounds.width <= bounds.height ? bounds.width : bounds.height
let circlePath = UIBezierPath (arcCenter: center, radius: CGFloat(minLength / 2), startAngle: 0, endAngle: CGFloat(Float(360).degreesToRadians) , clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = circleColor.cgColor
layer.addSublayer(shapeLayer)
}
The issue with approach is I have a custom property circleColor
#IBInspectable
public var circleColor : UIColor = UIColor.gray
which is not yet set when init method is called and it always draws a gray circle.
Please help to get me know the reason why draw method is not being called.
Thanks in advance
I am not sure where your root mistake lies but you should not override draw(:) just to add a layer. That would mean a new layer will be added again every time the component rerenders. That's not what you want.
You should also make sure the layer color is updated when you change the color property. To do that, just save the reference to the layer:
private let shapeLayer = CAShapeLayer()
#IBInspectable
public var circleColor : UIColor = UIColor.gray {
didSet {
shapeLayer.fillColor = circleColor.cgColor
}
}
public override init(frame: CGRect = .zero) {
super.init(frame: frame)
updateLayer()
layer.addSublayer(shapeLayer)
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
updateLayer()
layer.addSublayer(shapeLayer)
}
fileprivate func updateLayer() {
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
let minLength = min(bounds.width, bounds.height)
let circlePath = UIBezierPath(arcCenter: center, radius: minLength / 2, startAngle: 0, endAngle: CGFloat(Float(360).degreesToRadians), clockwise: true)
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = circleColor.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
// update layer position
updateLayer()
}
If you really want to override draw(rect:), you don't need to create a layer or override initializers at all:
#IBInspectable
public var circleColor : UIColor = UIColor.gray {
didSet {
setNeedsDisplay()
}
}
public override func draw(_ rect: CGRect) {
// no need to call super.draw(rect)
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
let minLength = min(bounds.width, bounds.height)
let circlePath = UIBezierPath(arcCenter: center, radius: minLength / 2, startAngle: 0, endAngle: .pi * 2, clockwise: true)
// the following methods use current CGContext
circleColor.set()
circlePath.fill()
}
I need to have a view with borders, and only borders should be clickable.
Here is my code which creates custom view, but when I added gesture all view becomes clickable.
class RectangleView: UIView {
override func draw(_ rect: CGRect) {
let aPath = UIBezierPath()
aPath.move(to: CGPoint(x:0, y: 0))
aPath.addLine(to: CGPoint(x:rect.width, y: 0))
aPath.addLine(to: CGPoint(x:rect.width, y:rect.height))
aPath.addLine(to: CGPoint(x:0, y:rect.height))
aPath.close()
UIColor.green.setStroke()
aPath.stroke()
UIColor.clear.setFill()
aPath.fill()
}
}
Edited, here is the screenshot from other app
so I will click on view under my top view, this one will be in front and will be clickable, so I cant add smaller subView
The most correct way here is to override func point(inside point: CGPoint, with event: UIEvent?) -> Bool for UIView subclass.
Here is example:
class CustomView: UIView {
private let shapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
setupShapeLayer()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupShapeLayer() {
//Draw your own shape here
shapeLayer.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
let innerPath = UIBezierPath(arcCenter: center, radius: frame.width / 3, startAngle: CGFloat(0), endAngle: CGFloat.pi * 2, clockwise: true)
let outerPath = UIBezierPath(arcCenter: center, radius: frame.width / 4, startAngle: CGFloat(0), endAngle: CGFloat.pi * 2, clockwise: true)
//Subtract inner path
innerPath.append(outerPath.reversing())
shapeLayer.path = innerPath.cgPath
shapeLayer.lineWidth = 10
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.fillColor = UIColor.yellow.cgColor
layer.addSublayer(shapeLayer)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return shapeLayer.path?.contains(point) ?? false
}
}
Now you can add UITapGestureRecognizer to check how it works:
class ViewController: UIViewController {
#IBOutlet weak var resultLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let customView = CustomView.init(frame: view.frame)
view.addSubview(customView)
let recognizer = UITapGestureRecognizer(target: self, action: #selector(tapAction))
view.addGestureRecognizer(recognizer)
}
#objc func tapAction(_ sender: UITapGestureRecognizer) {
let location = sender.location(in: sender.view)
let subview = view?.hitTest(location, with: nil)
if subview is CustomView {
resultLabel.text = "CustomView"
}
else {
resultLabel.text = "Other"
}
}
}
Result of tapAction:
If you need several views, there are two possible options:
Store CAShapeLayer inside one CustomView and iterate through it inside point function
Add CustomView for each instance and iterate it through inside UITapGestureRecognizer tap action
I want to expand and contract a circle using CABasicAnimation. I previously designed a clean and effective way to draw the animation using sin and the drawRect method of the UIView class, but unfortunately it seems you can't animate or force drawRect to fire in UIView.animate methods no matter what you do. Therefore, I've had to come up with a workaround using CABasicAnimation. The result kind of works, but the animation is ugly and doesn't look right. Here's an illustration of what I mean:
Warping occurs during the expansion and contraction, and there appears to be a dent or cavity on the right side when the circle is expanding. Here's the class responsible for animating it:
struct PathConfig {
var arcCenter: CGPoint
var radius: CGFloat
let startAngle: CGFloat = 0.0
let endAngle: CGFloat = CGFloat(2 * M_PI)
}
class RGSStandbyIndicatorView: UIView {
// MARK: - Variables & Constants
/// The drawing layer.
var shapeLayer: CAShapeLayer!
// MARK: - Class Methods
/// Animation function.
func animate(period: CFTimeInterval) {
let p: PathConfig = PathConfig(arcCenter: CGPoint(x: frame.width / 2, y: frame.height / 2) , radius: 17.5)
let b: UIBezierPath = UIBezierPath(arcCenter: p.arcCenter, radius: p.radius, startAngle: p.startAngle, endAngle: p.endAngle, clockwise: true)
let a: CABasicAnimation = CABasicAnimation(keyPath: "path")
a.fromValue = shapeLayer.path
a.toValue = b.cgPath
a.autoreverses = true
a.duration = period
a.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
a.repeatCount = HUGE
shapeLayer.add(a, forKey: "animateRadius")
}
/// Initializes and returns a CAShapeLayer with the given draw path and fill color.
private class func shapeLayerForPath(_ path: CGPath, with fillColor: CGColor) -> CAShapeLayer {
let shapeLayer: CAShapeLayer = CAShapeLayer()
shapeLayer.path = path
shapeLayer.fillColor = fillColor
return shapeLayer
}
/// Configures the UIView
private func setup() {
// Init config, path.
let cfg: PathConfig = PathConfig(arcCenter: CGPoint(x: frame.width / 2, y: frame.height / 2) , radius: 0.0)
let path: UIBezierPath = UIBezierPath(arcCenter: cfg.arcCenter, radius: cfg.radius, startAngle: cfg.startAngle, endAngle: cfg.endAngle, clockwise: true)
// Init CAShapeLayer
shapeLayer = RGSStandbyIndicatorView.shapeLayerForPath(path.cgPath, with: UIColor.white.cgColor)
layer.addSublayer(shapeLayer)
}
// MARK: Class Method Overrides
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
}
I've tried adjusting the start and end angles hoping it was some artifact of the path having no overlap, but it didn't change anything. I'm not sure how to fix this problem, and It's based off Chadwick Wood's answer to a similar question.
Use this UIView subclass
class RadioAnimationView: UIView {
var animatableLayer : CAShapeLayer?
override func awakeFromNib() {
super.awakeFromNib()
self.layer.cornerRadius = self.bounds.height/2
self.animatableLayer = CAShapeLayer()
self.animatableLayer?.fillColor = self.backgroundColor?.cgColor
self.animatableLayer?.path = UIBezierPath(roundedRect: self.bounds, cornerRadius: self.layer.cornerRadius).cgPath
self.animatableLayer?.frame = self.bounds
self.animatableLayer?.cornerRadius = self.bounds.height/2
self.animatableLayer?.masksToBounds = true
self.layer.addSublayer(self.animatableLayer!)
self.startAnimation()
}
func startAnimation()
{
let layerAnimation = CABasicAnimation(keyPath: "transform.scale")
layerAnimation.fromValue = 1
layerAnimation.toValue = 3
layerAnimation.isAdditive = false
layerAnimation.duration = CFTimeInterval(2)
layerAnimation.fillMode = kCAFillModeForwards
layerAnimation.isRemovedOnCompletion = true
layerAnimation.repeatCount = .infinity
layerAnimation.autoreverses = true
self.animatableLayer?.add(layerAnimation, forKey: "growingAnimation")
}
}
Hope this helps you
I made a custom class that creates a sort of timeline that is meant to be scrolled horizontally. Here's the code for my custom UIScrollView :
import UIKit
struct DataPoint {
var fillColor: UIColor = UIColor.grayColor()
init(color: UIColor) {
fillColor = color
}
}
#IBDesignable
class MyView: UIScrollView {
#IBInspectable var lineColor: UIColor = UIColor.grayColor()
#IBInspectable var lineHeight: CGFloat = 65
#IBInspectable var lineWidth: CGFloat = 15
#IBInspectable var lineGap: CGFloat = 25
#IBInspectable var lineCount: Int = 0
var dataPoints = [DataPoint(color: UIColor.greenColor()), DataPoint(color: UIColor.blueColor())]
override init(frame: CGRect) {
super.init(frame: frame)
setupValues()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupValues()
//fatalError("init(coder:) has not been implemented")
}
func setupValues() {
self.contentSize = CGSize(width: self.frame.width * 2, height: self.frame.height)
self.backgroundColor = UIColor.whiteColor()
self.lineCount = Int(self.frame.width / lineGap)
}
override internal func layoutSubviews() {
super.layoutSubviews()
self.setNeedsDisplay()
self.layoutIfNeeded()
}
override func drawRect(rect: CGRect) {
let ctx = UIGraphicsGetCurrentContext()
CGContextSaveGState(ctx)
for i in 0...lineCount {
let start = CGPoint(x: CGFloat(i) * lineGap, y: self.frame.height)
let end = CGPoint(x: CGFloat(i) * lineGap, y: self.frame.height - lineHeight)
drawLine(from: start, to: end, color: UIColor.grayColor())
if i % (lineCount / (dataPoints.count + 2)) == 0 && i != 0 && i != lineCount {
drawPoint(at: end, radius: 5, color: UIColor.orangeColor())
}
}
CGContextRestoreGState(ctx)
}
func drawLine(from start: CGPoint, to end: CGPoint, color: UIColor) {
let path = UIBezierPath()
path.moveToPoint(start)
path.addLineToPoint(end)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.CGPath
shapeLayer.strokeColor = color.CGColor
shapeLayer.lineWidth = 1
self.layer.addSublayer(shapeLayer)
}
func drawPoint(at center: CGPoint, radius: CGFloat, color: UIColor) {
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(0), endAngle: CGFloat(M_PI * 2), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.CGPath
shapeLayer.fillColor = color.CGColor
shapeLayer.strokeColor = UIColor.blackColor().CGColor
shapeLayer.lineWidth = 0.5
self.layer.addSublayer(shapeLayer)
}
}
On my device and on the simulators, this is extremely slow and laggy. What exactly am I doing wrong here ? And what steps can I take to achieve a solid 60fps while scrolling ?
Hello I found your problem, your problem is
override internal func layoutSubviews() {
super.layoutSubviews()
self.setNeedsDisplay()
self.layoutIfNeeded()
}
you are accidentally in a never-ending paint loop
replace this by this
override internal func layoutSubviews() {
super.layoutSubviews()
}
or remove at all
I hope this Helps you, for me works great
It looks to me that you are drawing the same thing every time. Everything is static... So why not draw it all once into a graphics context and make a UIImage out of it to add to the background of the scrollview (or foreground, depending on anything else you are doing)?