Artefact drawing in Swift - ios

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.

Related

Swift 3 Gesture Recognizers path.boundingBox is infinite

I wanted to learn more about custom gesture recognizers so I was reading the Ray Wenderlich tutorial, which I planned to modify in order to learn the details and what I can change easily to learn how each piece works, but it was written in a previous version of Swift. Swift updated most of the code and I was able to fix the rest manually except that I am having trouble getting the touch gestures to be drawn on the screen, and no shapes are recognized as circles, which I'm hoping both tie back to the same problem. The website and code snippet are as follows:
https://www.raywenderlich.com/104744/uigesturerecognizer-tutorial-creating-custom-recognizers
import UIKit
import UIKit.UIGestureRecognizerSubclass
class CircleGestureRecognizer: UIGestureRecognizer {
fileprivate var touchedPoints = [CGPoint]() // point history
var fitResult = CircleResult() // information about how circle-like is the path
var tolerance: CGFloat = 0.2 // circle wiggle room, lower is more circle like higher is oval or other
var isCircle = false
var path = CGMutablePath() // running CGPath - helps with drawing
override func touchesBegan(_ touches: (Set<UITouch>!), with event: UIEvent) {
if touches.count != 1 {
state = .failed
}
state = .began
let window = view?.window
if let touches = touches, let loc = touches.first?.location(in: window) {
//print("path 1 \(path.currentPoint)")
path.move(to: CGPoint(x: loc.x, y: loc.y)) // start the path
print("path 2 \(path.currentPoint)")
}
//super.touchesBegan(touches, with: event)
}
override func touchesMoved(_ touches: (Set<UITouch>!), with event: UIEvent) {
// 1
if state == .failed {
return
}
// 2
let window = view?.window
if let touches = touches, let loc = touches.first?.location(in: window) {
// 3
touchedPoints.append(loc)
print("path 3 \(path.currentPoint)")
path.move(to: CGPoint(x: loc.x, y: loc.y))
print("path 4 \(path.currentPoint)")
// 4
state = .changed
}
}
override func touchesEnded(_ touches: (Set<UITouch>!), with event: UIEvent) {
print("path 5 \(path.currentPoint)")
// now that the user has stopped touching, figure out if the path was a circle
fitResult = fitCircle(touchedPoints)
// make sure there are no points in the middle of the circle
let hasInside = anyPointsInTheMiddle()
let percentOverlap = calculateBoundingOverlap()
isCircle = fitResult.error <= tolerance && !hasInside && percentOverlap > (1-tolerance)
state = isCircle ? .ended : .failed
}
override func reset() {
//super.reset()
touchedPoints.removeAll(keepingCapacity: true)
path = CGMutablePath()
isCircle = false
state = .possible
}
fileprivate func anyPointsInTheMiddle() -> Bool {
// 1
let fitInnerRadius = fitResult.radius / sqrt(2) * tolerance
// 2
let innerBox = CGRect(
x: fitResult.center.x - fitInnerRadius,
y: fitResult.center.y - fitInnerRadius,
width: 2 * fitInnerRadius,
height: 2 * fitInnerRadius)
// 3
var hasInside = false
for point in touchedPoints {
if innerBox.contains(point) {
hasInside = true
break
}
}
//print(hasInside)
return hasInside
}
fileprivate func calculateBoundingOverlap() -> CGFloat {
// 1
let fitBoundingBox = CGRect(
x: fitResult.center.x - fitResult.radius,
y: fitResult.center.y - fitResult.radius,
width: 2 * fitResult.radius,
height: 2 * fitResult.radius)
let pathBoundingBox = path.boundingBox
// 2
let overlapRect = fitBoundingBox.intersection(pathBoundingBox)
// 3
let overlapRectArea = overlapRect.width * overlapRect.height
let circleBoxArea = fitBoundingBox.height * fitBoundingBox.width
let percentOverlap = overlapRectArea / circleBoxArea
print("Percent Overlap \(percentOverlap)")
print("pathBoundingBox \(pathBoundingBox)")
print("path 6 \(path.currentPoint)")
return percentOverlap
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
state = .cancelled // forward the cancel state
}
}
As shown in the tutorial this bit of code is supposed to compare a bounding box for the path to a box that would fit a circle and compare the overlap, but when I print the pathBoundingBox is states: "pathBoundingBox (inf, inf, 0.0, 0.0)" which is probably why the percentOverlap is 0. I was thinking it was the path.move(to: loc) where loc is the first touch location but the documentation for move(to:) says "This method implicitly ends the current subpath (if any) and sets the current point to the value in the point parameter." so I'm struggling to figure out why the path.boundingBox is infinite...
That's not an infinite bounding box, it's just the opposite — a zero bounding box. The problem is that your path is empty.

Swift 3 Generate evenly-spaced SKSpriteNodes along path drawn by user

everyone! First of all, I'm aware that this question is very similar to Draw images evenly spaced along a path in iOS. However, that is in Objective-C (which I can't read) and it is in a normal ViewController working with CGImageRefs. I need it in swift and using SKSpriteNodes (not CGImageRefs). Here's my issue:
I'm trying to make a program that lets the user draw a simple shape (like a circle) and places SKSpriteNodes at fixed intervals along the path drawn by the user. I've got it working fine at a slow pace, but if the user draws too quickly then the nodes get placed too far apart. Here's an example of when I draw it slowly:
User-drawn path with nodes placed approximately 60 pixels apart from each other. Blue is the start node, purple is the end node.
The goal is that each node would have a physicsBody that kept entities from crossing the line drawn by the user (those entities wouldn't be able to squeeze in between evenly spaced nodes). If the user draws too fast, however, there will be a gap in defenses that I can't fix. For example:
Note the visibly larger gap between the 7th and 8th nodes. This occurred because I drew too quickly. Many people have questions that are slightly similar but are unhelpful for my task (e.g. place a specific amount of nodes evenly spaced along a path, rather than place as many nodes as neccessary to get them 60 pixels apart along the path).
In conclusion, here is my main question again: How can I place nodes perfectly spaced along a user-drawn path of any shape? Thank you in advance for your help! Here is my GameScene.swift file:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
let minDist: CGFloat = 60 //The minimum distance between one point and the next
var points: [CGPoint] = []
var circleNodes: [SKShapeNode] = []
override func didMove(to view: SKView) {
}
func getDistance (fromPoint: CGPoint, toPoint: CGPoint) -> CGFloat {
let deltaX = fromPoint.x - toPoint.x
let deltaY = fromPoint.y - toPoint.y
let deltaXSquared = deltaX*deltaX
let deltaYSquared = deltaY*deltaY
return sqrt(deltaXSquared + deltaYSquared) //Return the distance
}
func touchDown(atPoint pos : CGPoint) {
self.removeAllChildren()
//The first time the user touches, we need to place a point and mark that as the firstCircleNode
print(pos)
points.append(pos)
//allPoints.append(pos)
let firstCircleNode = SKShapeNode(circleOfRadius: 5.0)
firstCircleNode.fillColor = UIColor.blue
firstCircleNode.strokeColor = UIColor.blue
firstCircleNode.position = pos
circleNodes.append(firstCircleNode)
self.addChild(firstCircleNode)
}
func touchMoved(toPoint pos : CGPoint) {
let lastIndex = points.count - 1 //The index of the last recorded point
let distance = getDistance(fromPoint: points[lastIndex], toPoint: pos)
//vector_distance(vector_double2(Double(points[lastIndex].x), Double(points[lastIndex].y)), vector_double2(Double(pos.x), Double(pos.y))) //The distance between the user's finger and the last placed circleNode
if distance >= minDist {
points.append(pos)
//Add a box to that point
let newCircleNode = SKShapeNode(circleOfRadius: 5.0)
newCircleNode.fillColor = UIColor.red
newCircleNode.strokeColor = UIColor.red
newCircleNode.position = pos
circleNodes.append(newCircleNode)
self.addChild(newCircleNode)
}
}
func touchUp(atPoint pos : CGPoint) {
//When the user has finished drawing a circle:
circleNodes[circleNodes.count-1].fillColor = UIColor.purple //Make the last node purple
circleNodes[circleNodes.count-1].strokeColor = UIColor.purple
//Calculate the distance between the first placed node and the last placed node:
let distance = getDistance(fromPoint: points[0], toPoint: points[points.count-1])
//vector_distance(vector_double2(Double(points[0].x), Double(points[0].y)), vector_double2(Double(points[points.count - 1].x), Double(points[points.count - 1].y)))
if distance <= minDist { //If the distance is closer than the minimum distance
print("Successful circle")
} else { //If the distance is too far
print("Failed circle")
}
points = []
circleNodes = []
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchDown(atPoint: t.location(in: self)) }
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchMoved(toPoint: t.location(in: self)) }
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchUp(atPoint: t.location(in: self)) }
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchUp(atPoint: t.location(in: self)) }
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}
}
You could try resizing the vector:
func touchMoved(toPoint pos : CGPoint) {
let lastIndex = points.count - 1 //The index of the last recorded point
let distance = getDistance(fromPoint: points[lastIndex], toPoint: pos)
if distance >= minDist {
// find a new "pos" which is EXACTLY minDist distant
let vx = pos.x - points[lastIndex].x
let vy = pos.y - points[lastIndex].y
vx /= distance
vy /= distance
vx *= minDist
vy *= minDist
let newpos = CGPoint(x: vx, y:vy)
points.append(newpos)
//Add a box to that point
let newCircleNode = SKShapeNode(circleOfRadius: 5.0)
newCircleNode.fillColor = UIColor.red
newCircleNode.strokeColor = UIColor.red
newCircleNode.position = newpos // NOTE
circleNodes.append(newCircleNode)
self.addChild(newCircleNode)
}
}
It probably won't be perfect but might look better.
I've figured it out! I was inspired by Christian Cerri's suggestion so I used the following code to make what I wanted:
import SpriteKit
import GameplayKit
// MARK: - GameScene
class GameScene: SKScene {
// MARK: - Allows me to work with vectors. Derived from https://www.raywenderlich.com/145318/spritekit-swift-3-tutorial-beginners
func subtract(point: CGPoint, fromPoint: CGPoint) -> CGPoint {
return CGPoint(x: point.x - fromPoint.x, y: point.y - fromPoint.y) //Returns a the first vector minus the second
}
func add(point: CGPoint, toPoint: CGPoint) -> CGPoint {
return CGPoint(x: point.x + toPoint.x, y: point.y + toPoint.y) //Returns a the first vector minus the second
}
func multiply(point: CGPoint, by scalar: CGFloat) -> CGPoint {
return CGPoint(x: point.x * scalar, y: point.y * scalar)
}
func divide(point: CGPoint, by scalar: CGFloat) -> CGPoint {
return CGPoint(x: point.x / scalar, y: point.y / scalar)
}
func magnitude(point: CGPoint) -> CGFloat {
return sqrt(point.x*point.x + point.y*point.y)
}
func normalize(aPoint: CGPoint) -> CGPoint {
return divide(point: aPoint, by: magnitude(point: aPoint))
}
// MARK: - Properties
let minDist: CGFloat = 60
var userPath: [CGPoint] = [] //Holds the coordinates collected when the user drags their finger accross the screen
override func didMove(to view: SKView) {
}
// MARK: - Helper methods
func getDistance (fromPoint: CGPoint, toPoint: CGPoint) -> CGFloat
{
let deltaX = fromPoint.x - toPoint.x
let deltaY = fromPoint.y - toPoint.y
let deltaXSquared = deltaX*deltaX
let deltaYSquared = deltaY*deltaY
return sqrt(deltaXSquared + deltaYSquared) //Return the distance
}
func touchDown(atPoint pos : CGPoint) {
userPath = []
self.removeAllChildren()
//Get the first point the user makes
userPath.append(pos)
}
func touchMoved(toPoint pos : CGPoint) {
//Get every point the user makes as they drag their finger across the screen
userPath.append(pos)
}
func touchUp(atPoint pos : CGPoint) {
//Get the last position the user was left touching when they've completed the motion
userPath.append(pos)
//Print the entire path:
print(userPath)
print(userPath.count)
plotNodesAlongPath()
}
/**
Puts nodes equidistance from each other along the path that the user placed
*/
func plotNodesAlongPath() {
//Start at the first point
var currentPoint = userPath[0]
var circleNodePoints = [currentPoint] //Holds the points that I will then use to generate circle nodes
for i in 1..<userPath.count {
let distance = getDistance(fromPoint: currentPoint, toPoint: userPath[i]) //The distance between the point and the next
if distance >= minDist { //If userPath[i] is at least minDist pixels away
//Then we can make a vector that points from currentPoint to userPath[i]
var newNodePoint = subtract(point: userPath[i], fromPoint: currentPoint)
newNodePoint = normalize(aPoint: newNodePoint) //Normalize the vector so that we have only the direction and a magnitude of 1
newNodePoint = multiply(point: newNodePoint, by: minDist) //Stretch the vector to a length of minDist so that we now have a point for the next node to be drawn on
newNodePoint = add(point: currentPoint, toPoint: newNodePoint) //Now add the vector to the currentPoint so that we get a point in the correct position
currentPoint = newNodePoint //Update the current point. Next we want to draw a point minDist away from the new current point
circleNodePoints.append(newNodePoint) //Add the new node
}
//If distance was less than minDist, then we want to move on to the next point in line
}
generateNodesFromPoints(positions: circleNodePoints)
}
func generateNodesFromPoints(positions: [CGPoint]) {
print("generateNodesFromPoints")
for pos in positions {
let firstCircleNode = SKShapeNode(circleOfRadius: 5.0)
firstCircleNode.fillColor = UIColor.blue
firstCircleNode.strokeColor = UIColor.blue
firstCircleNode.position = pos //Put the node in the correct position
self.addChild(firstCircleNode)
}
}
// MARK: - Touch responders
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchDown(atPoint: t.location(in: self)) }
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchMoved(toPoint: t.location(in: self)) }
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchUp(atPoint: t.location(in: self)) }
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchUp(atPoint: t.location(in: self)) }
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}
}
And this results in the following:
No matter how quickly the user moves their finger, it places nodes evenly along their path. Thanks so much for your help, and I hope it helps more people in the future!

Removing lagging latency in drawing UIBezierPath smooth lines in Swift

The code below draws smooth curved lines by overriding touches, but there is noticeable lagging or latency. The code uses addCurveToPoint and calls setNeedsDisplay after every 4 touch points which causes a jumpy appearance as the drawing doesn't keep up with finger movements. To remove the lagging or perceived latency, touch points 1, 2, 3 (leading up to touch point 4) could be temporarily filled with addQuadCurveToPoint and addLineToPoint.
How can this actually be achieved in code to remove perceived lagging by using a temporary Line and QuadCurved line before displaying a final Curved line?
If the below class is attached to one UIView (e.g. viewOne or self), how do I make a copy of the drawing to another UIView outside the class (e.g. viewTwo) after touchesEnded?
// ViewController.swift
import UIKit
class drawSmoothCurvedLinesWithLagging: UIView {
let path=UIBezierPath()
var incrementalImage:UIImage?
var points = [CGPoint?](count: 5, repeatedValue: nil)
var counter:Int?
var strokeColor:UIColor?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func drawRect(rect: CGRect) {
autoreleasepool {
incrementalImage?.drawInRect(rect)
strokeColor = UIColor.blueColor()
strokeColor?.setStroke()
path.lineWidth = 20
path.lineCapStyle = CGLineCap.Round
path.stroke()
}
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
counter = 0
let touch: AnyObject? = touches.first
points[0] = touch!.locationInView(self)
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
let point = touch!.locationInView(self)
counter = counter! + 1
points[counter!] = point
if counter == 2{
//use path.addLineToPoint ?
//use self.setNeedsDisplay() ?
}
if counter == 3{
//use path.addQuadCurveToPoint ?
//use self.setNeedsDisplay() ?
}
if counter == 4{
points[3]! = CGPointMake((points[2]!.x + points[4]!.x)/2.0, (points[2]!.y + points[4]!.y)/2.0)
path.moveToPoint(points[0]!)
path.addCurveToPoint(points[3]!, controlPoint1: points[1]!, controlPoint2: points[2]!)
self.setNeedsDisplay()
points[0]! = points[3]!
points[1]! = points[4]!
counter = 1
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.drawBitmap()
self.setNeedsDisplay()
path.removeAllPoints()
counter = 0
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
self.touchesEnded(touches!, withEvent: event)
}
func drawBitmap(){
UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0.0)
strokeColor?.setStroke()
if((incrementalImage) == nil){
let rectPath:UIBezierPath = UIBezierPath(rect: self.bounds)
UIColor.whiteColor().setFill()
rectPath.fill()
}
incrementalImage?.drawAtPoint(CGPointZero)
path.stroke()
incrementalImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
class ViewController: UIViewController {
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.
}
}
Yes, adding a curve every few points will give it a stuttering lag. So, yes, you can reduce this affect by adding a line to points[1], adding a quad curve to points[2] and adding a cubic curve to points[3].
As you said, make sure to add this to a separate path, though. So, in Swift 3/4:
class SmoothCurvedLinesView: UIView {
var strokeColor = UIColor.blue
var lineWidth: CGFloat = 20
var snapshotImage: UIImage?
private var path: UIBezierPath?
private var temporaryPath: UIBezierPath?
private var points = [CGPoint]()
override func draw(_ rect: CGRect) {
snapshotImage?.draw(in: rect)
strokeColor.setStroke()
path?.stroke()
temporaryPath?.stroke()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
points = [touch.location(in: self)]
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let point = touch.location(in: self)
points.append(point)
updatePaths()
setNeedsDisplay()
}
private func updatePaths() {
// update main path
while points.count > 4 {
points[3] = CGPoint(x: (points[2].x + points[4].x)/2.0, y: (points[2].y + points[4].y)/2.0)
if path == nil {
path = createPathStarting(at: points[0])
}
path?.addCurve(to: points[3], controlPoint1: points[1], controlPoint2: points[2])
points.removeFirst(3)
temporaryPath = nil
}
// build temporary path up to last touch point
if points.count == 2 {
temporaryPath = createPathStarting(at: points[0])
temporaryPath?.addLine(to: points[1])
} else if points.count == 3 {
temporaryPath = createPathStarting(at: points[0])
temporaryPath?.addQuadCurve(to: points[2], controlPoint: points[1])
} else if points.count == 4 {
temporaryPath = createPathStarting(at: points[0])
temporaryPath?.addCurve(to: points[3], controlPoint1: points[1], controlPoint2: points[2])
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
finishPath()
}
override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
finishPath()
}
private func finishPath() {
constructIncrementalImage()
path = nil
setNeedsDisplay()
}
private func createPathStarting(at point: CGPoint) -> UIBezierPath {
let localPath = UIBezierPath()
localPath.move(to: point)
localPath.lineWidth = lineWidth
localPath.lineCapStyle = .round
localPath.lineJoinStyle = .round
return localPath
}
private func constructIncrementalImage() {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
strokeColor.setStroke()
snapshotImage?.draw(at: .zero)
path?.stroke()
temporaryPath?.stroke()
snapshotImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
You could even marry this with iOS 9 predictive touches (as I described in my other answer), which could reduce lag even further.
To take this resulting image and use it elsewhere, you can just grab the incrementalImage (which I renamed to snapshotImage, above), and drop it into an image view of the other view.
For Swift 2 rendition, see previous revision of this answer.

Artefacts drawing lines in Swift Xcode

The below class attaches to a UIView and draws lines while moving a finger across the screen. However, when moving a finger very fast from left to right moving from top to bottom, the drawing temporarily shows sharp pointy edges on changing direction. This occurs both on the device and simulator.
What is causing this issue and how can this artefact be eliminated in the code so that only smooth rounded, not sharp edges are seen when changing direction fast?
class drawLine: UIView
{
var comittedSegments: Int = 0
var points = [CGPoint]()
var committedPath = UIBezierPath()
var drawPath = UIBezierPath()
var incrementalImage: UIImage?
var strokeColor:UIColor?
override func drawRect(rect: CGRect) {
autoreleasepool {
incrementalImage?.drawInRect(rect)
strokeColor = UIColor.darkGrayColor()
strokeColor?.setStroke()
drawPath.lineWidth = 20
drawPath.lineCapStyle = CGLineCap.Round
drawPath.stroke()
}
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
comittedSegments = 0
committedPath.removeAllPoints()
points.removeAll()
points.append( touch!.locationInView(self) )
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
let point = touch!.locationInView(self)
points.append( point )
if points.count == 5
{
points[3] = CGPointMake((points[2].x + points[4].x)/2.0, (points[2].y + points[4].y)/2.0)
committedPath.moveToPoint(points[0])
committedPath.addCurveToPoint(points[3], controlPoint1: points[1], controlPoint2: points[2])
comittedSegments = comittedSegments + 1
self.setNeedsDisplay()
points[0] = points[3]
points[1] = points[4]
points.removeRange(2...4)
drawPath = committedPath
}
else if points.count > 1
{
drawPath = committedPath.copy() as! UIBezierPath
drawPath.CGPath = committedPath.CGPath
drawPath.moveToPoint( points[0] )
for point in points[1..<points.count] {
drawPath.addLineToPoint(point)
}
self.setNeedsDisplay()
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.drawBitmap()
self.setNeedsDisplay()
committedPath.removeAllPoints()
points.removeAll()
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
self.touchesEnded(touches!, withEvent: event)
}
func drawBitmap() {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0.0)
strokeColor?.setStroke()
if(incrementalImage == nil) {
let rectPath:UIBezierPath = UIBezierPath(rect: self.bounds)
UIColor.whiteColor().setFill()
rectPath.fill()
}
incrementalImage?.drawAtPoint(CGPointZero)
committedPath.stroke()
incrementalImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
In addition to setting lineCapStyle, set lineJoinStyle:
drawPath.lineJoinStyle = .Round
drawPath.lineCapStyle = .Round

Drawing class drawing straight lines instead of curved lines

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

Resources