Border in circle UIView using Swift - ios

I'm trying to make an UIView with border but I only make it appear in UIView not only on the circle.
In the brown circle I want a black border, but it has to be in the circle not in the UIView.
I have a class called Ball where I draw the circle.
class Ball: UIView {
var desiredColour = UIColor.blueColor()
struct mine {
static var p = UIBezierPath(ovalInRect: CGRectMake(0,0,118,117))
}
override func drawRect(rect: CGRect) {
// Drawing code
desiredColour.setFill()
mine.p.fill()
}
func colour() {
var randColor: UIColor = Colors.randomColor()
Colors.ballColor = randColor
Colors.colorPosition = find(Colors.arrayColors, randColor)!
desiredColour = randColor
self.setNeedsDisplay()
}
}
I used the code:
override func drawRect(rect: CGRect) {
// Drawing code
desiredColour.setFill()
let desiredBorderColor = UIColor.blackColor()
desiredBorderColor.setStroke()
self.layer.borderWidth = 3.0
self.layer.cornerRadius = self.frame.size.width/2.0
mine.p.fill()
mine.p.stroke()
}
but i get a border with a little cuts:

Try calling this function by passing you view as a parameter
func drawBlackBorder(view: UIView) {
view.layer.borderColor = UIColor.blackColor
view.layer.borderWidth = 1.0
view.layer.cornerRadius = view.frame.size.width/2.0
view.backgroundColor = UIColor.brownColor
}

override func drawRect(rect: CGRect) {
// Drawing code
desiredColour.setFill()
let desiredBorderColor = UIColor.blackColor()
desiredBorderColor.setStroke()
mine.p.lineWidth = 2.0 //set to whatever you want
mine.p.fill()
mine.p.stroke()
}
But note that for border to work you need to have some space around you circle.
Do the following. Declare myBorderWidth (of type CGFloat) property and then change
static var p = UIBezierPath(ovalInRect: CGRectMake(0,0,118,117))
to
static var p = UIBezierPath(ovalInRect: CGRectMake(myBorderWidth/2,myBorderWidth/2,118-myBorderWidth/2,117-myBorderWidth/2))
You can also remove repetitions of myBorderWidth/2 by declaring it as a property.

No need to modify layer, just set up UIBezierPath with proper insets depending on border thickness and set it's width (tested on Swift 4.2):
var fillColor: UIColor //circle color
var strokeColor: UIColor //border color
var borderWidth: CGFloat //border width
override func draw(_ rect: CGRect) {
fillColor.setFill()
strokeColor.setStroke()
let circlePath = UIBezierPath(ovalIn: CGRect(x: borderWidth, y: borderWidth, width: rect.width - borderWidth*2, height: rect.height - borderWidth*2))
circlePath.lineWidth = borderWidth
circlePath.stroke()
circlePath.fill()
}

Related

Animation of drawing does not work in Swift

Screenshot
I have created a custom UIButton class where I'm filling its inside with a color but only partially.
The Goal:
The goal is to fill inside of the button with background color but only partially. For example you have a long button(height: 50.0, width: 300.0). I want to fill it with background color horizontally, let's say only for 50%. In this case only the half of my button will be filled with color, another half will stay transparent or white.
The entire class looks like this:
class CustomButton: UIButton {
var color: UIColor = .clear
var percentage: CGFloat = 0
func fill(withPercentage percentage: CGFloat, isCorrect correct: Bool, onRect rect: CGRect) {
self.color = correct ? .lightishGreen : .pastelRed
self.percentage = percentage
draw(rect)
}
override func draw(_ rect: CGRect) {
super.draw(rect)
let fillRect = CGRect(
origin: CGPoint(x: rect.origin.x, y: rect.origin.y),
size: CGSize(width: rect.size.width * percentage, height: rect.size.height)
)
color.set()
guard let context = UIGraphicsGetCurrentContext() else { return }
context.fill(fillRect)
}
}
and this class works perfectly. However, I want to animate this drawing process, so the button is getting filled with background color from 0.0 to 1.0 let's say.
I've tried to put context.fill(fillRect) inside of the UIView.animate(){} but it did not work. So I wondered, is it possible to animate drawing?
I just do not want to make button bg color clear and to put UIView behind it and simulate filling there. Wanted a clean way. So I will be happy, if someone can give me a hint or show me how to do it.
Thanks in advance!
It sounds to me like you want a custom animatable property. Let's call it percentage. Here's one way:
class MyView : UIView {
required init?(coder: NSCoder) {
super.init(coder: coder)
self.backgroundColor = .clear
}
override class var layerClass : AnyClass {
return MyLayer.self
}
override func draw(_ rect: CGRect) {}
}
class MyLayer : CALayer {
#NSManaged var percentage : CGFloat
override class func needsDisplay(forKey key: String) -> Bool {
if key == #keyPath(percentage) {
return true
}
return super.needsDisplay(forKey:key)
}
override func action(forKey key: String) -> CAAction? {
if key == #keyPath(percentage) {
let ba = CABasicAnimation(keyPath: key)
ba.fromValue = self.presentation()!.value(forKey:key)
return ba
}
return super.action(forKey:key)
}
override func draw(in con: CGContext) {
con.setFillColor(UIColor.green.cgColor)
let rect = self.bounds.insetBy(dx: 2, dy: 2)
let rounded = UIBezierPath.init(roundedRect: rect, cornerRadius: 20)
con.addPath(rounded.cgPath)
con.setStrokeColor(UIColor.lightGray.cgColor)
con.strokePath()
con.addPath(rounded.cgPath)
con.clip(using: .evenOdd)
let fillRect = CGRect(
origin: CGPoint(x: rect.origin.x, y: rect.origin.y),
size: CGSize(width: rect.size.width * percentage, height: rect.size.height)
)
con.fill(fillRect)
}
}
The result is that when you set this view's layer's percentage, the change is animated:

Swift callback function for draw()?

I have a customView where I overwrite drawRect:. I also have some cases, where I want to display a UILabel ontop of my customView:
if self.ticks.count < 50 {
self.label.frame = self.bounds
self.label.text = "This chart requires 50 or more ticks.\nKeep Ticking."
self.addSubview(label)
}
However if I call this code before the drawRect is called, then my label doesn't show up. Is there a callback method like viewDidDraw or something?
EDIT
Someone asked for it, so here is all the code that is now doing what I need it to do. I think the answer to the question is to override layoutSubviews.
#IBDesignable open class CustomView: UIView {
#objc open var ticks:[Tick] = []
let label = UILabel()
required public init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
required public override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
func setup(){
self.backgroundColor = UIColor.clear
self.label.frame = self.bounds
self.label.text = "This chart requires 50 or more ticks.\nKeep Ticking."
self.label.numberOfLines = 0
self.label.textAlignment = .center
self.addSubview(label)
}
override open func layoutSubviews() {
self.label.frame = self.bounds
}
#objc open func setSessions(sessions:[Session]){
self.ticks = []
for session in sessions.reversed(){
if self.ticks.count < 250{
self.ticks = self.ticks + (session.getShots() as! [Shot])
}else{
break
}
}
if self.ticks.count < 50 {
self.label.isHidden = false
}else{
self.label.isHidden = true
}
self.bringSubview(toFront: self.label)
}
override open func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
let width = rect.width - 10;
let height = rect.height - 10;
let center = CGPoint(x: width/2+5, y: height/2+5)
let big_r = min(width, height)/2
context.setLineWidth(1.5)
UIColor.white.setStroke()
//draw the rings
for i in stride(from: 1, to: 7, by: 1){
let r = big_r * CGFloat(i) / 6.0;
context.addArc(center:center, radius:r, startAngle: 0, endAngle: CGFloat(2*Double.pi), clockwise: false)
context.strokePath()
}
//draw the ticks
if self.ticks.count > 49 {
UIColor.red().setFill()
for (i, tick) in self.ticks.prefix(250).enumerated(){
let radius = min((100.0-CGFloat(shot.score))/30.0 * big_r, big_r);
let x = Utilities.calculateX(tick, radius)
let y = Utilities.calculateY(tick, radius)
let point = CGPoint(x: x+center.x,y: y+center.y)
context.addArc(center:point, radius:CGFloat(DOT_WIDTH/2), startAngle: 0, endAngle: CGFloat(2*Double.pi), clockwise: false)
context.fillPath()
}
}
}
}
You can make your custom view redraw by calling setNeedsDisplay() when needed. After that, you can add the label
You can set a delegation for the View class and call the delegate method at the end of the draw function. However, it seems that if you call that directly, the draw function will only end until the delegate function get processed. Which means that the code will be executed before the new drawing refresh on the screen. In that case, you may need to call it asynchronously.
// at end of draw function
DispatchQueue.main.async {
self.delegate?.drawEnd()
}

Custom view with CALayer wired effect when animating bounds change

I have implemented a custom view with adding CALayer as sublayer for UIView. When I animate the view with the following:UIView.animateWithDuration(2.0) { self.slider.bounds.size *= 2.0}, the scaling animation is kind of wrong. The CALayer start at the wrong position with scaled size and move to the final position instead of scaling with the view.
The CustomeView Code :
import UIKit
class GridMaskView: UIView {
private let cornerLayer: CAShapeLayer
private let borderLayer: CAShapeLayer
private let gridLayer: CAShapeLayer
private let gridSize: (horizontal: UInt, vertical: UInt) = (3, 3)
private let cornerThickness: CGFloat = 3.0
private let cornerLength: CGFloat = 20.0
private let borderThickness: CGFloat = 2.0
private let gridThickness: CGFloat = 1.0
private let lineColor: UIColor = UIColor(r: 120, g: 179, b: 193, a: 1)
var showGridLines: Bool = true {
didSet {
gridLayer.hidden = !showGridLines
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
cornerLayer = CAShapeLayer()
cornerLayer.fillColor = lineColor.CGColor
borderLayer = CAShapeLayer()
borderLayer.fillColor = UIColor.clearColor().CGColor
borderLayer.strokeColor = lineColor.CGColor
borderLayer.lineWidth = borderThickness
gridLayer = CAShapeLayer()
gridLayer.strokeColor = lineColor.CGColor
gridLayer.lineWidth = gridThickness
super.init(frame: frame)
layer.addSublayer(cornerLayer)
layer.addSublayer(borderLayer)
layer.addSublayer(gridLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
layoutLayers()
}
private func layoutLayers() {
drawCorner()
drawBorder()
drawGrid()
}
private func drawCorner() {
cornerLayer.frame = bounds.insetBy(dx: -cornerThickness, dy: -cornerThickness)
cornerLayer.path = cornerPath(forBounds: cornerLayer.bounds)
}
private func cornerPath(forBounds bounds: CGRect) -> CGPathRef {
let horizontalSize = CGSize(width: cornerLength, height: cornerThickness)
let verticalSize = CGSize(width: cornerThickness, height: cornerLength)
let corners: [(CGRectEdge, CGRectEdge)] = [(.MinXEdge, .MinYEdge), (.MinXEdge, .MaxYEdge), (.MaxXEdge, .MinYEdge), (.MaxXEdge, .MaxYEdge)]
var cornerRects = [CGRect]()
for corner in corners {
cornerRects.append(bounds.align(horizontalSize, corner: corner.0, corner.1))
cornerRects.append(bounds.align(verticalSize, corner: corner.0, corner.1))
}
let cornerPath = CGPathCreateMutable()
CGPathAddRects(cornerPath, nil, cornerRects, cornerRects.count)
return cornerPath
}
private func drawBorder() {
borderLayer.frame = bounds
borderLayer.path = borderPath(forBounds: borderLayer.bounds)
}
private func borderPath(forBounds bounds: CGRect) -> CGPathRef {
let borderPath = CGPathCreateMutable()
let borderCornerPoints = [bounds.topLeft, bounds.topRight, bounds.bottomRight, bounds.bottomLeft, bounds.topLeft]
CGPathAddLines(borderPath, nil, borderCornerPoints, borderCornerPoints.count)
return borderPath
}
private func drawGrid() {
gridLayer.frame = bounds
gridLayer.path = gridPath(forBounds: gridLayer.bounds)
}
private func gridPath(forBounds bounds: CGRect) -> CGPathRef {
let stepSize = bounds.size / (CGFloat(gridSize.horizontal), CGFloat(gridSize.vertical))
let gridPath = CGPathCreateMutable()
for i in (1...gridSize.vertical) {
let x = CGFloat(i) * stepSize.width
CGPathMoveToPoint(gridPath, nil, x, 0)
CGPathAddLineToPoint(gridPath, nil, x, bounds.size.height)
}
for i in (1...gridSize.horizontal) {
let y = CGFloat(i) * stepSize.height
CGPathMoveToPoint(gridPath, nil, 0, y)
CGPathAddLineToPoint(gridPath, nil, bounds.size.width, y)
}
return gridPath
}
override func intrinsicContentSize() -> CGSize {
return CGSize(width: cornerLength * 2, height: cornerLength * 2)
}
}
Anyone know how to fit this?
The problem is that when you do view animation you don't get any automatic animation of sublayers. You'd be better off using a subview of your original UIView, because view animation will animate that together with the original view, according to its autolayout constraints.

Recognizing touch events only inside the masked view

I am creating this structured through masks:
Each hexagon should be clickable. This is the code I used:
// To create one masked hexagun
let hex = UIImage(named: "hexum")
let mask = CALayer()
mask.contents = hexum!.CGImage
mask.frame = CGRectMake(0, 0, hex!.size.width, hex!.size.height)
let img = UIImageView(image: UIImage(named: "img"))
img.layer.mask = mask
img.layer.masksToBounds = true
// Gesture Recognizer
let singleTap = UITapGestureRecognizer(target: self, action: "tapDetected")
singleTap.numberOfTapsRequired = 1
img.addGestureRecognizer(singleTap)
img.userInteractionEnabled = true
func tapDetected() {
print("Clicked!")
}
The problem is that the click region is larger than the mask, which will cause the inconvenience of a region overlapping each other. Something like this:
The yellow border shows the clickable region (not actually visible)
I am a beginner, it may be a trivial problem, but can you help me solve? Thank you.
If you want to do this perfectly, use the UIGestureRecognizerDelegate method gestureRecognizer(gesture, shouldReceiveTouch: touch) -> Bool. You will need to map the given gesture recogniser to a particular hexagon and then do pixel precise hit-testing on the image for that hexagon. This latter part is achieved by rendering the mask image to a graphics context and finding the pixel at the point corresponding to the touch location.
However, this is likely overkill. You can simplify the problem by hit-testing each shape as a circle, not a hexagon. The circle shape roughly approximates the hexagon so it will work almost the same for a user and avoids messy pixel-level alpha equality. The inaccuracy of touch input will cover up the inaccurate regions.
Another option is to rework your views to be based on CAShapeLayer masks. CAShapeLayer includes a path property. Bezier paths in UIKit include their own rolled versions of path-contains-point methods so you can just use that for this purpose.
You can adopt the UIGestureRecognizerDelegate protocol, and implement the gestureRecognizer(_:shouldReceiveTouch:) method to further constrain whether or not a gesture should fire. The link suggested by #tnylee would be a good place to start in terms of figuring out how to do such hit testing.
#Benjamin Mayo gave great options to resolve the issue. I ended up choosing the simplest, yet, efficient one: hit-testing each shape as a circle.
I'm putting the code that can help someone else:
class hexum: UIImageView {
var maskFrame: CGRect?
convenience init(mask: String, inside: String) {
// Mask things:
let masked = CALayer()
let img = UIImage(named: mask)
masked.contents = img?.CGImage
masked.frame = CGRectMake(x, y, img!.size.width, img!.size.height)
self.init(image: UIImage(named: inside))
self.layer.mask = masked
self.layer.masksToBounds = true
maskFrame = masked.frame
}
// The touch event things
// Here, I got help from #Matt in (http://stackoverflow.com/a/21081518/3462518):
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
let p = UIBezierPath(ovalInRect: maskFrame!)
return p.containsPoint(point)
}
}
let firstOne = hexum(mask: "img1", inside: "img2")
let tap = UITapGestureRecognizer(target: self, action: "clicked")
tap.numberOfTapsRequired = 1
firstOne.userInteractionEnabled = true
firstOne.addGestureRecognizer(tap)
func clicked() {
...
}
Result:
Here is a Swift 3 HexagonImageView, tappable just within the hexagon:
First create a UIBezier path:
final class HexagonPath: UIBezierPath {
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var sideLength: CGFloat = 100 {
didSet {
redrawPath()
}
}
override init() {
super.init()
redrawPath()
}
private func redrawPath() {
removeAllPoints()
let yDrop = sideLength / 2
move(to: CGPoint(x: sideLength, y: 0))
addLine(to: CGPoint(x: sideLength * 2, y: yDrop))
addLine(to: CGPoint(x: sideLength * 2, y: yDrop + sideLength))
addLine(to: CGPoint(x: sideLength, y: (yDrop * 2) + sideLength))
addLine(to: CGPoint(x: 0, y: yDrop + sideLength))
addLine(to: CGPoint(x: 0, y: yDrop ))
//addLine(to: CGPoint(x: sideLength, y: 0))
close()
}
}
Then create a Hexagon UIImageView:
class HexagonImageView: UIImageView {
let hexagonPath = HexagonPath()
var sideLength: CGFloat = 100 {
didSet {
hexagonPath.sideLength = sideLength
initilize()
}
}
init() {
super.init(frame: CGRect())
initilize()
}
override init(frame: CGRect) {
super.init(frame: frame)
initilize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initilize()
}
private func initilize() {
self.frame.size.width = sideLength * 2
self.frame.size.height = sideLength * 2
contentMode = .scaleAspectFill
mask(withPath: hexagonPath)
}
// MAKE THE TAP-HIT POINT JUST THE INNER PATH
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return hexagonPath.contains(point)
}
}
Using this Extension:
extension UIView {
func mask(withRect rect: CGRect, inverse: Bool = false) {
let path = UIBezierPath(rect: rect)
let maskLayer = CAShapeLayer()
if inverse {
path.append(UIBezierPath(rect: self.bounds))
maskLayer.fillRule = kCAFillRuleEvenOdd
}
maskLayer.path = path.cgPath
self.layer.mask = maskLayer
}
func mask(withPath path: UIBezierPath, inverse: Bool = false) {
let path = path
let maskLayer = CAShapeLayer()
if inverse {
path.append(UIBezierPath(rect: self.bounds))
maskLayer.fillRule = kCAFillRuleEvenOdd
}
maskLayer.path = path.cgPath
self.layer.mask = maskLayer
}
}
Finally, you can use it like this in ViewController ViewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
let hexImageView = HexagonImageView()
hexImageView.image = UIImage(named: "hotcube")
hexImageView.sideLength = 100
let tap = UITapGestureRecognizer(target: self, action: #selector(imageTapped))
tap.numberOfTapsRequired = 1
hexImageView.isUserInteractionEnabled = true
hexImageView.addGestureRecognizer(tap)
view.addSubview(hexImageView)
}
func imageTapped() {
print("tapped")
}
I'm not sure it's the simplest and the rightest way but I'd
check the location of user's tap and override touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?)
.

Changing bounds of a child view centers it in its parent

I have been following a tutorial explaining a custom ios control. There is no problem with the tutorial but I got really confused about the frame/bounds clipping (not part of the tutorial).
I have a UIView instance in the scene in the storyboard. This UIView is sized at 120 x 120. I am adding a custom control (extending UIControl) to this container view with addSubview. I began to experiment with setting different widths and height to the frame and bounds of the custom control, this is the initializer of the control:
public override init(frame: CGRect) {
super.init(frame: frame)
createSublayers()
}
...and produces this result (red is the parent, blue circle is the child):
Now I change the init to this:
public override init(frame: CGRect) {
super.init(frame: frame)
createSublayers()
self.frame.size.width = 40
self.frame.size.height = 40
println("frame: \(self.frame)")
println("bounds: \(self.bounds)")
self.clipsToBounds = true
}
And that produces this result:
and prints:
frame: (0.0,0.0,40.0,40.0)
bounds: (0.0,0.0,40.0,40.0)
But when I change the initliazer to this:
public override init(frame: CGRect) {
super.init(frame: frame)
createSublayers()
self.bounds.size.width = 40
self.bounds.size.height = 40
println("frame: \(self.frame)")
println("bounds: \(self.bounds)")
self.clipsToBounds = true
}
I get this:
and prints: frame: (40.0,40.0,40.0,40.0)
bounds: (0.0,0.0,40.0,40.0)
I cannot seem to comprehend why this view is centered in its parent view when I change its bounds. What exactly is causing it? Is the 'frame' clipped always towards its center? Or is this view centered in its parent after the bounds have been modified and that causes the update of the 'frame'? Is it some property that can be changed? How could I manage to put it to top-left corner, for example (exactly in the same way as when I modify the 'frame')? Thanks a lot!
EDIT:
class ViewController: UIViewController {
#IBOutlet var knobPlaceholder: UIView!
#IBOutlet var valueLabel: UILabel!
#IBOutlet var valueSlider: UISlider!
#IBOutlet var animateSwitch: UISwitch!
var knob: Knob!
override func viewDidLoad() {
super.viewDidLoad()
knob = Knob(frame: knobPlaceholder.bounds)
knobPlaceholder.addSubview(knob)
knobPlaceholder.backgroundColor = UIColor.redColor();
}
#IBAction func sliderValueChanged(slider: UISlider) {
}
#IBAction func randomButtonTouched(button: UIButton) {
}
}
And the knob:
public class Knob: UIControl {
private let knobRenderer = KnobRenderer()
private var backingValue: Float = 0.0
/** Contains the receiver’s current value. */
public var value: Float {
get {
return backingValue
}
set {
setValue(newValue, animated: false)
}
}
/** Sets the receiver’s current value, allowing you to animate the change visually. */
public func setValue(value: Float, animated: Bool) {
if value != backingValue {
backingValue = min(maximumValue, max(minimumValue, value))
}
}
/** Contains the minimum value of the receiver. */
public var minimumValue: Float = 0.0
/** Contains the maximum value of the receiver. */
public var maximumValue: Float = 1.0
public override init(frame: CGRect) {
super.init(frame: frame)
createSublayers()
self.bounds.size.width = 40
self.bounds.size.height = 40
println("frame: \(self.frame)")
println("bounds: \(self.bounds)")
self.clipsToBounds = true
}
public required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func createSublayers() {
knobRenderer.update(bounds)
knobRenderer.strokeColor = tintColor
knobRenderer.startAngle = -CGFloat(M_PI * 11.0 / 8.0);
knobRenderer.endAngle = CGFloat(M_PI * 3.0 / 8.0);
knobRenderer.pointerAngle = knobRenderer.startAngle;
knobRenderer.lineWidth = 2.0
knobRenderer.pointerLength = 6.0
layer.addSublayer(knobRenderer.trackLayer)
layer.addSublayer(knobRenderer.pointerLayer)
}
}
private class KnobRenderer {
let trackLayer = CAShapeLayer()
let pointerLayer = CAShapeLayer()
var strokeColor: UIColor {
get {
return UIColor(CGColor: trackLayer.strokeColor)
}
set(strokeColor) {
trackLayer.strokeColor = strokeColor.CGColor
pointerLayer.strokeColor = strokeColor.CGColor
}
}
var lineWidth: CGFloat = 1.0 {
didSet {
update();
}
}
var startAngle: CGFloat = 0.0 {
didSet {
update();
}
}
var endAngle: CGFloat = 0.0 {
didSet {
update()
}
}
var backingPointerAngle: CGFloat = 0.0
var pointerAngle: CGFloat {
get { return backingPointerAngle }
set { setPointerAngle(newValue, animated: false) }
}
func setPointerAngle(pointerAngle: CGFloat, animated: Bool) {
backingPointerAngle = pointerAngle
}
var pointerLength: CGFloat = 0.0 {
didSet {
update()
}
}
init() {
trackLayer.fillColor = UIColor.clearColor().CGColor
pointerLayer.fillColor = UIColor.clearColor().CGColor
}
func updateTrackLayerPath() {
let arcCenter = CGPoint(x: trackLayer.bounds.width / 2.0, y: trackLayer.bounds.height / 2.0)
let offset = max(pointerLength, trackLayer.lineWidth / 2.0)
let radius = min(trackLayer.bounds.height, trackLayer.bounds.width) / 2.0 - offset;
trackLayer.path = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true).CGPath
}
func updatePointerLayerPath() {
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: pointerLayer.bounds.width - pointerLength - pointerLayer.lineWidth / 2.0, y: pointerLayer.bounds.height / 2.0))
path.addLineToPoint(CGPoint(x: pointerLayer.bounds.width, y: pointerLayer.bounds.height / 2.0))
pointerLayer.path = path.CGPath
}
func update(bounds: CGRect) {
let position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
trackLayer.bounds = bounds
trackLayer.position = position
pointerLayer.bounds = bounds
pointerLayer.position = position
update()
}
func update() {
trackLayer.lineWidth = lineWidth
pointerLayer.lineWidth = lineWidth
updateTrackLayerPath()
updatePointerLayerPath()
}
}
In your update function you are centering the view.
func update(bounds: CGRect) {
**let position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)**
trackLayer.bounds = bounds
trackLayer.position = position
pointerLayer.bounds = bounds
pointerLayer.position = position
update()
}
If you take that out, then you're view won't be centered anymore. The reason setting the frame would leave it in the upper left hand corner while setting the bounds resulted in it being in the center is because setting the bounds x/y does not override the frame x/y. When you set the frame then later on your code only sets the bounds, so the frame x/y is never overwritten so the view stays in the upper left hand corner. However, in the second there is no x/y set for the frame, so I guess it's taken what you set for the bounds, so it get's centered.
I would recommend not setting the bounds x/y for the view as that should always be 0, 0. If you want to reposition it then use the frame. Remember the frame is relative the parent while the bounds is relative to it's self.

Resources