Drawing class drawing straight lines instead of curved lines - ios

I have the below code that draws lines using UIBezierPath.
The code uses addCurveToPoint which should draw curved lines using a cubic bezier path, however the end result of the code is instead drawing connected straight lines but addLineToPoint isn't being used.
What could be going on, why isn't the code drawing curved lines?
import UIKit
class DrawingView: UIView, UITextFieldDelegate {
// Modifiable values within the code
let lineWidth : CGFloat = 2.0
let lineColor = UIColor.redColor()
let lineColorAlpha : CGFloat = 0.4
let shouldAllowUserChangeLineWidth = true
let maximumUndoRedoChances = 10
var path = UIBezierPath()
var previousImages : [UIImage] = [UIImage]()
// Represents current image index
var currentImageIndex = 0
// Control points for drawing curve smoothly
private var controlPoint1 : CGPoint?
private var controlPoint2 : CGPoint?
private var undoButton : UIButton!
private var redoButton : UIButton!
private var textField : UITextField!
//MARK: Init methods
override init(frame: CGRect) {
super.init(frame: frame)
setDefaultValues()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setDefaultValues()
}
// Draw the path when needed
override func drawRect(rect: CGRect) {
if currentImageIndex > 0 {
previousImages[currentImageIndex - 1].drawInRect(rect)
}
lineColor.setStroke()
path.strokeWithBlendMode(CGBlendMode.Normal, alpha: lineColorAlpha)
}
override func layoutSubviews() {
super.layoutSubviews()
redoButton.frame = CGRectMake(bounds.size.width - 58, 30, 50, 44)
if shouldAllowUserChangeLineWidth {
textField.center = CGPointMake(center.x, 52)
}
}
func setDefaultValues() {
multipleTouchEnabled = false
backgroundColor = UIColor.whiteColor()
path.lineWidth = lineWidth
addButtonsAndField()
}
func addButtonsAndField() {
undoButton = UIButton(frame: CGRectMake(8, 30, 50, 44))
undoButton.setTitle("Undo", forState: UIControlState.Normal)
undoButton.setTitleColor(UIColor.blackColor(), forState: UIControlState.Normal)
undoButton.backgroundColor = UIColor.lightGrayColor()
undoButton.addTarget(self, action: "undoButtonTapped:", forControlEvents: UIControlEvents.TouchUpInside)
addSubview(undoButton)
redoButton = UIButton(frame: CGRectMake(bounds.size.width - 58, 30, 50, 44))
redoButton.setTitle("Redo", forState: UIControlState.Normal)
redoButton.setTitleColor(UIColor.blackColor(), forState: UIControlState.Normal)
redoButton.backgroundColor = UIColor.lightGrayColor()
redoButton.addTarget(self, action: "redoButtonTapped:", forControlEvents: UIControlEvents.TouchUpInside)
addSubview(redoButton)
if shouldAllowUserChangeLineWidth {
textField = UITextField(frame: CGRectMake(0, 0, 50, 40))
textField.backgroundColor = UIColor.lightGrayColor()
textField.center = CGPointMake(center.x, 52)
textField.keyboardType = UIKeyboardType.NumberPad
textField.delegate = self
addSubview(textField)
}
}
//MARK: Touches methods
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
// Find the start point and move the path there
endEditing(true)
let touchPoint = touches.first?.locationInView(self)
path.moveToPoint(touchPoint!)
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touchPoint = touches.first?.locationInView(self)
controlPoint1 = CGPointMake((path.currentPoint.x + touchPoint!.x) / 2, (path.currentPoint.y + touchPoint!.y) / 2)
controlPoint2 = CGPointMake((path.currentPoint.x + touchPoint!.x) / 2, (path.currentPoint.y + touchPoint!.y) / 2)
path.addCurveToPoint(touchPoint!, controlPoint1: controlPoint1!, controlPoint2: controlPoint2!)
setNeedsDisplay()
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touchPoint = touches.first?.locationInView(self)
controlPoint1 = CGPointMake((path.currentPoint.x + touchPoint!.x) / 2, (path.currentPoint.y + touchPoint!.y) / 2)
controlPoint2 = CGPointMake((path.currentPoint.x + touchPoint!.x) / 2, (path.currentPoint.y + touchPoint!.y) / 2)
path.addCurveToPoint(touchPoint!, controlPoint1: controlPoint1!, controlPoint2: controlPoint2!)
savePreviousImage()
setNeedsDisplay()
// Remove all points to optimize the drawing speed
path.removeAllPoints()
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
touchesEnded(touches!, withEvent: event)
}
//MARK: Selector methods
func undoButtonTapped(sender : UIButton) {
if currentImageIndex > 0 {
setNeedsDisplay()
currentImageIndex--
}
}
func redoButtonTapped(sender : UIButton) {
if currentImageIndex != previousImages.count {
setNeedsDisplay()
currentImageIndex++
}
}
//MARK: UITextFieldDelegate
func textFieldDidEndEditing(textField: UITextField) {
if let n = NSNumberFormatter().numberFromString(textField.text!) {
if n.integerValue > 0 {
path.lineWidth = CGFloat(n)
}
}
}
//MARK: Saving images for reloading when undo or redo called
private func savePreviousImage() {
UIGraphicsBeginImageContextWithOptions(bounds.size, true, UIScreen.mainScreen().scale)
lineColor.setStroke()
// Create a image with white color
let rectPath = UIBezierPath(rect: bounds)
backgroundColor?.setFill()
rectPath.fill()
if currentImageIndex > 0 {
previousImages[currentImageIndex - 1].drawInRect(bounds)
}
path.strokeWithBlendMode(CGBlendMode.Normal, alpha: lineColorAlpha)
if previousImages.count >= currentImageIndex {
previousImages.removeRange(currentImageIndex..<previousImages.count)
}
if previousImages.count >= maximumUndoRedoChances {
previousImages.removeFirst()
}
else {
currentImageIndex++
}
previousImages.append(UIGraphicsGetImageFromCurrentImageContext())
UIGraphicsEndImageContext()
}
}

There are a few issues:
You are using control points that are midpoints between the two points, resulting in line segments. You probably want to choose control points that smooth the curve. See http://spin.atomicobject.com/2014/05/28/ios-interpolating-points/.
Here is a Swift 3 implementation of a simple smoothing algorithm, as well as Swift renditions of the above Hermite and Catmull-Rom Spline approaches:
extension UIBezierPath {
/// Simple smoothing algorithm
///
/// This iterates through the points in the array, drawing cubic bezier
/// from the first to the fourth points, using the second and third as
/// control points.
///
/// This takes every third point and moves it so that it is exactly inbetween
/// the points before and after it, which ensures that there is no discontinuity
/// in the first derivative as you join these cubic beziers together.
///
/// Note, if, at the end, there are not enough points for a cubic bezier, it
/// will perform a quadratic bezier, or if not enough points for that, a line.
///
/// - parameter points: The array of `CGPoint`.
convenience init?(simpleSmooth points: [CGPoint]) {
guard points.count > 1 else { return nil }
self.init()
move(to: points[0])
var index = 0
while index < (points.count - 1) {
switch (points.count - index) {
case 2:
index += 1
addLine(to: points[index])
case 3:
index += 2
addQuadCurve(to: points[index], controlPoint: points[index-1])
case 4:
index += 3
addCurve(to: points[index], controlPoint1: points[index-2], controlPoint2: points[index-1])
default:
index += 3
let point = CGPoint(x: (points[index-1].x + points[index+1].x) / 2,
y: (points[index-1].y + points[index+1].y) / 2)
addCurve(to: point, controlPoint1: points[index-2], controlPoint2: points[index-1])
}
}
}
/// Create smooth UIBezierPath using Hermite Spline
///
/// This requires at least two points.
///
/// Adapted from https://github.com/jnfisher/ios-curve-interpolation
/// See http://spin.atomicobject.com/2014/05/28/ios-interpolating-points/
///
/// - parameter hermiteInterpolatedPoints: The array of CGPoint values.
/// - parameter closed: Whether the path should be closed or not
///
/// - returns: An initialized `UIBezierPath`, or `nil` if an object could not be created for some reason (e.g. not enough points).
convenience init?(hermiteInterpolatedPoints points: [CGPoint], closed: Bool) {
self.init()
guard points.count > 1 else { return nil }
let numberOfCurves = closed ? points.count : points.count - 1
var previousPoint: CGPoint? = closed ? points.last : nil
var currentPoint: CGPoint = points[0]
var nextPoint: CGPoint? = points[1]
move(to: currentPoint)
for index in 0 ..< numberOfCurves {
let endPt = nextPoint!
var mx: CGFloat
var my: CGFloat
if previousPoint != nil {
mx = (nextPoint!.x - currentPoint.x) * 0.5 + (currentPoint.x - previousPoint!.x)*0.5
my = (nextPoint!.y - currentPoint.y) * 0.5 + (currentPoint.y - previousPoint!.y)*0.5
} else {
mx = (nextPoint!.x - currentPoint.x) * 0.5
my = (nextPoint!.y - currentPoint.y) * 0.5
}
let ctrlPt1 = CGPoint(x: currentPoint.x + mx / 3.0, y: currentPoint.y + my / 3.0)
previousPoint = currentPoint
currentPoint = nextPoint!
let nextIndex = index + 2
if closed {
nextPoint = points[nextIndex % points.count]
} else {
nextPoint = nextIndex < points.count ? points[nextIndex % points.count] : nil
}
if nextPoint != nil {
mx = (nextPoint!.x - currentPoint.x) * 0.5 + (currentPoint.x - previousPoint!.x) * 0.5
my = (nextPoint!.y - currentPoint.y) * 0.5 + (currentPoint.y - previousPoint!.y) * 0.5
}
else {
mx = (currentPoint.x - previousPoint!.x) * 0.5
my = (currentPoint.y - previousPoint!.y) * 0.5
}
let ctrlPt2 = CGPoint(x: currentPoint.x - mx / 3.0, y: currentPoint.y - my / 3.0)
addCurve(to: endPt, controlPoint1: ctrlPt1, controlPoint2: ctrlPt2)
}
if closed { close() }
}
/// Create smooth UIBezierPath using Catmull-Rom Splines
///
/// This requires at least four points.
///
/// Adapted from https://github.com/jnfisher/ios-curve-interpolation
/// See http://spin.atomicobject.com/2014/05/28/ios-interpolating-points/
///
/// - parameter catmullRomInterpolatedPoints: The array of CGPoint values.
/// - parameter closed: Whether the path should be closed or not
/// - parameter alpha: The alpha factor to be applied to Catmull-Rom spline.
///
/// - returns: An initialized `UIBezierPath`, or `nil` if an object could not be created for some reason (e.g. not enough points).
convenience init?(catmullRomInterpolatedPoints points: [CGPoint], closed: Bool, alpha: CGFloat) {
self.init()
guard points.count > 3 else { return nil }
assert(alpha >= 0 && alpha <= 1.0, "Alpha must be between 0 and 1")
let endIndex = closed ? points.count : points.count - 2
let startIndex = closed ? 0 : 1
let kEPSILON: CGFloat = 1.0e-5
move(to: points[startIndex])
for index in startIndex ..< endIndex {
let nextIndex = (index + 1) % points.count
let nextNextIndex = (nextIndex + 1) % points.count
let previousIndex = index < 1 ? points.count - 1 : index - 1
let point0 = points[previousIndex]
let point1 = points[index]
let point2 = points[nextIndex]
let point3 = points[nextNextIndex]
let d1 = hypot(CGFloat(point1.x - point0.x), CGFloat(point1.y - point0.y))
let d2 = hypot(CGFloat(point2.x - point1.x), CGFloat(point2.y - point1.y))
let d3 = hypot(CGFloat(point3.x - point2.x), CGFloat(point3.y - point2.y))
let d1a2 = pow(d1, alpha * 2)
let d1a = pow(d1, alpha)
let d2a2 = pow(d2, alpha * 2)
let d2a = pow(d2, alpha)
let d3a2 = pow(d3, alpha * 2)
let d3a = pow(d3, alpha)
var controlPoint1: CGPoint, controlPoint2: CGPoint
if abs(d1) < kEPSILON {
controlPoint1 = point2
} else {
controlPoint1 = (point2 * d1a2 - point0 * d2a2 + point1 * (2 * d1a2 + 3 * d1a * d2a + d2a2)) / (3 * d1a * (d1a + d2a))
}
if abs(d3) < kEPSILON {
controlPoint2 = point2
} else {
controlPoint2 = (point1 * d3a2 - point3 * d2a2 + point2 * (2 * d3a2 + 3 * d3a * d2a + d2a2)) / (3 * d3a * (d3a + d2a))
}
addCurve(to: point2, controlPoint1: controlPoint1, controlPoint2: controlPoint2)
}
if closed { close() }
}
}
// Some functions to make the Catmull-Rom splice code a little more readable.
// These multiply/divide a `CGPoint` by a scalar and add/subtract one `CGPoint`
// from another.
func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
return CGPoint(x: lhs.x * rhs, y: lhs.y * CGFloat(rhs))
}
func / (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
return CGPoint(x: lhs.x / rhs, y: lhs.y / CGFloat(rhs))
}
func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
}
Here are the "simple" smoothing algorithm, "Hermite" spline, and "Catmull Rom" spline curves in red, blue, and green, respectively. As you can see, the "simple" smoothing algorithm is computationally more simple, but generally doesn't pass through many of the points (but offers a more dramatic smoothing that eliminates any unsteadiness in the stroke). The points jumping around like this are exaggerating the behavior, whereas in a standard "gesture", it offers a pretty decent smoothing effect. The splines, on the other hand smooth the curve while passing through the points in the array.
If targeting iOS 9 and later, it introduces some nice features, notably:
Coalesced touches in case the user is using a device capable of such, notably the newer iPads. Bottom line, these devices (but not the simulators for them) are capable of generating more than 60 touches per second, and thus you can get multiple touches reported for each call to touchesMoved.
Predicted touches, where the device can show you where it anticipates the user's touches will progress (resulting in less latency in your drawing).
Pulling those together, you might do something like:
var points: [CGPoint]?
var path: UIBezierPath?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
points = [touch.location(in: view)]
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
if #available(iOS 9.0, *) {
if let coalescedTouches = event?.coalescedTouches(for: touch) {
points? += coalescedTouches.map { $0.location(in: view) }
} else {
points?.append(touch.location(in: view))
}
if let predictedTouches = event?.predictedTouches(for: touch) {
let predictedPoints = predictedTouches.map { $0.location(in: view) }
pathLayer.path = UIBezierPath(catmullRomInterpolatedPoints: points! + predictedPoints, closed: false, alpha: 0.5)?.cgPath
} else {
pathLayer.path = UIBezierPath(catmullRomInterpolatedPoints: points!, closed: false, alpha: 0.5)?.cgPath
}
} else {
points?.append(touch.location(in: view))
pathLayer.path = UIBezierPath(catmullRomInterpolatedPoints: points!, closed: false, alpha: 0.5)?.cgPath
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
path = UIBezierPath(catmullRomInterpolatedPoints: points!, closed: false, alpha: 0.5)
pathLayer.path = path?.cgPath
}
In this code snippet, I'm rendering the path by updating a CAShapeLayer, but if you want to render it some other way, feel free. For example, using your drawRect approach, you'd update path, and then call setNeedsDisplay().
And, the above illustrates the if #available(iOS 9, *) { ... } else { ... } syntax if you need to support iOS versions prior to 9.0, but obviously, if you are only supporting iOS 9 and later, you can remove that check and lose the else clause.
For more information, see WWDC 2015 video Advanced Touch Input on iOS.
Anyway, that yields something like:
(For Swift 2.3 rendition of the above, please see the previous version of this answer.)

Related

why when ever I try to tap on my SKSpriteNode it is not interactive

I am trying to have my SKSpriteNode named "slots" be tapped on when ever a user is trying to hit a target in the middle of the sprite. But whenever I Tapp on the slot in does not blink or do anything really.
I was trying to have it blink and print("Tapped on shotSlotNode") before I'll go on to continue creating this game but im still struck on this part of the game.
var slots = [shotSlot]()
var targets = [shotSlot]()
var gameScore: SKLabelNode!
var slotsRed = [targetSlotRed]()
var shotSlotNode: shotSlot? // Define a property for shotSlotNode
override func didMove(to view: SKView) {
let backGround = SKSpriteNode(imageNamed: "background")
backGround.position = CGPoint(x: 512, y: 384)
backGround.blendMode = .replace
backGround.zPosition = -1
// line 20 makes sure things go on top of the background view.
backGround.scale(to: CGSizeMake(1024, 768))
// I used line 21 to strech the image out to fix my scrrens size because I know I set my screen size to 1024 in the GameScene UI
addChild(backGround)
gameScore = SKLabelNode(fontNamed: "Chalkduster")
gameScore.text = "Score: 0"
gameScore.position = CGPoint(x: 480, y: 70)
addChild(gameScore)
self.view?.isMultipleTouchEnabled = true
for i in 0 ..< 3 { createSlot(at: CGPoint(x: 120 + (i * 370), y: 560)) }
for i in 0 ..< 3 { createSlot(at: CGPoint(x: 120 + (i * 370), y: 370)) }
for i in 0 ..< 3 { createSlot(at: CGPoint(x: 120 + (i * 370), y: 200)) }
// this code breaks my slots into 3 rows and 3 cloumms of the shotSlots.
for i in 0 ..< 1 { createGreenTarget(at: CGPoint(x: 120 + (i * 370), y: 560)) }
for i in 0 ..< 1 { createRedTarget(at: CGPoint(x: 860 + (i * 370), y: 200)) }
}
func slotTapped(_ slot: shotSlot) {
// Make the slot sprite blink
print("Tapped on slot")
let blinkOut = SKAction.fadeAlpha(to: 0.2, duration: 0.15)
let blinkIn = SKAction.fadeAlpha(to: 1, duration: 0.15)
let blink = SKAction.sequence([blinkOut, blinkIn])
let blinkForever = SKAction.repeatForever(blink)
slot.sprite.run(blinkForever)
print("Started blinking")
// Handle the slot tap logic
if slot === shotSlotNode {
print("Tapped on shot slot node")
}
}
// now we need to make this greenTarget slide.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
for slot in slots {
if slot.contains(location) {
// The touch is inside the slot node
print("Tapped on shot slot")
slotTapped(slot)
// You can also check if the tapped slot is your `shotSlotNode`
if slot === shotSlotNode {
print("Tapped on shot slot node")
}
}
}
}
}
func createSlot(at position: CGPoint) {
let slot = shotSlot()
slot.configure(at: position)
slot.zPosition = 1
slot.isUserInteractionEnabled = true
addChild(slot)
targets.append(slot) // Add the slot to the targets array
if position == CGPoint(x: 512, y: 100) {
slot.sprite.name = "shotSlotNode"
slot.isUserInteractionEnabled = true
self.shotSlotNode = slot
print("Set shotSlotNode to sprite with name \(slot.sprite.name)")
}
}
class shotSlot: SKNode {
let sprite = SKSpriteNode(imageNamed: "slots")
func configure(at position: CGPoint) {
self.position = position
sprite.name = "shotSlotNode"
// sprite.position = CGPoint(x: frame.midX, y: frame.midY)
sprite.scale(to: CGSizeMake(220, 140))
sprite.isUserInteractionEnabled = true
sprite.zPosition = 13
addChild(sprite)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
if let node = self.childNode(withName: "shotSlotNode") {
let convertedLocation = node.convert(location, from: self)
if node.contains(convertedLocation) {
print("Tapped on shotSlotNode")
}
}
}
}
}

Determining if custom iOS views overlap

I've defined a CircleView class:
class CircleView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.clear
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
// Get the Graphics Context
if let context = UIGraphicsGetCurrentContext() {
// Set the circle outerline-width
context.setLineWidth(5.0);
// Set the circle outerline-colour
UIColor.blue.set()
// Create Circle
let center = CGPoint(x: frame.size.width/2, y: frame.size.height/2)
let radius = (frame.size.width - 10)/2
context.addArc(center: center, radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
context.setFillColor(UIColor.blue.cgColor)
// Draw
context.strokePath()
context.fillPath()
}
}
}
And created an array of them with a randomly set number:
var numberOfCircles: Int!
var circles: [CircleView] = []
numberOfCircles = Int.random(in: 1..<10)
let circleWidth = CGFloat(50)
let circleHeight = circleWidth
var i = 0
while i < numberOfCircles {
let circleView = CircleView(frame: CGRect(x: 0.0, y: 0.0, width: circleWidth, height: circleHeight))
circles.append(circleView)
i += 1
}
After creating the circles, I call a function, drawCircles, that will draw them on the screen:
func drawCircles(){
for c in circles {
c.frame.origin = c.frame.randomPoint
while !UIScreen.main.bounds.contains(c.frame.origin) {
c.frame.origin = CGPoint()
c.frame.origin = c.frame.randomPoint
let prev = circles.before(c)
if prev?.frame.intersects(c.frame) == true {
c.frame.origin = c.frame.randomPoint
}
}
}
for c in circles {
self.view.addSubview(c)
}
}
The while loop in the drawCircles method makes sure that no circles are placed outside of the bounds of the screen, and works as expected.
What I'm struggling with is to make sure that the circles don't overlap each other, like so:
I'm using the following methods to determine either the next
I'm using this methods to determine what the previous / next element in the array of circles:
extension BidirectionalCollection where Iterator.Element: Equatable {
typealias Element = Self.Iterator.Element
func after(_ item: Element, loop: Bool = false) -> Element? {
if let itemIndex = self.firstIndex(of: item) {
let lastItem: Bool = (index(after:itemIndex) == endIndex)
if loop && lastItem {
return self.first
} else if lastItem {
return nil
} else {
return self[index(after:itemIndex)]
}
}
return nil
}
func before(_ item: Element, loop: Bool = false) -> Element? {
if let itemIndex = self.firstIndex(of: item) {
let firstItem: Bool = (itemIndex == startIndex)
if loop && firstItem {
return self.last
} else if firstItem {
return nil
} else {
return self[index(before:itemIndex)]
}
}
return nil
}
}
This if statement, however; doesn't seem to be doing what I'm wanting; which is to make sure that if a circle intersects with another one, to change it's origin to be something new:
if prev?.frame.intersects(c.frame) == true {
c.frame.origin = c.frame.randomPoint
}
If anyone has any ideas where the logic may be, or of other ideas on how to make sure that the circles don't overlap with each other, that would be helpful!
EDIT: I did try the suggestion that Eugene gave in his answer like so, but still get the same result:
func distance(_ a: CGPoint, _ b: CGPoint) -> CGFloat {
let xDist = a.x - b.x
let yDist = a.y - b.y
return CGFloat(sqrt(xDist * xDist + yDist * yDist))
}
if prev != nil {
if distance((prev?.frame.origin)!, c.frame.origin) <= 40 {
print("2")
c.frame.origin = CGPoint()
c.frame.origin = c.frame.randomPoint
}
}
But still the same result
EDIT 2
Modified my for loop based on Eugene's edited answer / clarifications; still having issues with overlapping circles:
for c in circles {
c.frame.origin = c.frame.randomPoint
let prev = circles.before(c)
let viewMidX = self.circlesView.bounds.midX
let viewMidY = self.circlesView.bounds.midY
let xPosition = self.circlesView.frame.midX - viewMidX + CGFloat(arc4random_uniform(UInt32(viewMidX*2)))
let yPosition = self.circlesView.frame.midY - viewMidY + CGFloat(arc4random_uniform(UInt32(viewMidY*2)))
if let prev = prev {
if distance(prev.center, c.center) <= 50 {
c.center = CGPoint(x: xPosition, y: yPosition)
}
}
}
That’s purely geometric challenge. Just ensure that distance between the circle centers greater than or equal to sum of their radiuses.
Edit 1
Use UIView.center instead of UIView.frame.origin. UIView.frame.origin gives you the top left corner of UIView.
if let prev = prev {
if distance(prev.center, c.center) <= 50 {
print("2")
c.center = ...
}
}
Edit 2
func distance(_ a: CGPoint, _ b: CGPoint) -> CGFloat {
let xDist = a.x - b.x
let yDist = a.y - b.y
return CGFloat(hypot(xDist, yDist))
}
let prev = circles.before(c)
if let prevCircleCenter = prev?.center {
let distance = distance(prevCenter, c.center)
if distance <= 50 {
let viewMidX = c.bounds.midX
let viewMidY = c.bounds.midY
var newCenter = c.center
var centersVector = CGVector(dx: newCenter.x - prevCircleCenter.x, dy: newCenter.y - prevCircleCenter.y)
centersVector.dx *= 51 / distance
centersVector.dy *= 51 / distance
newCenter.x = prevCircleCenter.x + centersVector.dx
newCenter.y = prevCircleCenter.y + centersVector.dy
c.center = newCenter
}
}

Artefact drawing in Swift

The code below draws lines by overriding touches, however there is an artefact that persists when drawing, seen in the images below.
When changing direction while zig zagging drawing across the screen, sometimes the line turns into a flat straight corner instead of remaining circular. The artefact is also experienced when drawing on the spot in small circles, the drawing point flashes half circles sometimes leaving half circles and partial circle residue when the finger leave the screen.
The artefacts are intermittent and not in an entirely consistent or predictable pattern making it difficult to find the issue in the code. It is present both in the simulator and on device in iOS7 - iOS9.
A zip containing two video screen captures of drawing dots and lines along with the Xcode project are uploaded to DropBox in a file called Archive.zip (23MB) https://www.dropbox.com/s/hm39rdiuk0mf578/Archive.zip?dl=0
Questions:
1 - In code, what is causing this dot/half circle artefact and how can it be corrected?
class SmoothCurvedLinesView: UIView {
var strokeColor = UIColor.blueColor()
var lineWidth: CGFloat = 20
var snapshotImage: UIImage?
private var path: UIBezierPath?
private var temporaryPath: UIBezierPath?
private var points = [CGPoint]()
private var totalPointCount = 0
override func drawRect(rect: CGRect) {
snapshotImage?.drawInRect(rect)
strokeColor.setStroke()
path?.stroke()
temporaryPath?.stroke()
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
points = [touch!.locationInView(self)]
totalPointCount = totalPointCount + 1
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
let point = touch!.locationInView(self)
points.append(point)
totalPointCount = totalPointCount + 1
updatePaths()
if totalPointCount > 50 {
constructIncrementalImage(includeTemporaryPath: false)
path = nil
totalPointCount = 0
}
setNeedsDisplay()
}
private func updatePaths() {
// update main path
while points.count > 4 {
points[3] = CGPointMake((points[2].x + points[4].x)/2.0, (points[2].y + points[4].y)/2.0)
if path == nil {
path = createPathStartingAtPoint(points[0])
}
path?.addCurveToPoint(points[3], controlPoint1: points[1], controlPoint2: points[2])
points.removeFirst(3)
}
// build temporary path up to last touch point
let pointCount = points.count
if pointCount == 2 {
temporaryPath = createPathStartingAtPoint(points[0])
temporaryPath?.addLineToPoint(points[1])
} else if pointCount == 3 {
temporaryPath = createPathStartingAtPoint(points[0])
temporaryPath?.addQuadCurveToPoint(points[2], controlPoint: points[1])
} else if pointCount == 4 {
temporaryPath = createPathStartingAtPoint(points[0])
temporaryPath?.addCurveToPoint(points[3], controlPoint1: points[1], controlPoint2: points[2])
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
constructIncrementalImage()
path = nil
setNeedsDisplay()
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
touchesEnded(touches!, withEvent: event)
}
private func createPathStartingAtPoint(point: CGPoint) -> UIBezierPath {
let localPath = UIBezierPath()
localPath.moveToPoint(point)
localPath.lineWidth = lineWidth
localPath.lineCapStyle = .Round
localPath.lineJoinStyle = .Round
return localPath
}
private func constructIncrementalImage(includeTemporaryPath includeTemporaryPath: Bool = true) {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
strokeColor.setStroke()
snapshotImage?.drawAtPoint(CGPointZero)
path?.stroke()
if (includeTemporaryPath) { temporaryPath?.stroke() }
snapshotImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
This would appear to be a fascinating bug in addQuadCurveToPoint and addCurveToPoint where, if the control point(s) are on the same line as the two end points, it doesn't honor the lineJoinStyle. So you can test for this (by looking at the atan2 of the various points and make sure there are not the same), and if so, just do addLineToPoint instead:
I found that this revised code removed those artifacts:
private func updatePaths() {
// update main path
while points.count > 4 {
points[3] = CGPointMake((points[2].x + points[4].x)/2.0, (points[2].y + points[4].y)/2.0)
if path == nil {
path = createPathStartingAtPoint(points[0])
}
addCubicCurveToPath(path)
points.removeFirst(3)
}
// build temporary path up to last touch point
let pointCount = points.count
if pointCount == 2 {
temporaryPath = createPathStartingAtPoint(points[0])
temporaryPath?.addLineToPoint(points[1])
} else if pointCount == 3 {
temporaryPath = createPathStartingAtPoint(points[0])
addQuadCurveToPath(temporaryPath)
} else if pointCount == 4 {
temporaryPath = createPathStartingAtPoint(points[0])
addCubicCurveToPath(temporaryPath)
}
}
/// Add cubic curve to path
///
/// Because of bug with bezier curves that fold back on themselves do no honor `lineJoinStyle`,
/// check to see if this occurs, and if so, just add lines rather than cubic bezier path.
private func addCubicCurveToPath(somePath: UIBezierPath?) {
let m01 = atan2(points[0].x - points[1].x, points[0].y - points[1].y)
let m23 = atan2(points[2].x - points[3].x, points[2].y - points[3].y)
let m03 = atan2(points[0].x - points[3].x, points[0].y - points[3].y)
if m01 == m03 || m23 == m03 || points[0] == points[3] {
somePath?.addLineToPoint(points[1])
somePath?.addLineToPoint(points[2])
somePath?.addLineToPoint(points[3])
} else {
somePath?.addCurveToPoint(points[3], controlPoint1: points[1], controlPoint2: points[2])
}
}
/// Add quadratic curve to path
///
/// Because of bug with bezier curves that fold back on themselves do no honor `lineJoinStyle`,
/// check to see if this occurs, and if so, just add lines rather than quadratic bezier path.
private func addQuadCurveToPath(somePath: UIBezierPath?) {
let m01 = atan2(points[0].x - points[1].x, points[0].y - points[1].y)
let m12 = atan2(points[1].x - points[2].x, points[1].y - points[2].y)
let m02 = atan2(points[0].x - points[2].x, points[0].y - points[2].y)
if m01 == m02 || m12 == m02 || points[0] == points[2] {
somePath?.addLineToPoint(points[1])
somePath?.addLineToPoint(points[2])
} else {
somePath?.addQuadCurveToPoint(points[2], controlPoint: points[1])
}
}
Also, this may be overly cautious, but it might be prudent to ensure that two successive points are never the same with a guard statements:
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
let point = touch!.locationInView(self)
guard point != points.last else { return }
points.append(point)
totalPointCount = totalPointCount + 1
updatePaths()
if totalPointCount > 50 {
constructIncrementalImage(includeTemporaryPath: false)
path = nil
totalPointCount = 0
}
setNeedsDisplay()
}
If you find other situations where there are problems, you can repeat the debugging exercise that I just did. Namely, run the code until a problem manifested itself, but stop immediately and look at the log of points array to see what points caused a problem, and then create a init?(coder:) that consistently reproduced the problem 100% of the time, e.g.:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
points.append(CGPoint(x: 239.33332824707, y: 419.0))
points.append(CGPoint(x: 239.33332824707, y: 420.0))
points.append(CGPoint(x: 239.33332824707, y: 419.3))
updatePaths()
}
Then, with a consistently reproducible problem, the debugging was easy. So having diagnosed the problem, I then revised updatePaths until the problem was resolved. I then commented out init? and repeated the whole exercise.

Swift ios rotate custom control from touch point

I have written an custom control in Swift for IOS. The custom control is a circle with 4 segments in it, all with different colors. The goal of the control is to serve as some sort of "select tool". You rotate the circle and whatever color is on top is the selected color. The structure is as follows: There is one CircleLayer and in that CircleLayer there are 4 SegmentLayers all with different colors (See screenshot for example).
Rotating the circle works but the thing is that it's always rotating from the same point. For example when I click the circle on the red segment the circle rotates so that the green segment is where I just clicked.
Any suggestions to how I could make the control start rotating from where I clicked.
Current code for wheel control:
import UIKit
func degreesToRadians (value:Double) -> Double {
return value * M_PI / 180.0
}
func radiansToDegrees (value:Double) -> Double {
return value * 180.0 / M_PI
}
func square (value:CGFloat) -> CGFloat {
return value * value
}
#IBDesignable public class CircleControl: UIControl, RotateStartDelegate {
private var backingValue: CGFloat = 0.0
/** Layer renderer **/
private let circleRenderer = CircleRenderer()
/** Contains the receiver’s current value. */
public var value: CGFloat {
get { return backingValue }
set { setValue(newValue, animated: false) }
}
public var angle: Double = 0.0
/** Set the angle of the circle */
public func setValue(value: CGFloat, animated: Bool) {
if(value != self.value) {
self.backingValue = value
// Update human-readable angle
var a = radiansToDegrees(Double(value))
self.angle = (a >= 0) ? a : a + 360.0
// Set angle
circleRenderer.setCircleAngle(value, animated: animated)
sendActionsForControlEvents(.ValueChanged)
}
}
/** Contains a Boolean value indicating whether changes in the sliders value generate continuous update events. */
public var continuous = true
public override init(frame: CGRect) {
super.init(frame: frame)
createSubLayers()
let gr = RotationGestureRecognizer(delegate: self, target: self, action: "handleRotation:")
self.addGestureRecognizer(gr)
let tr = TapGestureRecognizer(target: self, action: "handleTap:")
self.addGestureRecognizer(tr)
self.setValue(circleRenderer.segmentMiddle, animated: false)
}
public required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/** Set bounds of all the sub layers **/
func createSubLayers() {
circleRenderer.update(bounds)
layer.addSublayer(circleRenderer.circleLayer)
}
func rotationStart(startingPoint: CGPoint, angle: CGFloat) {
println("Rotation started")
println(startingPoint)
// circleRenderer.setDragStartingAngle(angle)
//
//// circleRenderer.circleLayer.anchorPoint = startingPoint
////
//// circleRenderer.backingPointerAngle = angle
//// circleRenderer.setCircleAngle(angle, animated: true)
}
/** Animate to the color usert tapped on **/
func handleTap(sender: AnyObject) {
let tr = sender as! TapGestureRecognizer
}
/** Update the angle **/
func handleRotation(sender: AnyObject) {
let gr = sender as! RotationGestureRecognizer
self.value = gr.rotation
// When the gesture has stopped make a small correction to the middle of the segment
if (gr.state == UIGestureRecognizerState.Ended) || (gr.state == UIGestureRecognizerState.Cancelled) {
var segmentSize:Double = 360 / 4
if self.angle >= 0.0 && self.angle < segmentSize {
self.setValue(circleRenderer.segmentMiddle, animated: true)
} else if self.angle >= segmentSize && self.angle < segmentSize * 2 {
self.setValue(circleRenderer.segmentMiddle * 3, animated: true)
} else if self.angle >= segmentSize * 2 && self.angle < segmentSize * 3 {
self.setValue(circleRenderer.segmentMiddle * -3, animated: true)
} else if self.angle >= segmentSize * 3 && self.angle < segmentSize * 4 {
self.setValue(circleRenderer.segmentMiddle * -1, animated: true)
}
}
}
}
private class CircleRenderer {
/** Angle properties **/
var backingPointerAngle: CGFloat = 0.0
var circleAngle: CGFloat {
get { return backingPointerAngle }
set { setCircleAngle(newValue, animated: false) }
}
/** Layers **/
let circleLayer = CALayer()
let seg1Layer = CAShapeLayer()
let seg2Layer = CAShapeLayer()
let seg3Layer = CAShapeLayer()
let seg4Layer = CAShapeLayer()
/** Segment size **/
let segmentSize:CGFloat = CGFloat( ( M_PI*2 ) / 4)
/** Middle of a segment. The angle to rotate to **/
let segmentMiddle:CGFloat = CGFloat(( M_PI*2 ) / 8)
/** Initialize the colors **/
init() {
seg1Layer.fillColor = UIColor.greenColor().CGColor!
seg2Layer.fillColor = UIColor(red: 204, green: 0, blue: 0, alpha: 100).CGColor!
seg3Layer.fillColor = UIColor(red: 255, green: 203, blue: 0, alpha: 100).CGColor!
seg4Layer.fillColor = UIColor.blueColor().CGColor!
circleLayer.opaque = true
circleLayer.backgroundColor = UIColor.clearColor().CGColor!
}
func setDragStartingAngle(angle: CGFloat) {
// Set orientation?
}
/** Change the angle of the circle, animated or not animated **/
func setCircleAngle(circleAngle: CGFloat, animated: Bool) {
CATransaction.begin()
CATransaction.setDisableActions(true)
circleLayer.transform = CATransform3DMakeRotation(circleAngle, 0.0, 0.0, 0.1)
if animated {
let midAngle = (max(circleAngle, self.circleAngle) - min(circleAngle, self.circleAngle) ) / 2.0 + min(circleAngle, self.circleAngle)
let animation = CAKeyframeAnimation(keyPath: "transform.rotation.z")
animation.duration = 0.25
animation.values = [self.circleAngle, midAngle, circleAngle]
animation.keyTimes = [0.0, 0.5, 1.0]
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
circleLayer.addAnimation(animation, forKey: "shake")
}
CATransaction.commit()
self.backingPointerAngle = circleAngle
}
/** Draw the segment layers paths **/
func update() {
let center = CGPoint(x: circleLayer.bounds.size.width / 2.0, y: circleLayer.bounds.size.height / 2.0)
let radius:CGFloat = min(circleLayer.bounds.size.width, circleLayer.bounds.size.height) / 2 * 1
let seg1Path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0.0, endAngle: segmentSize, clockwise: true)
seg1Path.addLineToPoint(center)
seg1Layer.path = seg1Path.CGPath
let seg2Path = UIBezierPath(arcCenter: center, radius: radius, startAngle: segmentSize, endAngle: segmentSize*2, clockwise: true)
seg2Path.addLineToPoint(center)
seg2Layer.path = seg2Path.CGPath
let seg3Path = UIBezierPath(arcCenter: center, radius: radius, startAngle: segmentSize*2, endAngle: segmentSize*3, clockwise: true)
seg3Path.addLineToPoint(center)
seg3Layer.path = seg3Path.CGPath
let seg4Path = UIBezierPath(arcCenter: center, radius: radius, startAngle: segmentSize*3, endAngle: segmentSize*4, clockwise: true)
seg4Path.addLineToPoint(center)
seg4Layer.path = seg4Path.CGPath
}
/** Update the frame and position of the layers **/
func update(bounds: CGRect) {
let position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
circleLayer.position = position
circleLayer.bounds = bounds
circleLayer.addSublayer(seg1Layer)
circleLayer.addSublayer(seg2Layer)
circleLayer.addSublayer(seg3Layer)
circleLayer.addSublayer(seg4Layer)
update()
}
}
import UIKit.UIGestureRecognizerSubclass
private class TapGestureRecognizer : UITapGestureRecognizer {
var rotation: CGFloat = 0.0
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
super.touchesBegan(touches, withEvent: event)
if let touch = touches[touches.startIndex] as? UITouch {
self.rotation = rotationForLocation(touch.locationInView(self.view))
}
}
func rotationForLocation(location: CGPoint) -> CGFloat {
let offset = CGPoint(x: location.x - view!.bounds.midX, y: location.y - view!.bounds.midY)
return atan2(offset.y, offset.x)
}
}
protocol RotateStartDelegate {
func rotationStart(startingPoint: CGPoint, angle: CGFloat)
}
private class RotationGestureRecognizer: UIPanGestureRecognizer {
/** Angle of rotation **/
var rotation: CGFloat = 0.0
var rotationStart: CGFloat = 0.0
var currentState: String = "idle"
var rotationDelegate: RotateStartDelegate
init(delegate: RotateStartDelegate, target: AnyObject, action: Selector) {
self.rotationDelegate = delegate
super.init(target: target, action: action)
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
super.touchesBegan(touches, withEvent: event)
self.currentState = "begin"
if let touch = touches[touches.startIndex] as? UITouch {
self.rotationDelegate.rotationStart(touch.locationInView(self.view), angle: rotationForLocation(touch.locationInView(self.view)))
}
}
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
super.touchesMoved(touches, withEvent: event)
self.currentState = "dragging"
updateRotationWithTouches(touches)
}
override func touchesEnded(touches: Set<NSObject>!, withEvent event: UIEvent!) {
super.touchesEnded(touches, withEvent: event)
self.currentState = "idle"
}
func updateRotationWithTouches(touches: Set<NSObject>) {
if let touch = touches[touches.startIndex] as? UITouch {
self.rotation = rotationForLocation(touch.locationInView(self.view))
}
}
func rotationForLocation(location: CGPoint) -> CGFloat {
let offset = CGPoint(x: location.x - view!.bounds.midX, y: location.y - view!.bounds.midY)
return atan2(offset.y, offset.x)
}
}

Pie Chart slices in swift

I'm trying to make a pie chart. Actually it's done, but I would like to get some values, and each value should be a slice of the pie. The only thing I could do is fill the pie with a slider. How can I make different slices with different colors for some values?
Here is my code for drawing the chart (I got here in stack) :
import UIKit
#IBDesignable class ChartView: UIView {
#IBInspectable var progress : Double = 0.0 {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var noProgress : Double = 0.0 {
didSet {
self.setNeedsDisplay()
}
}
required init(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
self.contentMode = .Redraw
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clearColor()
self.contentMode = .Redraw
}
override func drawRect(rect: CGRect) {
let color = UIColor.blueColor().CGColor
let lineWidth : CGFloat = 2.0
// Calculate box with insets
let margin: CGFloat = lineWidth
let box0 = CGRectInset(self.bounds, margin, margin)
let side : CGFloat = min(box0.width, box0.height)
let box = CGRectMake((self.bounds.width-side)/2, (self.bounds.height-side)/2,side,side)
let ctx = UIGraphicsGetCurrentContext()
// Draw outline
CGContextBeginPath(ctx)
CGContextSetStrokeColorWithColor(ctx, UIColor.blackColor().CGColor)
CGContextSetLineWidth(ctx, lineWidth)
CGContextAddEllipseInRect(ctx, box)
CGContextClosePath(ctx)
CGContextStrokePath(ctx)
// Draw arc
let delta : CGFloat = -CGFloat(M_PI_2)
let radius : CGFloat = min(box.width, box.height)/2.0
func prog_to_rad(p: Double) -> CGFloat {
let rad = CGFloat((p * M_PI)/180)
return rad
}
func draw_arc(s: CGFloat, e: CGFloat, color: CGColor) {
CGContextBeginPath(ctx)
CGContextMoveToPoint(ctx, box.midX, box.midY)
CGContextSetFillColorWithColor(ctx, color)
CGContextAddArc(ctx, box.midX, box.midY, radius-lineWidth/2, s, e, 0)
CGContextClosePath(ctx)
CGContextFillPath(ctx)
}
if progress > 0 {
let s = prog_to_rad(noProgress * 360/100)
let e = prog_to_rad(progress * 360/100)
draw_arc(s, e, color)
}
}
}
And here is my ViewController:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var pieChartView: ChartView!
#IBOutlet weak var slider: UISlider!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func setValue(sender: UISlider) {
pieChartView.progress = Double(sender.value)
}
}
This code is from my blogpost, it uses CAShapeLayer and UIBezierPath. You can create any number of segments with whichever choice of colour you like.
extension CGFloat {
func radians() -> CGFloat {
let b = CGFloat(M_PI) * (self/180)
return b
}
}
extension UIBezierPath {
convenience init(circleSegmentCenter center:CGPoint, radius:CGFloat, startAngle:CGFloat, endAngle:CGFloat)
{
self.init()
self.moveToPoint(CGPointMake(center.x, center.y))
self.addArcWithCenter(center, radius:radius, startAngle:startAngle.radians(), endAngle: endAngle.radians(), clockwise:true)
self.closePath()
}
}
func pieChart(pieces:[(UIBezierPath, UIColor)], viewRect:CGRect) -> UIView {
var layers = [CAShapeLayer]()
for p in pieces {
let layer = CAShapeLayer()
layer.path = p.0.CGPath
layer.fillColor = p.1.CGColor
layer.strokeColor = UIColor.whiteColor().CGColor
layers.append(layer)
}
let view = UIView(frame: viewRect)
for l in layers {
view.layer.addSublayer(l)
}
return view
}
let rectSize = CGRectMake(0,0,400,400)
let centrePointOfChart = CGPointMake(CGRectGetMidX(rectSize),CGRectGetMidY(rectSize))
let radius:CGFloat = 100
let piePieces = [(UIBezierPath(circleSegmentCenter: centrePointOfChart, radius: radius, startAngle: 250, endAngle: 360),UIColor.brownColor()), (UIBezierPath(circleSegmentCenter: centrePointOfChart, radius: radius, startAngle: 0, endAngle: 200),UIColor.orangeColor()), (UIBezierPath(circleSegmentCenter: centrePointOfChart, radius: radius, startAngle: 200, endAngle: 250),UIColor.lightGrayColor())]
pieChart(piePieces, viewRect: CGRectMake(0,0,400,400))
You posted a bunch of code that appears to draw a single pie chart "slice" in a single color.
Are you saying that you don't know how to make it draw an entire pie, with slices of different sizes, and that you don't know how to make each slice a different color?
It sounds to me like you are copy/pasting code you got from somewhere and have no idea how it works. How about you walk us through what your code does and give us a clearer idea of where you're stuck?
We're not here to take your copy/paste code and modify it for you to make it meet your requirements. Sounds like custom development to me. I don't know about the other posters on this board, but I get paid for that.
As it happens I've written a development blog post that includes a sample app that generates pie charts in Swift. You can see it here:
http://wareto.com/swift-piecharts
Instead of overriding drawRect like the code you posted, it creates a CAShapeLayer that holds the pie chart. It manages a pie chart with a variable number of "slices", and will either change the arc of each slice, the radius, or both.
It is not set up to make each slice a different color. For that you'd have to modify it to use separate shape layers for each slice, which would be a fairly big structural change to the program.
It does at least show you how to draw a pie chart in Swift for iOS:
Below Code is useful for Pie Chart Slice space in swift. Check out once
import UIKit
private extension CGFloat {
/// Formats the CGFloat to a maximum of 1 decimal place.
var formattedToOneDecimalPlace : String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 1
return formatter.string(from: NSNumber(value: self.native)) ?? "\(self)"
}
}
/// Defines a segment of the pie chart
struct Segment {
/// The color of the segment
var color : UIColor
/// The name of the segment
var name : String
/// The value of the segment
var value : CGFloat
}
class PieChartView: UIView {
/// An array of structs representing the segments of the pie chart
var segments = [Segment]() {
didSet {
totalValue = segments.reduce(0) { $0 + $1.value }
setupLabels()
setNeedsDisplay() // re-draw view when the values get set
layoutLabels();
} // re-draw view when the values get set
}
/// Defines whether the segment labels should be shown when drawing the pie chart
var showSegmentLabels = true {
didSet { setNeedsDisplay() }
}
/// Defines whether the segment labels will show the value of the segment in brackets
var showSegmentValueInLabel = false {
didSet { setNeedsDisplay() }
}
/// The font to be used on the segment labels
var segmentLabelFont = UIFont.systemFont(ofSize: 14) {
didSet {
textAttributes[NSAttributedStringKey.font] = segmentLabelFont
setNeedsDisplay()
}
}
private let paragraphStyle : NSParagraphStyle = {
var p = NSMutableParagraphStyle()
p.alignment = .center
return p.copy() as! NSParagraphStyle
}()
private lazy var textAttributes : [NSAttributedStringKey : NSObject] = {
return [NSAttributedStringKey.paragraphStyle : self.paragraphStyle, NSAttributedStringKey.font : self.segmentLabelFont]
}()
override init(frame: CGRect) {
super.init(frame: frame)
isOpaque = false // when overriding drawRect, you must specify this to maintain transparency.
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private var labels: [UILabel] = []
private var totalValue: CGFloat = 1;
override func draw(_ rect: CGRect) {
let anglePI2 = (CGFloat.pi * 2)
let center = CGPoint.init(x: bounds.size.width / 2, y: bounds.size.height / 2)
let radius = min(bounds.size.width, bounds.size.height) / 2;
let lineWidth: CGFloat = 1.5;
let ctx = UIGraphicsGetCurrentContext()
ctx?.setLineWidth(lineWidth)
var currentAngle: CGFloat = 0
if totalValue <= 0 {
totalValue = 1
}
let iRange = 0 ..< segments.count
for i in iRange {
let segment = segments[i]
// calculate percent
let percent = segment.value / totalValue
let angle = anglePI2 * percent
ctx?.beginPath()
ctx?.move(to: center)
ctx?.addArc(center: center, radius: radius - lineWidth, startAngle: currentAngle, endAngle: currentAngle + angle, clockwise: false)
ctx?.closePath()
ctx?.setFillColor(segment.color.cgColor)
ctx?.fillPath()
ctx?.beginPath()
ctx?.move(to: center)
ctx?.addArc(center: center, radius: radius - (lineWidth / 2), startAngle: currentAngle, endAngle: currentAngle + angle, clockwise: false)
ctx?.closePath()
ctx?.setStrokeColor(UIColor.white.cgColor)
ctx?.strokePath()
currentAngle += angle
}
}
override func layoutSubviews() {
super.layoutSubviews()
self.layoutLabels()
}
private func setupLabels() {
var diff = segments.count - labels.count;
if diff >= 0 {
for _ in 0 ..< diff {
let lbl = UILabel()
self.addSubview(lbl)
labels.append(lbl)
}
} else {
while diff != 0 {
var lbl: UILabel!
if labels.count <= 0 {
break;
}
lbl = labels.removeLast()
if lbl.superview != nil {
lbl.removeFromSuperview()
}
diff += 1;
}
}
for i in 0 ..< segments.count {
let lbl = labels[i]
lbl.textColor = UIColor.white
// Change here for your text display
// I currently display percent of each pies
lbl.text = "\(segments[i].value.formattedToOneDecimalPlace)%" //String.init(format: "%0.0f", segments[i].value)
lbl.font = UIFont.systemFont(ofSize: 14)
}
}
func layoutLabels() {
let anglePI2 = CGFloat.pi * 2
let center = CGPoint.init(x: bounds.size.width / 2, y: bounds.size.height / 2)
let radius = min(bounds.size.width / 2, bounds.size.height / 2) / 1.5
var currentAngle: CGFloat = 0;
let iRange = 0 ..< labels.count
for i in iRange {
let lbl = labels[i]
let percent = segments[i].value / totalValue
let intervalAngle = anglePI2 * percent;
lbl.frame = .zero;
lbl.sizeToFit()
let x = center.x + radius * cos(currentAngle + (intervalAngle / 2))
let y = center.y + radius * sin(currentAngle + (intervalAngle / 2))
lbl.center = CGPoint.init(x: x, y: y)
currentAngle += intervalAngle
}
}
}

Resources