draw(_ rect: CGRect) not getting called in custom view - ios

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

Related

CAShapeLayer path not displayed

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

path.stroke() is crashing

I am writing a Swift 4.2 iOS app.
I am drawing a path using below code. It works perfectly when VC gets pushed but if I navigate back to this VC (pop in navigationController) path.stroke() is crashing.
override public func draw(_ rect: CGRect) {
drawGauge()
}
public override init(frame: CGRect) {
super.init(frame: frame)
drawGauge()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
drawGauge()
}
override public func layoutSubviews() {
super.layoutSubviews()
drawGauge()
}
func drawGauge() {
layer.sublayers = []
drawSmartArc()
drawNeedle()
drawNeedleCircle()
}
func drawSmartArc() {
var angles = getAllAngles()
let arcColors = colorCodes.components(separatedBy: ",")
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
var arcs = [ArcModel(startAngle: angles[0],
endAngle: angles.last!,
strokeColor: _shadowColor,
arcCap: CGLineCap.round,
center:CGPoint(x: bounds.width / 2, y: (bounds.height / 2)+5))]
for index in 0..<arcColors.count {
let arc = ArcModel(startAngle: angles[index], endAngle: angles[index+1],
strokeColor: UIColor(hex: arcColors[index]),
arcCap: CGLineCap.butt,
center: center)
arcs.append(arc)
}
arcs.rearrange(from: arcs.count-1, to: 2)
arcs[1].arcCap = self.capStyle
arcs[2].arcCap = self.capStyle
for i in 0..<arcs.count {
createArcWith(startAngle: arcs[i].startAngle, endAngle: arcs[i].endAngle, arcCap: arcs[i].arcCap, strokeColor: arcs[i].strokeColor, center: arcs[i].center)
}
if blinkAnimate {
blink()
}
}
func createArcWith(startAngle: CGFloat, endAngle: CGFloat, arcCap: CGLineCap, strokeColor: UIColor, center:CGPoint) {
// 1
let center = center
let radius: CGFloat = max(bounds.width, bounds.height)/2 - self.frame.width/10
let lineWidth: CGFloat = self.frame.width/5
// 2
let path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
// 3
path.lineWidth = lineWidth
path.lineCapStyle = arcCap
strokeColor.setStroke()
do {
try path.stroke() //CRASHING WHEN NAVIGATING BACK TO THIS VC
} catch let error {
Log.b(error.localizedDescription)
}
}
Thread 1: EXC_BAD_ACCESS (code=1, address=0x6b00000038)
I tried using try but it shows:
No calls to throwing functions occur within 'try' expression.
'catch' block is unreachable because no errors are thrown in 'do' block
You can't just draw a path at some point in your app's execution. The drawing context needs to be set to a particular context. Usually you do drawing commands in a view's draw(_:) method, where the context is set for you. Failing that, you would need to set the graphics context yourself before drawing (Which is very unusual.)
How/where is your createArcWith() function called?

CAShapeLayer doesn't show up

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

create an Arc in swift for Ui collection view

Hi i like to create the following view in ios swift for adding in UIcollection view , how can i achieve this ?
my current code to draw which is returning circle view
#IBInspectable public var fillColor: UIColor = #colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1) { didSet { setNeedsLayout() } }
#IBInspectable public var strokeColor: UIColor = #colorLiteral(red: 0.9254902005, green: 0.2352941185, blue: 0.1019607857, alpha: 1) { didSet { setNeedsLayout() } }
#IBInspectable public var lineWidth: CGFloat = 0 { didSet { setNeedsLayout() } }
lazy private var shapeLayer: CAShapeLayer = {
let _shapeLayer = CAShapeLayer()
self.layer.insertSublayer(_shapeLayer, at: 0)
return _shapeLayer
}()
override func layoutSubviews() {
super.layoutSubviews()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = (min(bounds.size.width, bounds.size.height) - lineWidth) / 2
shapeLayer.path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: 90, clockwise: true).cgPath
shapeLayer.fillColor = fillColor.cgColor
shapeLayer.strokeColor = strokeColor.cgColor
shapeLayer.lineWidth = lineWidth
}
You can draw the arc in view using BezierPath. Add this arcView in your collection view cell.
Let's see the ArcView :
class ArcView : UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
super.draw(rect)
createArc(rect: rect)
}
private func createArc(rect : CGRect) {
let center = CGPoint(x: rect.width/2, y: rect.height/2)
let lineWidth : CGFloat = 50.0
let radius = rect.width / 2 - lineWidth
let startingAngle = CGFloat(-10.0/180) * CGFloat.pi
let endingAngle = CGFloat(-80/180.0) * CGFloat.pi
let bezierPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: startingAngle , endAngle: endingAngle, clockwise: false)
bezierPath.lineWidth = lineWidth
UIColor(red: 249/255.0, green: 179/255.0, blue: 127/255.0, alpha: 1.0).setStroke()
bezierPath.stroke()
}
}
The center of arc is the middle of view.
Drawing the arc from -10 degree to -80 degree in anti-clockwise direction.
Adding the ArcView in ViewController's view Hierarchy (You will add this view in your collectionView Cell)
import UIKit
class ViewController: UIViewController {
private var arcView : ArcView!
override func viewDidLoad() {
super.viewDidLoad()
arcView = ArcView(frame: view.frame)
view.addSubview(arcView)
arcView.setNeedsDisplay()
}
}
Output :
The sample code is here
Try this and set angle as you want
let circlePath = UIBezierPath(arcCenter: CGPoint(x: 100,y: 100), radius: CGFloat(20), startAngle: CGFloat(0), endAngle:CGFloat(Double.pi / 2), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
//change the fill color
shapeLayer.fillColor = UIColor.clear.cgColor
//you can change the stroke color
shapeLayer.strokeColor = UIColor.red.cgColor
//you can change the line width
shapeLayer.lineWidth = 3.0
view.layer.addSublayer(shapeLayer)

My CoreGraphics code is working really slowly. How can I make this custom UIScrollView perform better?

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

Resources