Delay when adding lines to a UIBezierPath in CAShapeLayer - ios

I am trying to implement functionality to allow a user to draw in a UIImageView. My current code is as follows:
class DrawImageView: UIImageView {
// MARK: - Properties
private var lastPoint = CGPoint.zero
private var lineColor = UIColor.black
private var lineWidth: CGFloat = 10.0
private var opacity: CGFloat = 1.0
private var path: UIBezierPath?
private var swiped = false
private var tempShapeLayer: CAShapeLayer?
// MARK: - Initializers
override init(image: UIImage?) {
super.init(image: image)
isUserInteractionEnabled = true
clipsToBounds = true
}
#available(*, unavailable)
private init() {
fatalError("init() has not been implemented")
}
#available(*, unavailable)
private override init(frame: CGRect) {
fatalError("init(frame:) has not been implemented")
}
#available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - Draw Methods
extension DrawImageView {
private func drawLine(from fromPoint: CGPoint, to toPoint: CGPoint) {
guard let tempShapeLayer = tempShapeLayer,
let cgPath = tempShapeLayer.path
else { return }
let path = UIBezierPath(cgPath: cgPath)
path.move(to: fromPoint)
path.addLine(to: toPoint)
tempShapeLayer.path = path.cgPath
setNeedsDisplay()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
swiped = false
lastPoint = touch.location(in: self)
let shapeLayer = CAShapeLayer()
shapeLayer.lineCap = .round
shapeLayer.path = path?.cgPath
shapeLayer.strokeColor = lineColor.cgColor
shapeLayer.lineWidth = lineWidth
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.frame = bounds
let path = UIBezierPath()
shapeLayer.path = path.cgPath
layer.addSublayer(shapeLayer)
shapeLayer.fillColor = UIColor.red.cgColor
tempShapeLayer = shapeLayer
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
swiped = true
let currentPoint = touch.location(in: self)
drawLine(from: lastPoint, to: currentPoint)
lastPoint = currentPoint
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if !swiped {
// Draw a single point
drawLine(from: lastPoint, to: lastPoint)
}
}
}
This image view is embedded inside a scroll view:
class ZoomImageVC: UIViewController {
// MARK: - Properties
private let bag = DisposeBag()
// MARK: - Views
private let scrollView = UIScrollView()
// Important this is init with image immediately
private let backgroundImageView = DrawImageView(image: UIImage(named: "test.jpg"))
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
// This will update on rotate too
updateMinZoomScaleForSize(view.bounds.size)
}
// MARK: - Setup
private func setupViews() {
view.backgroundColor = .white
scrollView.delegate = self
view.addSubview(scrollView)
scrollView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
scrollView.rx.didZoom
.subscribe(with: self, onNext: { `self`, _ in
self.updateConstraintsForSize(self.view.bounds.size)
})
.disposed(by: bag)
scrollView.addSubview(backgroundImageView)
// This is DrawingImageView
backgroundImageView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
}
private func updateMinZoomScaleForSize(_ size: CGSize) {
let widthScale = size.width / backgroundImageView.bounds.width
let heightScale = size.height / backgroundImageView.bounds.height
let minScale = min(widthScale, heightScale)
scrollView.minimumZoomScale = minScale
scrollView.zoomScale = minScale
}
private func updateConstraintsForSize(_ size: CGSize) {
let yOffset = max(0, (size.height - backgroundImageView.frame.height) / 2.0)
let xOffset = max(0, (size.width - backgroundImageView.frame.width) / 2.0)
backgroundImageView.snp.remakeConstraints {
$0.leading.trailing.equalToSuperview().offset(xOffset)
$0.top.bottom.equalToSuperview().offset(yOffset)
}
view.layoutIfNeeded()
}
}
// MARK: - UIScrollViewDelegate
extension ZoomImageVC: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
backgroundImageView
}
}
The code works fine however there seems to be a small delay. For example if I start stroking a path there will be a slight delay until the path appears, continuing to draw the path seems to be smooth. I can scribble the path very fast and it keeps up. It's just the start of the drawing that lags.
If I just paint a single dot to the image view without dragging there also seems to be a small delay from me tapping to it appearing on the screen.
If I remove DrawImageView from the scroll view there seems to be no delay. Why would there be a delay when this is in a scroll view?

Ran your code and not sure what "delay" you're seeing.
However, you're doing a few things that you don't need to do...
You can use a single path and add lines as the touch moves... no need to moveTo / addLineTo every time.
Also, if you want the initial "dot" to show up as soon as the touch begins, you can moveTo / addLineTo the touch point on touchesBegan.
Here's your class, edited to (hopefully) get rid of the delay you're experiencing:
class DrawImageView: UIImageView {
// MARK: - Properties
private var lineColor = UIColor.black
private var lineWidth: CGFloat = 10.0
private var opacity: CGFloat = 1.0
// let's make these non-optional
private var drawingPath: UIBezierPath!
private var drawingShapeLayer: CAShapeLayer!
// MARK: - Initializers
override init(image: UIImage?) {
super.init(image: image)
isUserInteractionEnabled = true
clipsToBounds = true
}
#available(*, unavailable)
private init() {
fatalError("init() has not been implemented")
}
#available(*, unavailable)
private override init(frame: CGRect) {
fatalError("init(frame:) has not been implemented")
}
#available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
/// MARK: - Draw Methods
extension DrawImageView {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let thisPoint = touch.location(in: self)
// create new shape layer
drawingShapeLayer = CAShapeLayer()
drawingShapeLayer.lineCap = .round
drawingShapeLayer.strokeColor = lineColor.cgColor
drawingShapeLayer.lineWidth = lineWidth
drawingShapeLayer.fillColor = UIColor.clear.cgColor
drawingShapeLayer.frame = bounds
// create new path
drawingPath = UIBezierPath()
// move to touch point
drawingPath.move(to: thisPoint)
// if we want the line (an initial "dot")
// to show up immediately
// add line to same touch point
drawingPath.addLine(to: thisPoint)
// assign the shape layer path
drawingShapeLayer.path = drawingPath.cgPath
// add the layer
layer.addSublayer(drawingShapeLayer)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let thisPoint = touch.location(in: self)
// add line to existing path
drawingPath.addLine(to: thisPoint)
// update path of shape layer
drawingShapeLayer.path = drawingPath.cgPath
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// don't really need to do anything here
// unless you're taking some other action when
// touch ends
}
}

Related

CALayer drawing outside of view

I have this behaviour with this code:
import UIKit
class Page: UIView {
var bezierMemory = [BezierRecord]()
var currentBezier: UIBezierPath = UIBezierPath()
var firstPoint: CGPoint = CGPoint()
var previousPoint: CGPoint = CGPoint()
var morePreviousPoint: CGPoint = CGPoint()
var previousCALayer: CALayer = CALayer()
var pointCounter = 0
var selectedPen: Pen = Pen(width: 3.0, strokeOpacity: 1, strokeColor: .red, fillColor: .init(gray: 0, alpha: 0.5), isPencil: true, connectsToStart: true, fillPencil: true)
enum StandardPageSizes {
case A4, LEGAL, LETTER
}
var firstCALayer = true
var pointsTotal = 0
override init(frame: CGRect) {
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard let touch = touches.first else { return }
let point = touch.location(in: self)
firstPoint = point
pointCounter = 1
currentBezier = UIBezierPath()
currentBezier.lineWidth = selectedPen.width
selectedPen.getStroke().setStroke()
currentBezier.move(to: point)
previousPoint = point
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
guard let touch = touches.first else { return }
let point = touch.location(in: self)
pointCounter += 1
if (pointCounter == 3) {
let midpoint = CGPoint(x: (morePreviousPoint.x + point.x)/2.0, y: (morePreviousPoint.y + point.y)/2.0)
currentBezier.addQuadCurve(to: midpoint, controlPoint: morePreviousPoint)
let updatedCALayer = CAShapeLayer()
updatedCALayer.path = currentBezier.cgPath
updatedCALayer.lineWidth = selectedPen.width
updatedCALayer.opacity = selectedPen.strokeOpacity
updatedCALayer.strokeColor = selectedPen.getStroke().cgColor
updatedCALayer.fillColor = selectedPen.getFill()
if (firstCALayer) {
layer.addSublayer(updatedCALayer)
firstCALayer = false
} else {
layer.replaceSublayer(previousCALayer, with: updatedCALayer)
}
previousCALayer = updatedCALayer
pointCounter = 1
}
morePreviousPoint = previousPoint
previousPoint = point
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
guard let touch = touches.first else { return }
let point = touch.location(in: self)
if (pointCounter != 3) {
if (selectedPen.connectsToStart) {
currentBezier.addQuadCurve(to: firstPoint, controlPoint: previousPoint)
} else {
currentBezier.addQuadCurve(to: point, controlPoint: previousPoint)
}
let updatedCALayer = CAShapeLayer()
updatedCALayer.path = currentBezier.cgPath
updatedCALayer.lineWidth = selectedPen.width
updatedCALayer.opacity = selectedPen.strokeOpacity
updatedCALayer.strokeColor = selectedPen.getStroke().cgColor
updatedCALayer.fillColor = selectedPen.getFill()
if (firstCALayer) {
layer.addSublayer(updatedCALayer)
firstCALayer = false
} else {
// layer.setNeedsDisplay()
layer.replaceSublayer(previousCALayer, with: updatedCALayer)
}
}
firstCALayer = true
let bezierRecord = BezierRecord(bezier: currentBezier, strokeColor: selectedPen.getStroke(), fillColor: selectedPen.getFill(), strokeWidth: selectedPen.width)
bezierMemory.append(bezierRecord)
}
private func normPoint(point: CGPoint) -> CGPoint {
return CGPoint(x: point.x/frame.width, y: point.y/frame.height)
}
public class BezierRecord {
var bezier: UIBezierPath
var strokeColor: UIColor
var strokeWidth: CGFloat
var fillColor: CGColor
init(bezier: UIBezierPath, strokeColor: UIColor, fillColor: CGColor, strokeWidth: CGFloat) {
self.bezier = bezier
self.strokeColor = strokeColor
self.strokeWidth = strokeWidth
self.fillColor = fillColor
}
}
}
Really the only relevant parts are touchesMoved and touchesEnded, where CALayer's are dealt with. As you can see from the gif, I can draw outside the bounds of the Page (UIView) as long as I start drawing inside the bounds. I do not want this - what I would like is something where you can maintain a stroke outside of the bounds of the Page (as long as you start it on the Page), but the stroke wont appear outside of the Page. Any ideas?
EDIT: I should add that for these UIBezierCurves, (0, 0) is considered to be the top left of the Page (white), and not the entire view. Thus, for example, beziers that start on the page and continue on, are negative.
All you should need to do is set .clipsToBounds = true on the view.
One approach:
override init(frame: CGRect) {
super.init(frame: .zero)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
self.clipsToBounds = true
}
You could put self.clipsToBounds = true in both of the init funcs, but it is common practice (no pun intended) to add a "common init" func like this (it can be named whatever... this is how I do it). Frequently we have other "initial setup" code that we want called, and this avoids duplicating the code.

Touch drawing not working when UIPanGestureRecognizer implemented for view

I have screen to test the device touch screen with popping bubbles. And some imageView added in subviews of bubbles made of cross for them. Then user swipe over the bubbles to check the touch screen.
And I want drawing on the same view. When user swipes the finger over the bubbles, a line will be drawn. I have separate class for drawing and assign it to main parent view of controller.
If I remove code for UIPanGestureRecognizer then drawing works and there are no lags.
If I add gesture to view for popping the bubbles like this
view.addGestureRecognizer(gestureRecognizer)
Then there is a lag, and drawing doesn't work.
I want both things like popping bubbles and drawing on view.
The main problem of this gesture is when I add this in view, then drawing works without any lag but popping bubbles doesn't work.
let gestureRecognizer : UIPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognized(_:)))
gestureRecognizer.maximumNumberOfTouches = 1
gestureRecognizer.minimumNumberOfTouches = 1
view.addGestureRecognizer(gestureRecognizer)
Drawing view class
import UIKit
class DrawingView: UIView {
var drawColor = UIColor.black
var lineWidth: CGFloat = 5
private var lastPoint: CGPoint!
private var bezierPath: UIBezierPath!
private var pointCounter: Int = 0
private let pointLimit: Int = 128
private var preRenderImage: UIImage!
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
initBezierPath()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initBezierPath()
}
func initBezierPath() {
bezierPath = UIBezierPath()
bezierPath.lineCapStyle = CGLineCap.round
bezierPath.lineJoinStyle = CGLineJoin.round
}
// MARK: - Touch handling
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch: AnyObject? = touches.first
lastPoint = touch!.location(in: self)
pointCounter = 0
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch: AnyObject? = touches.first
let newPoint = touch!.location(in: self)
bezierPath.move(to: lastPoint)
bezierPath.addLine(to: newPoint)
lastPoint = newPoint
pointCounter += 1
if pointCounter == pointLimit {
pointCounter = 0
renderToImage()
setNeedsDisplay()
bezierPath.removeAllPoints()
}
else {
setNeedsDisplay()
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
pointCounter = 0
renderToImage()
setNeedsDisplay()
bezierPath.removeAllPoints()
}
override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
touchesEnded(touches!, with: event)
}
// MARK: - Pre render
func renderToImage() {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0.0)
if preRenderImage != nil {
preRenderImage.draw(in: self.bounds)
}
bezierPath.lineWidth = lineWidth
drawColor.setFill()
drawColor.setStroke()
bezierPath.stroke()
preRenderImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
// MARK: - Render
override func draw(_ rect: CGRect) {
super.draw(rect)
if preRenderImage != nil {
preRenderImage.draw(in: self.bounds)
}
bezierPath.lineWidth = lineWidth
drawColor.setFill()
drawColor.setStroke()
bezierPath.stroke()
}
// MARK: - Clearing
func clear() {
preRenderImage = nil
bezierPath.removeAllPoints()
setNeedsDisplay()
}
// MARK: - Other
func hasLines() -> Bool {
return preRenderImage != nil || !bezierPath.isEmpty
}
}

Draw on large UIView take a lot of memory

when I start to draw on large UIView ( width: 3700 , height: 40000 ), it takes a lot of memory
when app starts, memory is 150 MB and when start drawing on it( calling setNeedsDisplay method) take around 1 GB and app is gonna crash
class DrawingVc: UIViewController {
let contentView = DrawableView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.backgroundColor = .clear
self.view.addSubview(contentView)
contentView.frame = CGRect(x: 0, y: 0, width:view.frame.width, height:
view.frame.height * 50)
}
here is the code of custom view, as you can see, setNeedsDisplay runs on touchMoves
class DrawableView: UIView {
var mLastPath: UIBezierPath?
weak var scdelegate: DrawableViewDelegate?
var isDrawEnable = true
private var drawingLines : [UIBezierPath] = []
override init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
debugPrint("request draw")
drawLine()
}
private func drawLine() {
UIColor.blue.setStroke()
for line in drawingList {
line.lineWidth = 4
line.stroke()
line.lineCapStyle = .round
}
}
var drawingList = [UIBezierPath]()
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if touches.count == 2 {
return
}
let location = (touches.first?.location(in: self))!
mLastPath = UIBezierPath()
mLastPath?.move(to: location)
prevPoint = location
drawingList.append(mLastPath!)
}
var prevPoint: CGPoint?
var isFirst = true
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
debugPrint("touchesMoved: " , (touches.first?.location(in: self).x)! , (touches.first?.location(in: self).y)! )
if let coalescedtouches = event?.coalescedTouches(for: touches.first!)
{
for coalescedTouch in coalescedtouches
{
let locationInView = coalescedTouch.location(in: self)
if let prevPoint = prevPoint {
let midPoint = CGPoint( x: (locationInView.x + prevPoint.x) / 2, y: (locationInView.y + prevPoint.y) / 2)
if isFirst {
mLastPath?.addLine(to: midPoint)
}else {
mLastPath?.addQuadCurve(to: midPoint, controlPoint: prevPoint)
}
isFirst = false
} else {
mLastPath?.move(to: locationInView)
}
prevPoint = locationInView
}
}
setNeedsDisplay()
}
}
What makes this problem and how that fix?
Your view is larger than the largest possible screen on an iOS device, so I suppose your view is embedded in a scrollview. You should only draw the visible parts of your view. Unfortunately, this is not supported by UIView directly. You may take a look on CATiledLayer, which supports drawing of only visible parts of a layer, and it supports different levels of details for zoomed layers, too.

Swift - why is my Eraser tool pixilated

There are numerous questions about creating an eraser tool in CoreGraphics. I cannot find one that matches "pixilated".
Here's the situation. I'm playing with a simple drawing project. The pen tools work fine. The eraser tool is horribly pixilated. Here's a screen shot of what I mean:
Here's the drawing code I'm using (UPDATED):
// DrawingView
//
//
// Created by David DelMonte on 12/9/16.
// Copyright © 2016 David DelMonte. All rights reserved.
//
import UIKit
public protocol DrawingViewDelegate {
func didBeginDrawing(view: DrawingView)
func isDrawing(view: DrawingView)
func didFinishDrawing(view: DrawingView)
func didCancelDrawing(view: DrawingView)
}
open class DrawingView: UIView {
//initial settings
public var lineColor: UIColor = UIColor.black
public var lineWidth: CGFloat = 10.0
public var lineOpacity: CGFloat = 1.0
//public var lineBlendMode: CGBlendMode = .normal
//used for zoom actions
public var drawingEnabled: Bool = true
public var delegate: DrawingViewDelegate?
private var currentPoint: CGPoint = CGPoint()
private var previousPoint: CGPoint = CGPoint()
private var previousPreviousPoint: CGPoint = CGPoint()
private var pathArray: [Line] = []
private var redoArray: [Line] = []
var toolType: Int = 0
let π = CGFloat(M_PI)
private let forceSensitivity: CGFloat = 4.0
private struct Line {
var path: CGMutablePath
var color: UIColor
var width: CGFloat
var opacity: CGFloat
//var blendMode: CGBlendMode
init(path : CGMutablePath, color: UIColor, width: CGFloat, opacity: CGFloat) {
self.path = path
self.color = color
self.width = width
self.opacity = opacity
//self.blendMode = blendMode
}
}
override public init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.backgroundColor = UIColor.clear
}
override open func draw(_ rect: CGRect) {
let context : CGContext = UIGraphicsGetCurrentContext()!
for line in pathArray {
context.setLineWidth(line.width)
context.setAlpha(line.opacity)
context.setLineCap(.round)
switch toolType {
case 0: //pen
context.setStrokeColor(line.color.cgColor)
context.addPath(line.path)
context.setBlendMode(.normal)
break
case 1: //eraser
context.setStrokeColor(UIColor.clear.cgColor)
context.addPath(line.path)
context.setBlendMode(.clear)
break
case 3: //multiply
context.setStrokeColor(line.color.cgColor)
context.addPath(line.path)
context.setBlendMode(.multiply)
break
default:
break
}
context.beginTransparencyLayer(auxiliaryInfo: nil)
context.strokePath()
context.endTransparencyLayer()
}
}
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard drawingEnabled == true else {
return
}
self.delegate?.didBeginDrawing(view: self)
if let touch = touches.first as UITouch! {
//setTouchPoints(touch, view: self)
previousPoint = touch.previousLocation(in: self)
previousPreviousPoint = touch.previousLocation(in: self)
currentPoint = touch.location(in: self)
let newLine = Line(path: CGMutablePath(), color: self.lineColor, width: self.lineWidth, opacity: self.lineOpacity)
newLine.path.addPath(createNewPath())
pathArray.append(newLine)
}
}
override open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard drawingEnabled == true else {
return
}
self.delegate?.isDrawing(view: self)
if let touch = touches.first as UITouch! {
//updateTouchPoints(touch, view: self)
previousPreviousPoint = previousPoint
previousPoint = touch.previousLocation(in: self)
currentPoint = touch.location(in: self)
let newLine = createNewPath()
if let currentPath = pathArray.last {
currentPath.path.addPath(newLine)
}
}
}
override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard drawingEnabled == true else {
return
}
}
override open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
guard drawingEnabled == true else {
return
}
}
public func canUndo() -> Bool {
if pathArray.count > 0 {return true}
return false
}
public func canRedo() -> Bool {
return redoArray.count > 0
}
public func undo() {
if pathArray.count > 0 {
redoArray.append(pathArray.last!)
pathArray.removeLast()
}
setNeedsDisplay()
}
public func redo() {
if redoArray.count > 0 {
pathArray.append(redoArray.last!)
redoArray.removeLast()
}
setNeedsDisplay()
}
public func clearCanvas() {
pathArray = []
setNeedsDisplay()
}
private func createNewPath() -> CGMutablePath {
//print(#function)
let midPoints = getMidPoints()
let subPath = createSubPath(midPoints.0, mid2: midPoints.1)
let newPath = addSubPathToPath(subPath)
return newPath
}
private func calculateMidPoint(_ p1 : CGPoint, p2 : CGPoint) -> CGPoint {
//print(#function)
return CGPoint(x: (p1.x + p2.x) * 0.5, y: (p1.y + p2.y) * 0.5);
}
private func getMidPoints() -> (CGPoint, CGPoint) {
//print(#function)
let mid1 : CGPoint = calculateMidPoint(previousPoint, p2: previousPreviousPoint)
let mid2 : CGPoint = calculateMidPoint(currentPoint, p2: previousPoint)
return (mid1, mid2)
}
private func createSubPath(_ mid1: CGPoint, mid2: CGPoint) -> CGMutablePath {
//print(#function)
let subpath : CGMutablePath = CGMutablePath()
subpath.move(to: CGPoint(x: mid1.x, y: mid1.y))
subpath.addQuadCurve(to: CGPoint(x: mid2.x, y: mid2.y), control: CGPoint(x: previousPoint.x, y: previousPoint.y))
return subpath
}
private func addSubPathToPath(_ subpath: CGMutablePath) -> CGMutablePath {
//print(#function)
let bounds : CGRect = subpath.boundingBox
let drawBox : CGRect = bounds.insetBy(dx: -0.54 * lineWidth, dy: -0.54 * lineWidth)
self.setNeedsDisplay(drawBox)
return subpath
}
}
UPDATE:
I notice that each eraser touch is square. Please see the second image to show in more detail:
I then rewrote some code as suggested by Pranal Jaiswal:
override open func draw(_ rect: CGRect) {
print(#function)
let context : CGContext = UIGraphicsGetCurrentContext()!
if isEraserSelected {
for line in undoArray {
//context.beginTransparencyLayer(auxiliaryInfo: nil)
context.setLineWidth(line.width)
context.addPath(line.path)
context.setStrokeColor(UIColor.clear.cgColor)
context.setBlendMode(.clear)
context.setAlpha(line.opacity)
context.setLineCap(.round)
context.strokePath()
}
} else {
for line in undoArray {
context.setLineWidth(line.width)
context.setLineCap(.round)
context.addPath(line.path)
context.setStrokeColor(line.color.cgColor)
context.setBlendMode(.normal)
context.setAlpha(line.opacity)
context.strokePath()
}
}
}
I'm still getting the same result. I'd appreciate any more help.
I couldn't exactly look at ur code But I had done something similar in Swift 2.3 a while ago (I do understand u are looking at swift 3 but right now this is version that I have).
Here is how the drawing class works looks like.
import Foundation
import UIKit
import QuartzCore
class PRSignatureView: UIView
{
var drawingColor:CGColorRef = UIColor.blackColor().CGColor //Col
var drawingThickness:CGFloat = 0.5
var drawingAlpha:CGFloat = 1.0
var isEraserSelected: Bool
private var currentPoint:CGPoint?
private var previousPoint1:CGPoint?
private var previousPoint2:CGPoint?
private var path:CGMutablePathRef = CGPathCreateMutable()
var image:UIImage?
required init?(coder aDecoder: NSCoder) {
//self.backgroundColor = UIColor.clearColor()
self.isEraserSelected = false
super.init(coder: aDecoder)
self.backgroundColor = UIColor.clearColor()
}
override func drawRect(rect: CGRect)
{
self.isEraserSelected ? self.eraseMode() : self.drawingMode()
}
private func drawingMode()
{
if (self.image != nil)
{
self.image!.drawInRect(self.bounds)
}
let context:CGContextRef = UIGraphicsGetCurrentContext()!
CGContextAddPath(context, path)
CGContextSetLineCap(context, CGLineCap.Round)
CGContextSetLineWidth(context, self.drawingThickness)
CGContextSetStrokeColorWithColor(context, drawingColor)
CGContextSetBlendMode(context, CGBlendMode.Normal)
CGContextSetAlpha(context, self.drawingAlpha)
CGContextStrokePath(context);
}
private func eraseMode()
{
if (self.image != nil)
{
self.image!.drawInRect(self.bounds)
}
let context:CGContextRef = UIGraphicsGetCurrentContext()!
CGContextSaveGState(context)
CGContextAddPath(context, path);
CGContextSetLineCap(context, CGLineCap.Round)
CGContextSetLineWidth(context, self.drawingThickness)
CGContextSetBlendMode(context, CGBlendMode.Clear)
CGContextStrokePath(context)
CGContextRestoreGState(context)
}
private func midPoint (p1:CGPoint, p2:CGPoint)->CGPoint
{
return CGPointMake((p1.x + p2.x) * 0.5, (p1.y + p2.y) * 0.5)
}
private func finishDrawing()
{
UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0.0);
drawViewHierarchyInRect(self.bounds, afterScreenUpdates: true)
self.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
func clearSignature()
{
path = CGPathCreateMutable()
self.image = nil;
self.setNeedsDisplay();
}
// MARK: - Touch Delegates
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
path = CGPathCreateMutable()
let touch = touches.first!
previousPoint1 = touch.previousLocationInView(self)
currentPoint = touch.locationInView(self)
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first!
previousPoint2 = previousPoint1
previousPoint1 = touch.previousLocationInView(self)
currentPoint = touch.locationInView(self)
let mid1 = midPoint(previousPoint2!, p2: previousPoint1!)
let mid2 = midPoint(currentPoint!, p2: previousPoint1!)
let subpath:CGMutablePathRef = CGPathCreateMutable()
CGPathMoveToPoint(subpath, nil, mid1.x, mid1.y)
CGPathAddQuadCurveToPoint(subpath, nil, previousPoint1!.x, previousPoint1!.y, mid2.x, mid2.y)
CGPathAddPath(path, nil, subpath);
self.setNeedsDisplay()
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.touchesMoved(touches, withEvent: event)
self.finishDrawing()
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
self.touchesMoved(touches!, withEvent: event)
self.finishDrawing()
}
}
Source Code for test app I created using the above code
Edit: Converting few lines code to swift 3 as requested
subpath.move(to: CGPoint(x: mid1.x, y: mid1.y))
subpath.addQuadCurve(to:CGPoint(x: mid2.x, y: mid2.y) , control: CGPoint(x: previousPoint1!.x, y: previousPoint1!.y))
path.addPath(subpath)
Edit: In response to the updated Question
Here is the updated Drawing Class that must solve the issue for sure. https://drive.google.com/file/d/0B5nqEBSJjCriTU5oRXd5c2hRV28/view?usp=sharing
Issues addressed:
Line Struct did not hold the tool type associated. Whenever setNeedsDislpay() is called you redraw all the objects in pathArray and all Objects were getting redrawn with the current selected tool. I have added a new variable associatedTool to address the issue.
Use of function beginTransparencyLayer will set the blend mode to kCGBlendModeNormal. As this was common for all cases related to tooltype this was causing the mode to be set to normal. I have removed these two lines
//context.beginTransparencyLayer(auxiliaryInfo: nil)
//context.endTransparencyLayer()
Try this it has no error while erasing and it can be us for drawing erasing and clearing your screen. you can even increase or decrease ur size of the pencil and eraser. also u may change color accordingly.
hope this is helpfull for u.....
import UIKit
class DrawingView: UIView {
var lineColor:CGColor = UIColor.black.cgColor
var lineWidth:CGFloat = 5
var drawingAlpha:CGFloat = 1.0
var isEraserSelected: Bool
private var currentPoint:CGPoint?
private var previousPoint1:CGPoint?
private var previousPoint2:CGPoint?
private var path:CGMutablePath = CGMutablePath()
var image:UIImage?
required init?(coder aDecoder: NSCoder) {
//self.backgroundColor = UIColor.clearColor()
self.isEraserSelected = false
super.init(coder: aDecoder)
self.backgroundColor = UIColor.clear
}
override func draw(_ rect: CGRect)
{
self.isEraserSelected ? self.eraseMode() : self.drawingMode()
}
private func drawingMode()
{
if (self.image != nil)
{
self.image!.draw(in: self.bounds)
}
let context:CGContext = UIGraphicsGetCurrentContext()!
context.addPath(path)
context.setLineCap(CGLineCap.round)
context.setLineWidth(self.lineWidth)
context.setStrokeColor(lineColor)
context.setBlendMode(CGBlendMode.normal)
context.setAlpha(self.drawingAlpha)
context.strokePath();
}
private func eraseMode()
{
if (self.image != nil)
{
self.image!.draw(in: self.bounds)
}
let context:CGContext = UIGraphicsGetCurrentContext()!
context.saveGState()
context.addPath(path);
context.setLineCap(CGLineCap.round)
context.setLineWidth(self.lineWidth)
context.setBlendMode(CGBlendMode.clear)
context.strokePath()
context.restoreGState()
}
private func midPoint (p1:CGPoint, p2:CGPoint)->CGPoint
{
return CGPoint(x: (p1.x + p2.x) * 0.5, y: (p1.y + p2.y) * 0.5);
}
private func finishDrawing()
{
UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0.0);
drawHierarchy(in: self.bounds, afterScreenUpdates: true)
self.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
func clearSignature()
{
path = CGMutablePath()
self.image = nil;
self.setNeedsDisplay();
}
// MARK: - Touch Delegates
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
path = CGMutablePath()
let touch = touches.first!
previousPoint1 = touch.previousLocation(in: self)
currentPoint = touch.location(in: self)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
previousPoint2 = previousPoint1
previousPoint1 = touch.previousLocation(in: self)
currentPoint = touch.location(in: self)
let mid1 = midPoint(p1: previousPoint2!, p2: previousPoint1!)
let mid2 = midPoint(p1: currentPoint!, p2: previousPoint1!)
let subpath:CGMutablePath = CGMutablePath()
subpath.move(to: CGPoint(x: mid1.x, y: mid1.y), transform: .identity)
subpath.addQuadCurve(to: CGPoint(x: mid2.x, y: mid2.y), control: CGPoint(x: (previousPoint1?.x)!, y: (previousPoint1?.y)!))
path.addPath(subpath, transform: .identity)
self.setNeedsDisplay()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.touchesMoved(touches, with: event)
self.finishDrawing()
}
override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
self.touchesMoved(touches!, with: event)
self.finishDrawing()
}
}

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.

Resources