CALayer drawing outside of view - ios

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.

Related

Delay when adding lines to a UIBezierPath in CAShapeLayer

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

How to transform UIBezierPath with UITouch

i am trying to change the shape of UIBezierPath that i have created like in this tutorial:
[https://www.appcoda.com/bezier-paths-introduction/]
thats my code:
class Geometry: UIView {
//var path: UIBezierPath!
var path = UIBezierPath()
override func draw(_ rect: CGRect) {
createLine(x: 100, y: 100)
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.white
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func transformShape(x:CGFloat, y:CGFloat)
{
path.removeAllPoints()
createLine(x: x, y: y)
}
func createLine(x: CGFloat, y: CGFloat)
{
var path = UIBezierPath()
path.move(to: CGPoint(x: 0.0, y: 0.0))
path.addLine(to: CGPoint(x: x, y: y))
path.close()
path.fill()
path.stroke()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
let touch = touches.first
let location = (touch?.location(in: self))!;
transformShape(x:location.x, y:location.y)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
{
let touch = touches.first
let location = (touch?.location(in: self))!;
transformShape(x:location.x, y:location.y)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
{
let touch = touches.first
let location = (touch?.location(in: self))!;
transformShape(x:location.x, y:location.y)
}
}
i have added the the view in the viewcontroller like this:
import UIKit
var geo = Geometry()
let screenWidth: Int = 1024
let screenHeight: Int = 768
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
geo = Geometry(frame: CGRect(x: 0, y: 0, width: screenWidth, height: screenHeight))
self.view.addSubview(geo)
}
}
to make it easier to understand what i want to do i made this. i want to be able to move the end of both lines to the left and right on x without changing the y-postion
i have not found anything besides animation. but thats not what i want to accomplish. the top of the triangle should follow the movements of the finger. thanks
It certainly isn't difficult to do this in a crude way:
Here's how I'm doing that:
class MyView : UIView {
override init(frame: CGRect) {
super.init(frame:frame)
backgroundColor = .yellow
let g = UIPanGestureRecognizer(target: self, action: #selector(pan))
self.addGestureRecognizer(g)
}
#objc
func pan(_ g:UIGestureRecognizer) {
switch g.state {
case .changed:
let p = g.location(in: self)
self.apex.x = p.x
self.setNeedsDisplay()
default: break
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
lazy var apex = CGPoint(x:self.bounds.midX, y:self.bounds.midY)
override func draw(_ rect: CGRect) {
let con = UIGraphicsGetCurrentContext()!
con.move(to: CGPoint(x: 0, y: self.bounds.maxY))
con.addLine(to: self.apex)
con.addLine(to: CGPoint(x: self.bounds.maxX, y: self.bounds.maxY))
con.strokePath()
}
}
In that code, I'm simply setting the apex point to where the finger is (keeping the y-component constant, as you specify), and redrawing the whole bezier path. So everything from there is just a question of refinement — reducing latency, for example (which you can do by anticipating touches).

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.

How to clear UIView?

I am working on a drawing app where the user draws something on a view. On the same view controller I would like to create a button that would erase everything that was on the view and give it a blank slate. How would I do this? I added all of my code that is attached to the uiview to draw the object.
import uikit
class ViewController: UIViewController {
#IBOutlet weak var jj: draw!
#IBAction func enterScore(_ sender: Any) {
(view as? drawViewSwift)?.clear()
}}
import UIKit
struct stroke {
let startPoint: CGPoint
let endPoint: CGPoint
let color: CGColor
}
class drawViewSwift: subClassOFUIVIEW {
var isDrawing = false
var lastPoint : CGPoint!
var strokeColor : CGColor = UIColor.black.cgColor
var storkes = [stroke]()
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard !isDrawing else {return}
isDrawing = true
guard let touch = touches.first else {return}
let currentPoint = touch.location(in: self)
lastPoint = currentPoint
setNeedsDisplay()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard isDrawing else {return}
guard let touch = touches.first else {return}
setNeedsDisplay()
let currentPoint = touch.location(in: self)
print(currentPoint)
let sstroke = stroke(startPoint: lastPoint, endPoint: currentPoint, color: strokeColor)
storkes.append(sstroke)
lastPoint = currentPoint
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard isDrawing else {return}
isDrawing = false
guard let touch = touches.first else {return}
let currentPoint = touch.location(in: self)
let sstroke = stroke(startPoint: lastPoint, endPoint: currentPoint, color: strokeColor)
storkes.append(sstroke)
lastPoint = nil
setNeedsDisplay()
print(currentPoint)
}
private var clearing = false
func clear() {
clearing = true
setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
super.draw(rect)
defer { setNeedsDisplay() }
let context = UIGraphicsGetCurrentContext()
guard !clearing else {
context?.clear(bounds)
context?.setFillColor(backgroundColor?.cgColor ?? UIColor.clear.cgColor)
context?.fill(bounds)
storkes.removeAll()
clearing = false
return
}
context?.setLineWidth(10)
context?.setLineCap(.round)
for stroke in storkes {
context?.beginPath()
context?.move(to:stroke.startPoint)
context?.addLine(to: stroke.endPoint)
context?.setStrokeColor(stroke.color)
context?.strokePath()
}
}
func crase() {
storkes = []
strokeColor = UIColor.black.cgColor
setNeedsDisplay()
}
}
class subClassOFUIVIEW: UIView {
override func awakeFromNib() {
layer.shadowOpacity = 1
layer.shadowOffset = CGSize(width: 1, height: 1)
layer.shadowPath = CGPath(rect: bounds, transform: nil)
layer.shadowPath = CGPath(rect: bounds, transform: nil)
}
}
class MyViewController : UIViewController {
override func loadView() {
let view = drawViewSwift()
view.backgroundColor = .white
let button = UIButton()
button.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
button.setTitle("Clear", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(clear), for: .touchUpInside)
view.addSubview(button)
self.view = view
}
#objc func clear() {
(view as? drawViewSwift)?.clear()
}
}
You can add to your drawViewSwift class this code:
private var clearing = false
func clear() {
clearing = true
setNeedsDisplay()
}
At this point your drawRect should change to clear the view:
override func draw(_ rect: CGRect) {
super.draw(rect)
defer { setNeedsDisplay() }
let context = UIGraphicsGetCurrentContext()
guard !clearing else {
context?.clear(bounds)
context?.setFillColor(backgroundColor?.cgColor ?? UIColor.clear.cgColor)
context?.fill(bounds)
storkes.removeAll()
clearing = false
return
}
context?.setLineWidth(10)
context?.setLineCap(.round)
for stroke in storkes {
context?.beginPath()
context?.move(to:stroke.startPoint)
context?.addLine(to: stroke.endPoint)
context?.setStrokeColor(stroke.color)
context?.strokePath()
}
}
In the guard we will call the clerk method of the context which will reset the context to be at its initial state. This method will cause the context to redraw a transparent view. Since the view is now transparent we must redraw our canvas to be with a background color again. We do it by using the fill method after setting the fill color to be the background color of the view.
At this point all it's remaining to do is to clear our strokes array and switch back the value of clearing to false so that we are ready to draw again.
In your view controller, the clear button just have to call the clear method of this view.
Try it in playground:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
struct stroke {
let startPoint: CGPoint
let endPoint: CGPoint
let color: CGColor
}
class drawViewSwift: subClassOFUIVIEW {
var isDrawing = false
var lastPoint : CGPoint!
var strokeColor : CGColor = UIColor.black.cgColor
var storkes = [stroke]()
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard !isDrawing else {return}
isDrawing = true
guard let touch = touches.first else {return}
let currentPoint = touch.location(in: self)
lastPoint = currentPoint
setNeedsDisplay()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard isDrawing else {return}
guard let touch = touches.first else {return}
setNeedsDisplay()
let currentPoint = touch.location(in: self)
print(currentPoint)
let sstroke = stroke(startPoint: lastPoint, endPoint: currentPoint, color: strokeColor)
storkes.append(sstroke)
lastPoint = currentPoint
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard isDrawing else {return}
isDrawing = false
guard let touch = touches.first else {return}
let currentPoint = touch.location(in: self)
let sstroke = stroke(startPoint: lastPoint, endPoint: currentPoint, color: strokeColor)
storkes.append(sstroke)
lastPoint = nil
setNeedsDisplay()
print(currentPoint)
}
private var clearing = false
func clear() {
clearing = true
setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
super.draw(rect)
defer { setNeedsDisplay() }
let context = UIGraphicsGetCurrentContext()
guard !clearing else {
context?.clear(bounds)
context?.setFillColor(backgroundColor?.cgColor ?? UIColor.clear.cgColor)
context?.fill(bounds)
storkes.removeAll()
clearing = false
return
}
context?.setLineWidth(10)
context?.setLineCap(.round)
for stroke in storkes {
context?.beginPath()
context?.move(to:stroke.startPoint)
context?.addLine(to: stroke.endPoint)
context?.setStrokeColor(stroke.color)
context?.strokePath()
}
}
func crase() {
storkes = []
strokeColor = UIColor.black.cgColor
setNeedsDisplay()
}
}
class subClassOFUIVIEW: UIView {
override func awakeFromNib() {
layer.shadowOpacity = 1
layer.shadowOffset = CGSize(width: 1, height: 1)
layer.shadowPath = CGPath(rect: bounds, transform: nil)
layer.shadowPath = CGPath(rect: bounds, transform: nil)
}
}
class MyViewController : UIViewController {
override func loadView() {
let view = drawViewSwift()
view.backgroundColor = .white
let button = UIButton()
button.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
button.setTitle("Clear", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(clear), for: .touchUpInside)
view.addSubview(button)
self.view = view
}
#objc func clear() {
(view as? drawViewSwift)?.clear()
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
Your code to draw and to clear works. You should correct your viewController code and the layout structure you have choose. I think it's better to make a view dedicated to draw, so for example you could have 3 different objects to the main view, one for your label, another for your button and the last as your drawViewSwift. With this structure your main view can handle the constraints between objects and the other objects don't disturb the drawing (and obviusly the cleaning..):
Layout:
Output:
If number of strikes are high - Recommanded
If your strokes array is too big. I recommend to use below code
let rectangle = CGRect(x: 0, y: 0, width: self.height, height: self.height)
CGContextSetFillColorWithColor(context, UIColor.white.CGColor)
CGContextAddRect(context, rectangle)
CGContextDrawPath(context, .Fill)
If user hasn't drawn many strikes
Since you have all the current drawn strokes inside array strokes. below method will also work.
Just redraw above thus strokes with your view background color
override func clearView() {
let context = UIGraphicsGetCurrentContext()
context?.setLineWidth(10)
context?.setLineCap(.round)
for stroke in storkes {
context?.beginPath()
context?.move(to:stroke.startPoint)
context?.addLine(to: stroke.endPoint)
context?.setStrokeColor(UIColor.white.cgColor)
// Assuming your background color is white
context?.strokePath()
}
}

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

Resources