I have a class below that when attached to a view draws curved lines when the user touches their device. The problem is that the lines drawn seem to lag behind from the position of the finger on the screen. The lagging is enough to be noticeable and mildly annoying as new sections of the line display a small distance away from the finger touching the screen.
The code uses the addCurveToPoint curve method. (The alternative addQuadCurveToPoint curve method appears to be less superior in terms of a quality curved line but does display on screen faster.)
I suspect that this issue relates to when setNeedsDisplay is called once the counter == 4. It appears the code waits until 4 new touch points are received while drawing before a curved line is drawn. Ideally a curved line is drawn at every single touch point (i.e. counter == 1), eliminating the lagging. (Changing Counter == 1 doesn't seem to work.)
I'm lost and don't know how to update the code to improve it further to remove that short lag but retain the curved lines. What needs to change in the below code to remove that short lag?
// Swift 2 code below tested using Xcode 7.0.1.
class drawView: UIView {
var path:UIBezierPath?
var incrementalImage:UIImage?
var points = [CGPoint?](count: 5, repeatedValue: nil)
var counter:Int?
var infoView:UIView = UIView()
var strokeColor:UIColor?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.multipleTouchEnabled = false
self.backgroundColor = UIColor.whiteColor()
path = UIBezierPath()
path?.lineWidth = 20.0
strokeColor = UIColor.darkGrayColor()
path?.lineCapStyle = CGLineCap.Round
}
override init(frame: CGRect) {
super.init(frame: frame)
self.multipleTouchEnabled = false
path = UIBezierPath()
path?.lineWidth = 20.0
}
override func drawRect(rect: CGRect) {
incrementalImage?.drawInRect(rect)
strokeColor?.setStroke()
path?.stroke()
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
counter = 0
let touch: AnyObject? = touches.first
points[0] = touch!.locationInView(self)
infoView.removeFromSuperview()
}
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 == 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()
}
}
To start, I believe that you are doing it wrong. This usally works well if you want to draw a few lines not nesscarly by the users input but for circles, squiggly lines, simple things.
when using:
self.setNeedsDisplay()
You are redrawing ALL the lines EVERYTIME! this is tough CPU and that's why you have a lag. Image the user draws a few hundred lines then into a thousand and everytime he/she touches the screen it will redraw ALL of those lines.
OK. So, What I recommend doing is have 2 UIImageViews: 1) mainImageView - which will hold the overall drawing. 2) tempImageView - which the user will use to draw.
When the user touches/draws on "tempImageView" it draws until they let go of the screen then merge "tempImageView" to "mainImageView"
Here is a tutorial on:
http://www.raywenderlich.com/87899/make-simple-drawing-app-uikit-swift
Related
How do I rotate a SpriteNode with a one finger “touch and drag” so that:
It doesn’t jerk around
It moves in a circle (I’ve successfully accomplished this part several times- both with a code only solution and with an SKS file)
It produces a meaningful value (as a physical control knob would)
It moves while my finger is on it but not when my finger is off
The things I’ve tried:
Using CGAffineTransform’s CGAffineTransformMakeRotation to effect a rotation of the knob in SpriteKit. But I cannot figure out how to use CGAffineTransformMakeRotation on a SpriteNode. I could put a different sort of object into my Scene or on top of it, but that’s just not right.
For example, Matthijs Hollemans’ MHRotaryKnob https://github.com/hollance/MHRotaryKnob
. I translated Hollemans knob from Objective C to Swift 4 but ran into trouble attempting to use it in SpriteKit to rotate sprites. I didn’t get that because I could not figure out how to use knobImageView.transform = CGAffineTransformMakeRotation (newAngle * M_PI/180.0); in Swift with SpriteKit. I know I could use Hollemans Objective C class and push a UIImage over the top of my scene, but that doesn’t seem like the best nor most elegant solution.
I also translated Wex’s solution from Objective C to Swift
Rotate image on center using one finger touch
Using Allan Weir’s suggestions on dealing with the CGAffineTransform portions of the code https://stackoverflow.com/a/41724075/1678060 But that doesn’t work.
I've tried setting the zRotation on my sprite directly without using .physicalBody to no avail. It has the same jerky movement and will not stop where you want it to stop. And moves in the opposite direction of your finger drag- even when you put the '-' in front of the radian angle.
I’ve also tried 0x141E’s solution on Stack Overflow:
Drag Rotate a Node around a fixed point This is the solution posted below using an .sks file (somewhat modified- I've tried the un-modified version and it is no better). This solution jerks around, doesn’t smoothly follow my finger, cannot consistently move the knob to a specific point. Doesn’t matter if I set physicsBody attributes to create friction, mass, or angularDamping and linearDamping or reducing the speed of the SKSpriteNode.
I have also scoured the Internet looking for a good solution in Swift 4 using SpriteKit, but so far to no avail.
import Foundation
import SpriteKit
class Knob: SKSpriteNode
{
var startingAngle: CGFloat?
var currentAngle: CGFloat?
var startingTime: TimeInterval?
var startingTouchPoint: CGPoint?
override init(texture: SKTexture?, color: UIColor, size: CGSize) {
super.init(texture: texture, color: color, size: size)
self.setupKnob()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupKnob()
}
func setupKnob() {
self.physicsBody = SKPhysicsBody(circleOfRadius: CGFloat(self.size.height))
self.physicsBody?.pinned = true
self.physicsBody?.isDynamic = true
self.physicsBody?.affectedByGravity = false
self.physicsBody?.allowsRotation = true
self.physicsBody?.mass = 30.0
//self.physicsBody?.friction = 0.8
//self.physicsBody?.angularDamping = 0.8
//self.physicsBody?.linearDamping = 0.9
//self.speed = 0.1
self.isUserInteractionEnabled = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in:self)
let node = atPoint(location)
startingTouchPoint = CGPoint(x: location.x, y: location.y)
if node.name == "knobHandle" {
let dx = location.x - node.position.x
let dy = location.y - node.position.y
startingAngle = atan2(dy, dx)
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches{
let location = touch.location(in:self)
let node = atPoint(location)
guard startingAngle != nil else {return}
if node.name == "knobHandle" {
let dx:CGFloat = location.x - node.position.x
let dy:CGFloat = location.y - node.position.y
var angle: CGFloat = atan2(dy, dx)
angle = ((angle) * (180.0 / CGFloat.pi))
let rotate = SKAction.rotate(toAngle: angle, duration: 2.0, shortestUnitArc: false)
self.run(rotate)
startingAngle = angle
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
var touch: UITouch = touches.first!
var location: CGPoint = touch.location(in: self)
self.removeAllActions()
startingAngle = nil
startingTime = nil
}
}
Edit: If I remove the conversion to degrees and change the duration of the SKAction to 1.0 in SKAction.rotate(toAngle:duration: 1.0, shortestUnitArc:) then it almost works: not as jerky, but still jerks; the lever doesn't change directions well- meaning sometimes if you attempt to move it opposite of the direction it was traveling it continues to go the old direction around the anchorPoint instead of the new direction you're dragging it.
Edit 2: GreatBigBore and I discussed both the SKAction rotation and the self.zRotation- the code above and the code below.
Edit 3: sicvayne suggested some code for the SKScene and I've adapted to SKSpriteNode (below). It doesn't move consistently or allow to you stop in a specific place.
import Foundation
import SpriteKit
class Knob: SKSpriteNode {
var fingerLocation = CGPoint()
override init(texture: SKTexture?, color: UIColor, size: CGSize) {
super.init(texture: texture, color: color, size: size)
self.setupKnob()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupKnob()
}
func setupKnob() {
self.isUserInteractionEnabled = true
}
func rotateKnob(){
let radians = atan2(fingerLocation.x - self.position.x, fingerLocation.y - self.position.y)
self.zRotation = -radians
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
fingerLocation = touch.location(in: self)
}
self.rotateKnob()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
}
/*override func update(_ currentTime: TimeInterval) { //this is a SKScene function
rotateKnob()
}*/
}
The Math was wrong. Here's what I learned you need:
How this looks in swift:
if point.x.sign == .minus {
angle = atan(point.y/point.x) + CGFloat.pi/2
} else {
angle = atan(point.y/point.x) + CGFloat.pi/2 + CGFloat.pi
}
Also, you have to get the coordinates of another object in the scene because the entire coordinate system rotates with the object:
let body = parent?.childNode(withName: "objectInScene")
let point = touch.location(in: body!)
I usually do something like this without any jittering or jerking issues.
var fingerLocation = CGPoint()
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch: AnyObject in touches {
fingerLocation = touch.location(in: self)
}
}
func rotatePlayer(){
let radians = atan2(fingerLocation.x - playerNode.position.x, fingerLocation.y - playerNode.position.y)
playerNode.zRotation = -radians//this rotates the player
}
override func update(_ currentTime: TimeInterval) {
rotatePlayer()
}
Depending on how your images are facing, you're probably going to have to mess around with the radians. In my case, my "player" image is facing upwards. Hope this helped.
I'm trying to change the line width of my drawing app using a slider but every time I change the slider and redraw a line, all lines on the screen change to the currently selected line width. I must be doing something wrong.
var layers:[Array<CGPoint>] = []
var layerIndex = 0
var sliderValue: CGFloat = 3.0
var strokeInfo:[[String:Any]] = []
//change the slider
func slider(value: CGFloat) {
sliderValue = value
print("Slider value is \(sliderValue)")
}
//on new touch, start a new array (layer) of points
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
var points:[CGPoint] = []
layers.append(points)
var info = Dictionary<String,Any>()
info["line"] = sliderValue
strokeInfo.insert(info, at: layerIndex)
let pt = (touches.first!).location(in: self)
points.append(pt)
}
//add each point to the correct array as the finger moves
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let pt = (touches.first!).location(in: self)
layers[layerIndex].append(pt)
self.setNeedsDisplay()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
print("layer \(layerIndex) now has \(layers[layerIndex].count)")
layerIndex += 1
}
override func draw(_ rect: CGRect) {
//get pointer to view
let context = UIGraphicsGetCurrentContext()
context?.clear(rect)
for (index, element) in strokeInfo.enumerated() {
if index == layerIndex {
context?.setLineWidth(element["line"] as! CGFloat)
}
}
//loop through the layers if any
for points in layers {
UIColor.cyan.set()
//loop through each layer's point values
if points.count - 1 > 0 {
for i in 0 ..< points.count-1 {
let pt1 = points[i] as CGPoint
let pt2 = points[i + 1] as CGPoint
context?.setLineCap(.round)
context?.move(to: pt1)
context?.addLine(to: pt2)
context?.strokePath()
}
}
}
}
You're changing the line width and line cap of the context. The settings of the graphics context affect the entire path associated with the context.
If you want to draw different paths, I suggest you use multiple UIBezierPath objects, each with it's own width, color, and line cap settings. You can draw your bezier paths in your drawRect method.
Alternately you could use multiple CAShapeLayers, each with different path drawing settings and stack them on top of each other to get the composite image you want.
I have Swift 2 code below that draws curved lines when the user touches and moves their finger across the screen. New sections of the curved line are added as the user moves their finger to give one continuous curved line on screen. (top image)
However, I wish to change the below code so that as each new section of the line is added and drawn to screen, the previous section and earlier sections are deleted, so that all is seen on the screen is the new section and nothing else. (bottom image)
What needs to be modified in the section of code below to achieve this?
// Swift 2 code below tested using Xcode 7.0.1.
class drawLines: UIView {
let path=UIBezierPath()
var previousPoint:CGPoint = CGPoint.zero
var strokeColor:UIColor?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func drawRect(rect: CGRect) {
strokeColor = UIColor.blackColor()
strokeColor?.setStroke()
path.lineWidth = 10
path.stroke()
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
let currentPoint = touch!.locationInView(self)
path.moveToPoint(currentPoint)
previousPoint=currentPoint
self.setNeedsDisplay()
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
let currentPoint = touch!.locationInView(self)
let midPoint = self.midPoint(previousPoint, p1: currentPoint)
path.addQuadCurveToPoint(midPoint,controlPoint: previousPoint)
previousPoint=currentPoint
self.setNeedsDisplay()
path.moveToPoint(midPoint)
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.setNeedsDisplay()
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
self.touchesEnded(touches!, withEvent: event)
}
func midPoint(p0:CGPoint,p1:CGPoint)->CGPoint
{
let x=(p0.x+p1.x)/2
let y=(p0.y+p1.y)/2
return CGPoint(x: x, y: y)
}
}
As far as I know, there's no way to remove points from a UIBezierPath. The solution you're looking for is to store the points in an array, which you can modify freely, then create your path from that point array.
Update
I'm getting a bit confused re-reading your post alongside the comments you've posted. That being said, I've tried to make the example below contain all the elements you need – you might have to re-arrange them if I have not understood you 100%.
This code continuously adds points to an array from the point a touch begins, including while it moves. I added a cap to the maximum number of points so that it removes excess line points if needed. When the user releases their finger I've made it clear all the points.
Note: I've tried to keep the code as simple as possible so that it's clear. Once you find the correct combination of adding/removing points that matches your needs, you should probably look at optimising this. In particular, this assumes a naïve method of drawing where all drawing happens simultaneously (vs deltas), which isn't efficient. The method of removing excess points can also be optimised once you understand it.
import UIKit
class ViewController: UIViewController {
var points = [CGPoint]()
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
addTouch(touches.first)
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
addTouch(touches.first)
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
points.removeAll()
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
points.removeAll()
}
func addTouch(touch: UITouch?) {
guard let touch = touch else { return }
let currentPoint = touch.locationInView(self.view)
points.append(currentPoint)
// if you want a limit on your line length, you need these lines
// 20 is an arbitrary number
while (points.count > 20) {
points.removeFirst()
}
}
}
All that remains is for you to add that to your current line drawing code, and you should be good to go.
Apple have done a number of good WWDC talks on graphics performance. This one in particular might be helpful.
If I understand your question correctly, you'll want to reset the
path in the touchesBegan() method
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
path.removeAllPoints()
// ...
}
You could save an array of points and rebuild your bezier path each time as TwoStraws recommends.
Another option would be to draw your curve using a CAShapeLayer, and change the shapeBegin property. (As you move from shapeBegin = 1.0 to shapeBegin = 0.0 it truncates the beginning of the shape.)
A third option would be to edit the path to delete the earlier points. Erica Sadun has sample code in her outstanding iOS Developers' Cookbook series that shows how to parse the points inside a Bezier path (If I remember correctly it actually uses the underlying CGPath object that is inside a Bezier path.) That would be much faster than rebuilding your bezier path each time, but more work for you.
I have a function for drawing lines, edited below. It seems to work fine in the simulator, however there are performance issues, lines drawn slowly, when running on both an older iPhone (2011) and a newer iPad (2014). I believe this issue is due to creating a new CGContext for every touchesMoved event received.
How can I, for example, call let context = UIGraphicsGetCurrentContext() once when touchesBegan? (i.e. how can I make context a public variable that can be called once?)
Any other tips for improving the performance would be appreciated. Thank you.
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
autoreleasepool {
UIGraphicsBeginImageContextWithOptions(view.frame.size, false, 0.0)
let context = UIGraphicsGetCurrentContext()
...
...
...
UIGraphicsEndImageContext()
}
}
Do not execute drawing code in touchesMoved. You should store whatever you need to update your drawing (probably touch location), then call setNeedsDisplay. That will force a call to drawRect: which would contain all of your drawing code. You do not need to create a context, just use UIGraphicsGetCurrentContext().
Here is a contrived UIView subclass example that draws a red circle underneath the latest touch point.
class DrawView: UIView {
let circleSize:CGFloat = 50.0
var lastTouchPoint:CGPoint?
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
lastTouchPoint = touches.first?.locationInView(self)
self.setNeedsDisplay()
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
lastTouchPoint = touches.first?.locationInView(self)
self.setNeedsDisplay()
}
override func drawRect(rect: CGRect) {
if let touchPoint = lastTouchPoint {
let context = UIGraphicsGetCurrentContext()
CGContextSetRGBFillColor (context, 1, 0, 0, 1);
CGContextFillEllipseInRect(context, CGRectMake(touchPoint.x - circleSize/2.0, touchPoint.y - circleSize/2.0, circleSize , circleSize))
}
}
}
Thank you for taking the time to read my thread. Recently i’ve been developing a simple iOS free-draw app and have run in to an issue that’s currently above my skill level to solve. I’ve been scouring the internet for days to try and come up with a solution but have had no luck thus far. Fortunately I have thought of a remedy for my applications lag issue, however I still need help as really do not know how to implement it.
A brief description of how this part of the program operates:
As the user moves his/her finger across the screen ( in a UIView labelled: view_Draw), touchesBegan() and touchesMoved() interpret start and end points for x&y coordinates and store these coordinates in an array (lines). The drawView is then forced to update via setNeedsDisplay().
class view_Draw: UIView {
// view_Draw Class Variables
var lastPoint: CGPoint!
var drawColor:UIColor = UIColor.redColor()
required init(coder aDecoder:NSCoder) {
super.init(coder: aDecoder)
self.backgroundColor = UIColor.whiteColor()
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
lastPoint = touches.anyObject()?.locationInView(self)
}
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
var newPoint = touches.anyObject()?.locationInView(self)
lines.append(Line(start: self.lastPoint, end: newPoint!, color: self.drawColor))
self.lastPoint = newPoint
setNeedsDisplay()
}
override func drawRect(rect: CGRect) {
var context = UIGraphicsGetCurrentContext()
//set stroke style
CGContextSetLineCap(context, kCGLineCapRound)
//set brush parameters
CGContextSetStrokeColorWithColor(context, drawColor.CGColor)
CGContextSetAlpha(context, 0.95)
CGContextSetLineWidth(context, brushSize)
for line in lines {
CGContextBeginPath(context)
CGContextMoveToPoint(context, line.start.x, line.start.y)
CGContextAddLineToPoint(context, line.end.x, line.end.y)
CGContextStrokePath(context)
// CGBitmapContextCreateImage(context)
// CGBitmapContextReleaseDataCallback()
}
}
}
A brief description of the issue:
As the user continues draws on the screen, I notice in the instruments panel that the CPU on thread 1 reaches to around 95% – 100%. This causes elements in my program (timers, drawing response) to begin lagging.
Actions taken to remedy issue:
I’ve experimented by disabling setNeedsDisplay() and have discovered that filling the lines array equates to only 10% of the overall CPU demand. From what I understand, this is because nothing from drawRect is being applied to the coordinates within the lines array.
Disabling CGContextStrokePath() and enabling setNeedsDisplay() increases CPU demand to 49%. I've interpreted this as the coordinates within the lines array are now being manipulated by drawRect- however are not actually being drawn onto the view.
This means that by forcing setNeedsDisplay() to update with CGContextStrokePath enabled, it’s hogging roughly 85% – 90% of the available processing power of thread 1.
I’ve also experimented with adding a timer to control how often setNeedsDisplay forces an update, but the results are less than acceptable. Drawing feels choppy with this in place.
Proposed remedy:
I think that the principle issue is that setNeedsDisplay() is redrawing the entirety of the lines array- what the user has drawn, constantly while touchesMoved() is being accessed.
I have looked into potentially using GCD to try and take some load off of thread 1, however after reading up on it, it seems as though this would not be a 'safe' way. From what i've understood, GCD and/or dispatch_async... shouldn't be used for elements that directly interact with UI elements.
I’ve seen on various forums that people have tackled similar issues by converting the existing path context to a bitmap and only updating the newly generated path with setNeedsDisplay.
I’m hoping that by approaching the issue this way, the setNeedsDisplay will not have to draw the entire array live every time as the previously drawn lines will have been converted into a static image. I have run out of ideas on how to even start implementing this.
As you can probably tell, I started learning Swift only a few weeks ago. I am doing my best to learn and approach problems in an effective manner. If you have any suggestions on how I should proceed with this, it would be greatly appreciated. Again, thank you for your help.
Answer based upon Aky's Smooth Freehand Drawing on iOS tutorial
By implementing the following code, a "buffer" of sorts is created that helps lessen the load on thread 1. In my initial tests, the load topped out at 46% while drawing as opposed to my original programs load that would top out at 95%-100%
LinearInterpView.swift
import UIKit
class LinearInterpView:UIView {
var path = UIBezierPath() //(3)
var bezierPath = UIBezierPath()
required init(coder aDecoder:NSCoder) { //(1)
super.init(coder: aDecoder)
self.multipleTouchEnabled = false //(2)
self.backgroundColor = UIColor.whiteColor()
path = bezierPath
path.lineWidth = 40.0
}
override func drawRect(rect: CGRect) { //(5)
UIColor.blackColor().setStroke()
path.stroke()
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
var touch:UITouch = touches.anyObject() as UITouch
var p:CGPoint = touch.locationInView(self)
path.moveToPoint(p)
}
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
var touch:UITouch = touches.anyObject() as UITouch
var p:CGPoint = touch.locationInView(self)
path.addLineToPoint(p) //(4)
setNeedsDisplay()
}
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
touchesMoved(touches, withEvent: event)
}
override func touchesCancelled(touches: NSSet!, withEvent event: UIEvent!) {
touchesEnded(touches, withEvent: event)
}
}
CachedLIView.swift
import UIKit
class CachedLIView:UIView {
var path = UIBezierPath()
var bezierPath = UIBezierPath() // needed to add this
var incrementalImage = UIImage() //(1)
var firstRun:Bool = true
required init(coder aDecoder:NSCoder) {
super.init(coder: aDecoder)
self.multipleTouchEnabled = false
self.backgroundColor = UIColor.whiteColor()
path = bezierPath
path.lineWidth = 40.0
}
override func drawRect(rect: CGRect) {
incrementalImage.drawInRect(rect) //(3)
path.stroke()
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
var touch:UITouch = touches.anyObject() as UITouch
var p:CGPoint = touch.locationInView(self)
path.moveToPoint(p)
}
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
var touch:UITouch = touches.anyObject() as UITouch
var p:CGPoint = touch.locationInView(self)
path.addLineToPoint(p)
self.setNeedsDisplay()
}
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) { //(2)
var touch:UITouch = touches.anyObject() as UITouch
var p:CGPoint = touch.locationInView(self)
path.addLineToPoint(p)
self.drawBitmap() //(3)
self.setNeedsDisplay()
path.removeAllPoints() //(4)
}
override func touchesCancelled(touches: NSSet!, withEvent event: UIEvent!) {
touchesEnded(touches, withEvent: event)
}
func drawBitmap() { //(3)
var rectPath = UIBezierPath()
UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0.0)
UIColor.blackColor().setStroke()
if(firstRun == true) {
rectPath = UIBezierPath(rect: self.bounds)
UIColor.whiteColor().setFill()
rectPath.fill()
firstRun = false
}
incrementalImage.drawAtPoint(CGPointZero)
path.stroke()
incrementalImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
Thank you again for your help, I hope this can be of use to others as well.
I wrote a couple of tutorials a few years ago about drawing in iOS using Objective-C. You might be able to translate this code into Swift without too many problems.
The first article talks about how you can render the on-screen graphics context into an image every time the user lifts his finger from the screen, so you can generate a new path from fresh from that point onwards and just draw it on top of that image.
Smooth Freehand Drawing on iOS
The second one has a GCD-based implementation that lets you do this rendering in the background.
Advanced Freehand Drawing Techniques