Adding undo function to drawing app - swift

I followed a raywenderlich tutorial on using the UIKit to make a drawing app. I'm now trying to add in the functionality to undo the last stroke. Ideally I would like to undo up to 10ish strokes. I'm trying to figure out what is the best way to go about doing this. I was thinking of creating another ImageView which has only the last stroke and making the ImageView.image = nil when the user presses back. In the code from the tutorial there's something similar to this. When the touches end, the newest stroke is merged onto the imageview with all of the old ones at the right opacity. I'm not really sure how I could add this third (and potentially more) imageivews to this code to make it work. Any ideas / a better way to go about this? Code for touchesEnded is below.
Code
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
if !swiped {
// draw a single point
drawLineFrom(lastPoint, toPoint: lastPoint)
}
// Merge tempImageView into mainImageView
UIGraphicsBeginImageContext(mainImageView.frame.size)
mainImageView.image?.drawInRect(CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height), blendMode: kCGBlendModeNormal, alpha: 1.0)
tempImageView.image?.drawInRect(CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height), blendMode: kCGBlendModeNormal, alpha: opacity)
mainImageView.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
tempImageView.image = nil
}

#SpaceShroomies: I found/came up with a great solution..
I used parts from nsHiptser, and from the Raywenderlich tutorial.
And here i my solution:
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
//undoManager
let undo: UIImageView = undoManager?.prepareWithInvocationTarget(Temp2Image) as! UIImageView
UIGraphicsBeginImageContext(Temp2Image.frame.size)
Temp2Image.image?.drawInRect(CGRect(x: 0, y: 0, width: Temp2Image.frame.size.width, height: Temp2Image.frame.size.height), blendMode: kCGBlendModeNormal, alpha: 1.0)
TempImage.image?.drawInRect(CGRect(x: 0, y: 0, width: TempImage.frame.size.width, height: TempImage.frame.size.height), blendMode: kCGBlendModeNormal, alpha: opacity)
undo.image = Temp2Image.image
Temp2Image.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
TempImage.image = nil
}
The important thing here is to set, what UNDO should go back to I.e. undo.image = Temp2Image.image otherwise it will not change "it" back

So I managed to get an undo function working for one step backwards by having a UIImageView that stores the last stroke before it's added to the rest of the strokes when the next stroke is complete... Would there be something wrong with using the same technique to go backwards 10 steps? I would need 10 UIImageViews... Would that be inefficient / cause crashing? The code is below:
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
if !swiped {
// draw a single point
drawLineFrom(lastPoint, toPoint: lastPoint)
}
UIGraphicsBeginImageContext(mainImageView.frame.size)
mainImageView.image?.drawInRect(CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height), blendMode: kCGBlendModeNormal, alpha: 1.0)
undo1.image?.drawInRect(CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height), blendMode: kCGBlendModeNormal, alpha: 1.0)
mainImageView.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
undo1.image = nil
UIGraphicsBeginImageContext(undo1.frame.size)
undo1.image?.drawInRect(CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height), blendMode: kCGBlendModeNormal, alpha: 1.0)
tempImageView.image?.drawInRect(CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height), blendMode: kCGBlendModeNormal, alpha: opacity)
undo1.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
tempImageView.image = nil
}

Related

Erasing pixels from image and then reverting them back

I'm creating an eraser app where i have a picture and the user can erase the background with the following func :
func eraseImage( image: UIImage ,line: [CGPoint] ,brushSize: CGFloat) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(proxy.size, false, 0)
let context = UIGraphicsGetCurrentContext()
let rect = AVMakeRect(aspectRatio: image.size, insideRect: CGRect(x: 0, y:0, width: proxy.size.width, height: proxy!.size.height))
//lassoImageView.image?.draw(in: calculateRectOfImageInImageView(imageView: lassoImageView))
image.draw(in: rect, blendMode: .normal, alpha: 1)
context?.move(to: CGPoint(x: line.first!.x - 50, y: line.first!.y - 50))
for pointIndex in 1..<line.count {
context?.addLine(to: CGPoint(x: line[pointIndex].x - 50, y: line[pointIndex].y - 50))
}
context?.setBlendMode(.clear)
context?.setLineCap(.round)
context?.setLineWidth(brushSize)
context?.setShadow(offset: CGSize(width: 0, height: 0), blur: 8)
context?.strokePath()
if let img = UIGraphicsGetImageFromCurrentImageContext() {
return img
}
UIGraphicsEndImageContext()
return nil
}
I'm struggling with figuring out how can i add a drawing function where the user can correct it's earaser mistake and draw back some parts.
I'm thinking of drawing on the edited image the original image with a mask of the path which tracks the cgpoints of the users location. is that possible?
I would suggest setting up your image with a second CALayer installed as a mask. Install an image view into the mask layer's contents, and draw with clear/opaque colors into the mask to mask/expose pixels from the image layer.

Drawing a simple line chart using Swift

I am writing a Swift program using Xcode 9.2 for the MacOS with the objecting of displaying a simple line chart with three points.
The program's objective is to create a line graph using apple's core graphics. It is a horizontal line graph with three points, one at the center and at each end. If the mouse pointer is over the center point I want some text to display like "xyz" and when the mouse moves away the text would disappear. The end points will have labels containing numbers but for now let's call them "min" and "max".
Using XCode's playground, so far I have managed to accomplish to display a very clunky line graph using the following code:
class MyView : NSView {
override func draw(_ rect: NSRect) {
NSColor.green.setFill()
var path = NSBezierPath(rect: self.bounds)
path.fill()
let leftDot = NSRect(x: 0, y: 0, width: 10, height: 20)
path = NSBezierPath(ovalIn: leftDot)
NSColor.black.setFill()
path.fill()
let rightDot = NSRect(x: 90, y: 0, width: 10, height: 20)
path = NSBezierPath(ovalIn: rightDot)
NSColor.black.setFill()
path.fill()
let centerDot = NSRect(x: 50, y: 0, width: 10, height: 20)
path = NSBezierPath(ovalIn: centerDot)
NSColor.black.setFill()
path.fill()
}
}
let viewRect = NSRect(x: 0, y: 0, width: 100, height: 10)
let myEmptyView = MyView(frame: viewRect)
The code produces a clunky looking line graph as follows:
The chart below is from Excel and this line chart is much cleaner and is what I am looking for but I want the line to be horizontal instead of diagonal.
Thanks in advance for your help.
To draw a horizontal line similar to the sample image you provided, I took your sample code and modified it. This would give this result:
The code would look like this then:
override func draw(_ rect: NSRect) {
NSColor(red: 103/255, green: 146/255, blue: 195/255, alpha: 1).set()
let line = NSBezierPath()
line.move(to: NSMakePoint(10, 5))
line.line(to: NSMakePoint(90, 5))
line.lineWidth = 2
line.stroke()
NSColor(red: 163/255, green: 189/255, blue: 218/255, alpha: 1).setFill()
let origins = [NSPoint(x: 10, y: 1),
NSPoint(x: 50, y: 1),
NSPoint(x: 90, y: 1)]
let size = NSSize(width: 8, height: 8)
for origin in origins {
let quad = NSBezierPath(roundedRect: NSRect(origin: origin, size: size), xRadius: 2, yRadius: 2)
quad.fill()
quad.stroke()
}
}
Update for iOS
Due to a request in the comments section here the iOS variant:
override func draw(_ rect: CGRect) {
UIColor(red: 103/255, green: 146/255, blue: 195/255, alpha: 1).set()
let line = UIBezierPath()
line.move(to: CGPoint(x: 10, y:5))
line.addLine(to: CGPoint(x: 90, y:5))
line.lineWidth = 2
line.stroke()
UIColor(red: 163/255, green: 189/255, blue: 218/255, alpha: 1).setFill()
let origins = [CGPoint(x: 10, y: 1),
CGPoint(x: 50, y: 1),
CGPoint(x: 90, y: 1)]
let size = CGSize(width: 8, height: 8)
for origin in origins {
let quad = UIBezierPath.init(roundedRect: CGRect(origin: origin, size: size), cornerRadius: 2)
quad.fill()
quad.stroke()
}
}
Alternatively, you could add a CAShapeLayer sublayer whose path property is a UIBezierPath to the UIView.

merging imageviews in drawing app swift

I am working on drawing app. I have three image views -
imageView - Contains base Image
tempImageView - for drawing annotations. drawLineFrom function takes a point and draw then lines on tempImageView
func drawLineFrom(fromPoint: CGPoint, toPoint: CGPoint)
{
//print("drawLineFrom")
let mid1 = CGPoint(x:(prevPoint1.x + prevPoint2.x)*0.5, y:(prevPoint1.y + prevPoint2.y)*0.5)
let mid2 = CGPoint(x:(toPoint.x + prevPoint1.x)*0.5, y:(toPoint.y + prevPoint1.y)*0.5)
UIGraphicsBeginImageContextWithOptions(self.tempImageView.boundsSize, false, 0.0)
if let context = UIGraphicsGetCurrentContext()
{
tempImageView.image?.draw(in: CGRect(x: 0, y: 0, width: self.tempImageView.frame.size.width, height: self.tempImageView.frame.size.height))
let annotaionPath = UIBezierPath()
annotaionPath.move(to: CGPoint(x: mid1.x, y: mid1.y))
annotaionPath.addQuadCurve(to: CGPoint(x:mid2.x,y:mid2.y), controlPoint: CGPoint(x:prevPoint1.x,y:prevPoint1.y))
annotaionPath.lineCapStyle = CGLineCap.round
annotaionPath.lineJoinStyle = CGLineJoin.round
annotaionPath.lineWidth = editorPanelView.brushWidth
context.setStrokeColor(editorPanelView.drawingColor.cgColor)
annotaionPath.stroke()
tempImageView.image = UIGraphicsGetImageFromCurrentImageContext()
tempImageView.alpha = editorPanelView.opacity
UIGraphicsEndImageContext()
}
}
drawingImageView - after each touchesEnded method I am merging tempImageView with drawingImageView and setting tempImageView.image = nil .
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
{
isDrawing = false
if !swiped
{
drawLineFrom(fromPoint: lastPoint, toPoint: lastPoint)
}
annotationArray.append(annotationsPoints)
annotationsPoints.removeAll()
// Merge tempImageView into drawingImageView
UIGraphicsBeginImageContext(drawingImageView.frame.size)
drawingImageView.image?.draw(in: CGRect(x: 0, y: 0, width: drawingImageView.frame.size.width, height: drawingImageView.frame.size.height), blendMode: CGBlendMode.normal, alpha: 1.0)
tempImageView.image?.draw(in: CGRect(x: 0, y: 0, width: drawingImageView.frame.size.width, height: drawingImageView.frame.size.height), blendMode: CGBlendMode.normal, alpha: editorPanelView.opacity)
drawingImageView.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
tempImageView.image = nil
}
When save button clicked,
let drawingImage = self.drawingImageView.image
let combinedImage = self.imageView.combineWithOverlay(overlayImageView: self.drawingImageView)
and I am saving combinedImage.
Problem is, when I merge tempImage view with drawing image view, the annotations get blurred.I want to maintain same clarity. I am not able to find any solution for this. Any help (even if it's just a kick in the right direction) would be appreciated.
I think the issue is with using UIGraphicsBeginImageContext(drawingImageView.frame.size).
The default scale it uses is 1.0 so if you're using a retina screen, it will cause the content to be scaled up 2 or 3 times causing the blurry appearance.
You should use UIGraphicsBeginImageContextWithOptions like you have in drawLineFrom with a scale of 0.0 which will default to the screens scale.

Mask VisualEffect blur with drawn image

So in my swift app I'm allowing the user to paint with there finger on touch. I'm using cgcontext to do this. After the user lifts the finger and the touch ends I am dynamically adding a visualeffect view on top of the shape with the same height and width of the drawn shape. What i want to do next is use the shape as a mask for visualeffect view. The problem right now is if i try to mask the visualeffect view with the shape. the masked view does not show unless the origin point of the shape is (0,0). Here is a link to how i'm currently attempting to implementing this (https://pastebin.com/JaM9kx4G)
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
swiped = false
if let touch = touches.first as? UITouch {
if (touch.view == sideView){
return
}
tempPath = UIBezierPath()
lastPoint = touch.location(in: view)
tempPath.move(to:lastPoint)
}
}
func drawLineFrom(fromPoint: CGPoint, toPoint: CGPoint) {
tempPath.addLine(to: CGPoint(x: toPoint.x, y: toPoint.y))
UIGraphicsBeginImageContext(view.frame.size)
let context = UIGraphicsGetCurrentContext()
otherImageView.image?.draw(in: CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height))
// 2
context!.move(to: CGPoint(x:fromPoint.x,y:fromPoint.y))
context!.addLine(to: CGPoint(x:toPoint.x, y:toPoint.y))
// 3
context!.setLineCap(CGLineCap.round)
context!.setLineWidth(brushWidth)
context!.setStrokeColor(red: red, green: green, blue: blue, alpha: 1.0)
context!.setBlendMode(CGBlendMode.normal)
// 4
context!.strokePath()
// 5
otherImageView.image = UIGraphicsGetImageFromCurrentImageContext()
otherImageView.alpha = opacity
UIGraphicsEndImageContext()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
swiped = true
if let touch = touches.first as? UITouch {
let currentPoint = touch.location(in: view)
drawLineFrom(fromPoint: lastPoint, toPoint: currentPoint)
// 7
lastPoint = currentPoint
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
tempPath.close()
let tempImage = UIImageView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height))
UIGraphicsBeginImageContext(tempImage.frame.size)
tempImage.image?.draw(in: CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height), blendMode: CGBlendMode.normal, alpha: 1.0)
otherImageView.image?.draw(in: CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height), blendMode: CGBlendMode.normal, alpha: opacity)
let context = UIGraphicsGetCurrentContext()
let image = UIGraphicsGetImageFromCurrentImageContext()
tempImage.image = image
otherImageView.image = nil
imageView.addSubview(tempImage)
let blur = VisualEffectView()
blur.frame = CGRect(x:tempPath.bounds.origin.x,y:tempPath.bounds.origin.y, width:tempPath.bounds.width, height:tempPath.bounds.height)
blur.blurRadius = 5
blur.layer.mask = tempImage.layer
}

Drawing on UIImage in UIScrollView

The Problem
This is going to sound crazy. I'm making a drawing app and I want users to be able to draw on images that are bigger or smaller than the screen. So when the user selects an image from his photo library it is put into an image view in a scroll view. The user draws on image views that are the same dimensions as the selected image and in another scroll view on top of the other one. The scrolling of the two scroll views is synchronized so when you draw then scroll the drawing appears to be above the image (in the right place). For some reason however, when the user selects a long image (let's say 400 x 2000), the drawing works at the top of the image, but when you scroll down to draw, the lines you draw go to the top. I can't figure out what's going wrong... My code is below.
About The Code
cameraStill is the image view containing the image
drawable is the height of the image
myScroll is the scroll view for the image
mainImageView, tempImageView, undo1, undo2, undo3 are the drawing layers
drawScroll is the scroll view for the drawing layers
Image Selection
func imagePickerController(picker: UIImagePickerController, didFinishPickingImage image: UIImage!, editingInfo: [NSObject : AnyObject]!) {
self.dismissViewControllerAnimated(true, completion: { () -> Void in
})
if (image != nil) {
self.cameraStill.contentMode = UIViewContentMode.ScaleAspectFit
cameraStill.frame = CGRectMake(0, 0, screenWidth, screenWidth*(image.size.height/image.size.width))
// change uiimageivews size
mainImageView.frame = CGRectMake(0, 0, screenWidth, screenWidth*(image.size.height/image.size.width))
tempImageView.frame = CGRectMake(0, 0, screenWidth, screenWidth*(image.size.height/image.size.width))
undo1.frame = CGRectMake(0, 0, screenWidth, screenWidth*(image.size.height/image.size.width))
undo2.frame = CGRectMake(0, 0, screenWidth, screenWidth*(image.size.height/image.size.width))
undo3.frame = CGRectMake(0, 0, screenWidth, screenWidth*(image.size.height/image.size.width))
drawable = screenWidth*(image.size.height/image.size.width)
myScroll.contentSize = CGSize(width: screenWidth,height: screenWidth*(image.size.height/image.size.width))
drawScroll.contentSize = CGSize(width: screenWidth,height: screenWidth*(image.size.height/image.size.width))
if (screenWidth*(image.size.height/image.size.width) > (screenHeight-130)) {
myScroll.scrollEnabled = true
drawScroll.scrollEnabled = true
}
else {
myScroll.scrollEnabled = false
drawScroll.scrollEnabled = false
cameraStill.center = CGPoint(x: screenWidth/2, y: (screenHeight-130)/2)
mainImageView.center = CGPoint(x: screenWidth/2, y: (screenHeight-130)/2)
tempImageView.center = CGPoint(x: screenWidth/2, y: (screenHeight-130)/2)
undo1.center = CGPoint(x: screenWidth/2, y: (screenHeight-130)/2)
undo2.center = CGPoint(x: screenWidth/2, y: (screenHeight-130)/2)
undo3.center = CGPoint(x: screenWidth/2, y: (screenHeight-130)/2)
}
self.camera!.stopCamera()
}
//drawView.alpha = 1.0
}
Drawing
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
println("began")
if (drawingEnabled == true) {
c1 = 3
closeAllExtras()
swiped = false
if let touch = touches.first as? UITouch {
lastPoint = touch.locationInView(self.view)
}
}
}
func drawLineFrom(fromPoint: CGPoint, toPoint: CGPoint) {
//if (fromPoint.y > 50 && fromPoint.y < screenHeight-80 && toPoint.y > 50 && toPoint.y < screenHeight-80) {
// 1
UIGraphicsBeginImageContext(CGSize(width: view.frame.size.width,height: drawable))
let context = UIGraphicsGetCurrentContext()
tempImageView.image?.drawInRect(CGRect(x: 0, y: 0, width: view.frame.size.width, height: drawable))
// 2
CGContextMoveToPoint(context, fromPoint.x, fromPoint.y)
CGContextAddLineToPoint(context, toPoint.x, toPoint.y)
// 3
CGContextSetLineCap(context, kCGLineCapRound)
CGContextSetLineWidth(context, brushWidth)
CGContextSetRGBStrokeColor(context, red, green, blue, 1.0)
CGContextSetBlendMode(context, kCGBlendModeNormal)
// 4
CGContextStrokePath(context)
// 5
tempImageView.image = UIGraphicsGetImageFromCurrentImageContext()
tempImageView.alpha = opacity
UIGraphicsEndImageContext()
//}
}
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
// 6
if (drawingEnabled == true) {
swiped = true
if let touch = touches.first as? UITouch {
let currentPoint = touch.locationInView(view)
drawLineFrom(lastPoint, toPoint: currentPoint)
// 7
lastPoint = currentPoint
}
}
}
func mergeViewContext(v1 : UIImageView, v2: UIImageView) {
UIGraphicsBeginImageContext(v1.frame.size)
v1.image?.drawInRect(CGRect(x: 0, y: 0, width: view.frame.size.width, height: drawable), blendMode: kCGBlendModeNormal, alpha: 1.0)
v2.image?.drawInRect(CGRect(x: 0, y: 0, width: view.frame.size.width, height: drawable), blendMode: kCGBlendModeNormal, alpha: 1.0)
v1.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
v2.image = nil
}
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
if (drawingEnabled == true) {
if !swiped {
// draw a single point
drawLineFrom(lastPoint, toPoint: lastPoint)
}
mergeViewContext(mainImageView, v2: undo1)
undo1.image = undo2.image
undo2.image = nil
undo2.image = undo3.image
undo3.image = nil
UIGraphicsBeginImageContext(undo3.frame.size)
undo3.image?.drawInRect(CGRect(x: 0, y: 0, width: view.frame.size.width, height: drawable), blendMode: kCGBlendModeNormal, alpha: 1.0)
tempImageView.image?.drawInRect(CGRect(x: 0, y: 0, width: view.frame.size.width, height: drawable), blendMode: kCGBlendModeNormal, alpha: opacity)
undo3.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
tempImageView.image = nil
}
Synching Both Scroll Views
func scrollViewDidScroll(scrollView: UIScrollView) {
if (scrollView == drawScroll) {
var offset = scrollView.contentOffset
myScroll.setContentOffset(offset, animated: false)
}
}
I got it to work by correcting the following values with the offset of the scroll view. However, I get some blurring for long images and a very strange bug with short ones. No idea what's wrong.
CGContextMoveToPoint(context, fromPoint.x, fromPoint.y)
CGContextAddLineToPoint(context, toPoint.x, toPoint.y)