Drawing performance with Swift in Xcode for iOS app - ios

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

Related

let user to draw rectangle to select an area

I'm new in Swift and I'm trying to let the user draw a rectangle (touching and dragging) to select an area of an image just like when cropping but I don't want to crop I just want to know the CGRect the user created.
So far I have a .xib with a UIImage inside and its ViewController. I want to draw above the image but every tutorial I found about drawing is about subclassing UIView, override drawRect and put that as the xib class.
I figured it out. I just created a uiview and change its frame depending on the touches events
let overlay = UIView()
var lastPoint = CGPointZero
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
overlay.layer.borderColor = UIColor.blackColor().CGColor
overlay.backgroundColor = UIColor.clearColor().colorWithAlphaComponent(0.5)
overlay.hidden = true
self.view.addSubview(overlay)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
//Save original tap Point
if let touch = touches.first {
lastPoint = touch.locationInView(self.view)
}
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
//Get the current known point and redraw
if let touch = touches.first {
let currentPoint = touch.locationInView(view)
reDrawSelectionArea(lastPoint, toPoint: currentPoint)
}
}
func reDrawSelectionArea(fromPoint: CGPoint, toPoint: CGPoint) {
overlay.hidden = false
//Calculate rect from the original point and last known point
let rect = CGRectMake(min(fromPoint.x, toPoint.x),
min(fromPoint.y, toPoint.y),
fabs(fromPoint.x - toPoint.x),
fabs(fromPoint.y - toPoint.y));
overlay.frame = rect
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
overlay.hidden = true
//User has lift his finger, use the rect
applyFilterToSelectedArea(overlay.frame)
overlay.frame = CGRectZero //reset overlay for next tap
}

Removing old sections of curved line when drawing new sections in Swift

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.

Drawing curved lines without lagging?

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

locationInNode returning erroneous results

I have a subclass of SKNode called Player which pretty much consists of this: http://hub.ae/blog/2014/03/26/soft-body-physics-jellyusing-spritekit/ converted to swift (With a few changes). I've allowed the user to move the player node with his finger with the following code:
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
let touch = touches.first as! UITouch
player.runAction(SKAction.moveTo(touch.locationInNode(world), duration: 1))
}
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
let touch = touches.first as! UITouch
player.runAction(SKAction.moveTo(touch.locationInNode(world), duration: 1))
}
override func touchesCancelled(touches: Set<NSObject>!, withEvent event: UIEvent!) {
player.removeAllActions()
}
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
player.removeAllActions()
}
*As a note, the 'world' var you see is just another SKNode. 'player' is a child of this node.
This looks like it should work, however the node moves in very strange directions. This is an example of how it looks:
http://gyazo.com/87b0d09101bbbfd3ac0f2a3cdbf42e4c
How can I fix this? I found that settings the anchor point of the scene to 0.5 fixes this issue, however then the physics body of 'player' node gets messed up.
I had same problem but i can not remember if it is how i fixed it. Try changing this:
world > self
player.runAction(SKAction.moveTo(touch.locationInNode(world), duration: 1))
To:
player.runAction(SKAction.moveTo(touch.locationInNode(self), duration: 1))
If i am wrong let me know :)
EDIT
set anchor point to (0.5,0.5) and write those 2 methods to center on your node.
override func didSimulatePhysics() {
camera.position = CGPointMake(player.position.x, player.position.y);
centerOnNode(camera)
}
func centerOnNode(node: SKNode){
var positionInScene : CGPoint = convertPoint(node.position, fromNode: node.parent)
world.position = CGPointMake(world.position.x - positionInScene.x, world.position.y - positionInScene.y)
}

Performance issues pertaining to drawRect in iOS on Swift

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

Resources