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

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?

Related

Strange problem with UIBezierPath animation

I want to draw the Moon and then animate Moon's shadow. But after launching this code I can see some glitches on animation line:
GIF:
Why is this happening?
Playground code here
Update 1:
Both paths created by this function but with different angles (0 and π/2*0.6):
func calculateMoonPath(for angle: CGFloat) -> UIBezierPath {
let center = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
let radius = view.bounds.height/2
let path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: -.pi/2,
endAngle: .pi/2,
clockwise: true)
path.addArc(withCenter: .init(x: center.x - radius * tan(angle), y: center.y),
radius: radius / CGFloat(cosf(Float(angle))),
startAngle: .pi/2 - angle,
endAngle: angle - .pi/2,
clockwise: false
)
path.close()
return path
}
In my experience, the code that generates arcs creates different numbers of cubic bezier curves under the covers as the arc angle changes.
That changes the number of control points in the two curves, and messes up the animation. (as David Rönnqvist says, animations are undefined if the starting and ending path have a different number of control points.)
From what I've read, a full circle requires 4 cubic bezier curves to complete.
It wouldn't be that hard to create a variant of the addArc method that always built the arc using 4 cubic bezier curves, regardless of the arc angle. That's what I would suggest.
You could probably break your arc into 4 pieces (Using 4 sequential calls to addArc(withCenter:...) with different start and end angles such that they combine to make your desired full arc. Each of those should be short enough arc lengths to be a single Bezier curve, so you should get the same number of control points for the beginning and ending combined curve.
If you rewrite your calculateMoonPath function like this:
func calculateMoonPath(for angle: CGFloat) -> UIBezierPath {
let center = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
let radius = view.bounds.height/2
let path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: -.pi/2,
endAngle: .pi/2,
clockwise: true)
let startAngle = .pi/2 - angle
let endAngle = angle - .pi/2
let delta = (endAngle - startAngle) / 4
for index in 0...3 {
let thisStart = startAngle + delta * CGFloat(index)
let thisEnd = startAngle + delta * CGFloat(index + 1)
path.addArc(withCenter: .init(x: center.x - radius * tan(angle), y: center.y),
radius: radius / CGFloat(cosf(Float(angle))),
startAngle: thisStart,
endAngle: thisEnd,
clockwise: false
)
}
path.close()
return path
}
That yields the following:
The way you do each of the two lines,
is simply, two control points!
One at each end.
That's all there is to it. Don't try using an arc.
Here ...
https://www.desmos.com/calculator/cahqdxeshd

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.

How to draw a single dot in iOS?

Is it possible to draw a single dot in iOS? I've gone this way which I think is not right:
let circlePath = UIBezierPath(arcCenter: CGPoint(x: 30 ,y: 50 ), radius: CGFloat(1), startAngle: CGFloat(0), endAngle:CGFloat(Double.pi * 2), clockwise: true)
Is it ok to draw shapes with loops?
why can't you take a one view and give a background color black and give Width and height same sizes and give cornerradius height/2

swift - error when drawing curve

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.

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