swift - error when drawing curve - ios

I am attempting to write a program that draws a curve. The shorter the curve, the closer to green it gets. Here is the code for drawing the circle:
func drawCircle(percent: CGFloat) {
// Set up all the data for the circle
let redAmt = 1 / 100 * percent
let greenAmt = 1 - redAmt
let circleColor = UIColor.init(red: redAmt, green: greenAmt, blue: 0, alpha: 1)
let lineWidth = CGFloat(12)
let centerX = self.frame.size.width / 2
let centerY = self.frame.size.height / 2
let radius = (self.frame.size.width - lineWidth) / 2
// Clear the area where the circle is going to be drawn
let clearCirclePath = UIBezierPath(arcCenter: CGPoint(x: centerX, y: centerY), radius: radius, startAngle: CGFloat(0), endAngle: CGFloat(M_PI * 2), clockwise: true)
clearColor.setStroke()
clearCirclePath.lineWidth = lineWidth
clearCirclePath.stroke()
// Draw the circle
let circlePath = UIBezierPath(arcCenter: CGPoint(x: centerX, y: centerY), radius: radius, startAngle: CGFloat(0), endAngle: CGFloat(M_PI * 2) * percent / 100, clockwise: true)
circleColor.setStroke()
circlePath.lineWidth = lineWidth
circlePath.stroke()
}
It works just fine when called by drawRect(rect: CGRect), but when I try to call in another file, it does not work. It is drawing the arc in a view inside a view controller. The error it gives me is this: ExerciseTracker[7270] <Error>: CGContextSetStrokeColorWithColor: invalid context 0x0. If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable.
Thanks a lot!
(I am using Xcode 7.3.1)

You must draw in a valid context. drawRect: provides such a valid context. Simply putting code outside of drawRect: doesn't work unless you explicitly create your own context such as a bitmap context for creating an image by drawing.
The code in your drawCircle method needs to be in the drawRect: method of a custom view class. Instead of passing percent as a parameter, set it as an instance variable. Then anytime you update the value of percent, you call setNeedsDisplay() to trigger the view to redraw itself.

Related

Swift: addLine ignored in between two addArc calls when creating a path

This code does what I expect. It draws an arc, then adds a line 50 points wide from the top of that arc:
path.move(to: .init(x: myX, y: myY))
path.addArc(withCenter: CGPoint(x: centerX, y: centerY), radius: radius1, startAngle: .pi, endAngle: (3 * .pi)/2, clockwise: true)
let currentPoint = path.currentPoint
path.addLine(to: CGPoint(x: currentPoint.x + 50, y: currentPoint.y))
This code ignores the addLine for adding a 50 points wide line and just starts the second arc right at the top of the first arc.
path.move(to: .init(x: myX, y: myY))
path.addArc(withCenter: CGPoint(x: centerX, y: centerY), radius: radius1, startAngle: .pi, endAngle: (3 * .pi)/2, clockwise: true)
let currentPoint = path.currentPoint
path.addLine(to: CGPoint(x: currentPoint.x + 50, y: currentPoint.y))
path.addArc(withCenter: CGPoint(x: centerX + 50, y: centerY), radius: radius1, startAngle: (3 * .pi)/2, endAngle: .pi, clockwise: false)
With this second bit of code, I get the exact same output if I comment out the addLine code. I get the exact same output if I change the addLine code to add 300 pixels points instead of 50. The addLine code is ignored and I get two arcs, without a line between where the first ends and the second begins.
Any suggestions? Thanks so much!
You said:
With this second bit of code, I get the exact same output if I comment out the addLine code.
Yep, when you add an arc to an existing path, it will automatically draw a line from the currentPoint to the start of this second arc. If you don’t want it to add the line in between, you need to do a move(to:) in your path to where the second arc will start if you don’t want the line in-between. Or create two paths, one for each arc, and stroke them separately.
I get the exact same output if I change the addLine code to add 300 pixels instead of 50.
That doesn’t quite make sense and I cannot reproduce that behavior. For example, this is what I get when I move the second arc 50pt (and I’ll animate the stroke so you can see what’s going on):
But this is what I get when I move the line 300pt (but keep the second arc only 50pt from the first):
Clearly, if you not only make the line 300pt long, but move the center of the second arc by 300pt as well, then it will be just like the first example (except further apart).
However, if I replace your addLine(to:) with move(to:), then you won’t get the line in-between them:
FWIW, in all of these examples, I didn’t know what you were using for myX and myY, so I used a point to the left of the first arc. Clearly, if you don’t want that extra line, move myX and myY to the start of the first arc (or just comment that out entirely).
i dont know whats wrong with your code but have what i have tried .. it might help you thats why posting it
#IBDesignable
class CustomBg:UIView {
private lazy var curvedLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.yellow.cgColor
shapeLayer.lineWidth = 4
shapeLayer.strokeColor = UIColor.green.cgColor
return shapeLayer
}()
//MARK:- inializer
override init(frame: CGRect) {
super.init(frame: frame)
layer.addSublayer(curvedLayer)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
layer.addSublayer(curvedLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
updatePath()
}
func updatePath() {
let centerX = 100
let centerY = 100
let radius1 = CGFloat( 50.0)
let path = UIBezierPath()
path.addArc(withCenter: CGPoint(x: centerX, y: centerY), radius: radius1, startAngle:0, endAngle: (2 * .pi), clockwise: true)
let currentPoint = path.currentPoint
path.addLine(to: CGPoint(x: currentPoint.x + 150, y: currentPoint.y))
path.addArc(withCenter: CGPoint(x: Int(path.currentPoint.x + radius1) , y: centerY), radius: radius1 , startAngle: .pi, endAngle: (3 * .pi ), clockwise: true)
curvedLayer.path = path.cgPath
}
}

How to centre a subview in UIView?

I am trying to create a timer. I have a circle using UIBezierPath which I will animate to show the time remaining. This is the code that draws the shape and adds it to the view:
func drawBgShape() {
bgShapeLayer.path = UIBezierPath(arcCenter: CGPoint(x: center.x , y: center.y), radius:
bounds.width/2, startAngle: -.pi/2, endAngle: 1.5*.pi, clockwise: true).cgPath
bgShapeLayer.strokeColor = UIColor.red.cgColor
bgShapeLayer.fillColor = UIColor.clear.cgColor
bgShapeLayer.lineWidth = 10
layer.addSublayer(bgShapeLayer)
}
However, when the code is run, it looks like this;
I have tried a number of ways to centre the progress bar but none of them seem to work. For example, using frame.height/2 doesn't have any effect.
How can I centre the progress bar?
Thanks.
EDIT:
bgShapeLayer is defined like this:
let bgShapeLayer = CAShapeLayer()
The issue is probably this phrase:
arcCenter: CGPoint(x: center.x , y: center.y)
A view center is where the view is located in its superview. That’s not what you want. You want the center of the view. Try this:
arcCenter: CGPoint(x: bounds.midX , y: bounds.midY)
However, it would be better if you gave your shape layer a frame, size, and position and did everything in terms of the shape layer (which is, after all, where the shape is being drawn). For example:
bgShapeLayer.frame = self.layer.bounds
self.layer.addSublayer(bgShapeLayer)
bgShapeLayer.path = UIBezierPath(
arcCenter: CGPoint(x: bgShapeLayer.bounds.midX, y: bgShapeLayer.bounds.midY),
radius: bgShapeLayer.bounds.width/2,
startAngle: -.pi/2, endAngle: 1.5*.pi, clockwise: true).cgPath
That way we do not confuse the view coordinates and the layer coordinates, as your code does. To some extent you can get away with this because it happens that in this situation there is a general equivalence of the view internal coordinates, its layer internal coordinates, and the bgShapeLayer internal coordinates, but such confusion is not a good habit to get into. You should say what you mean rather than relying on a contingency.

Masking a CAShapeLayer creates this weird artifact?

I want to create a smooth progress bar for a to-do list. I used a circle (CGMutablePath) that masks the gray area and there's an obvious arc-like artifact. Not only that but there's also an artifact on the right side of the bar.
Note: I tried to rasterize the layers to no avail.
What causes iOS to do this and how can I smooth this out or get rid of it?
private var circleMask: CAShapeLayer = CAShapeLayer()
override func layoutSubviews() {
super.layoutSubviews()
backgroundColor = GradientColor(.leftToRight, frame: self.bounds, colors: GradientFlatRed())
layer.cornerRadius = bounds.height / 2
plainProgressBar.layer.cornerRadius = layer.cornerRadius
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
plainProgressBar.layer.shouldRasterize = true
plainProgressBar.layer.rasterizationScale = UIScreen.main.scale
createMask()
}
private func createMask() {
let path = CGMutablePath()
path.addArc(center: CGPoint(x: bounds.origin.x+25/2, y: bounds.origin.y+25/2), radius: 25/2, startAngle: 0, endAngle: CGFloat(Double.pi*2), clockwise: false)
path.addRect(bounds)
circleMask.path = path
circleMask.backgroundColor = plainMeterColor.cgColor
circleMask.fillRule = kCAFillRuleEvenOdd
plainProgressBar.layer.mask = circleMask
}
The issue is clearly an error in composition of the antialiased edges.
Knee-jerk question: do you have an atypical blend mode set?
Easiest solution: don't use path.addArc and path.addRect to generate two completely distinct shapes. Just use one of the init(roundedRect:... methods and set a corner radius of half your height, stretching the total path beyond the available space to the left to create a hard edge.
Or, if that doesn't appear, construct the path manually as a move, addLine(to:, addArc of only half a circle, then a final addLine(to: and a close. E.g. (thoroughly untested, especially re: start and end angles and clockwise versus anticlockwise versus iOS's upside-down coordinate system)
private func createMask() {
let path = CGMutablePath()
path.move(to: CGPoint(x: 0, y:0))
path.addLine(to: CGPoint(x: bounds.origin.x+25/2, y: 0))
path.addArc(center: CGPoint(x: bounds.origin.x+25/2, y: bounds.origin.y+25/2), radius: 25/2, startAngle: CGFloat(Double.pi/2.0), endAngle: CGFloat(Double.pi*3.0/2.0), clockwise: false)
path.addLine(to: CGPoint(x: 0, y: 25))
path.close()
...

Allowing a subview's radius to be larger than its superview's?

I've got a rounded button that I'm adding a circular progress ring to using code from here:
http://notebookheavy.com/2014/07/30/ios-7-style-progress-meter-in-swift/
var progressCircle = CAShapeLayer();
let centerPoint = CGPoint (x: circle.bounds.width / 2, y: circle.bounds.width / 2);
let circleRadius : CGFloat = circle.bounds.width / 2 // this is what I need to fix * 1.25
var circlePath = UIBezierPath(arcCenter: centerPoint, radius: circleRadius, startAngle: CGFloat(-0.5 * M_PI), endAngle: CGFloat(1.5 * M_PI), clockwise: true );
circle.layer.addSublayer(progressCircle);
self.view.addSubview(circle)
The above code draws a circle around my button, but I'm wanting instead of the circle to hug the inside of the circumference, I want it to hug the outside. I figured I could just make the radius larger, but once I go beyond width / 2, the subview is no longer visible in the view at all.
How would I allow the radius of the progressCircle to be larger than the button's and appear to be wrapping around the button instead of creating a circle inside of the circumference?

Adding curves to the ends of a UIBezierPath(arcCentre)

I have the following code added in the drawRect function of a UIView creating the following attached green arc.
Is there a way I can make the edges at the ends of the curve to look like the red arc rounded edges in the smaller image attached.
[![//Draw the Interior
let center = CGPoint(x: bounds.width/2,
y: bounds.height/2)
//Calculate the radius based on the max dimension of the view.
let radius = max(bounds.width, bounds.height)
//Thickness of the Arc
let arcWidth: CGFloat = 76
//Start and End of Circle angle
let startAngle: CGFloat = 3 * π/4
let endAngle: CGFloat = π/4
let path = UIBezierPath(arcCenter: center, radius: radius/2 - arcWidth/2,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
path.lineWidth = arcWidth
counterColor.setStroke()
path.stroke()]
Set the lineCapStyle property of the bezier path to
.Round:
A line with a rounded end. Quartz draws the line to extend beyond the
endpoint of the path. The line ends with a semicircular arc with a
radius of 1/2 the line’s width, centered on the endpoint.
path.lineCapStyle = .Round

Resources