I need to draw / doodle a line in UIImage like the picture above , I see a lot of tutorials doodle line on the UIView but not in the UIImage.
after the user doodle on the Image, I want to save it as the new image (Image that has lines). how do I do that in Swift?
i just can find draw line on UIView not in the UIImage
the for drawing on UIView is like this one in this youtube tutorial http://youtube.com/watch?v=gbFHqHHApC4 , I change it to inherit DrawView to UIImageView
struct Stroke {
let startPoint : CGPoint
let endPoint : CGPoint
let strokeColor : CGColor
}
class DrawView : UIImageView {
var isDrawing = false
var lastPoint : CGPoint!
var strokeColor : CGColor! = UIColor.black.cgColor
var strokes = [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
print(currentPoint)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard isDrawing else { return}
guard let touch = touches.first else {return}
let currentPoint = touch.location(in: self)
let stroke = Stroke(startPoint: lastPoint, endPoint: currentPoint, strokeColor: strokeColor)
strokes.append(stroke)
lastPoint = currentPoint
setNeedsDisplay()
print(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 stroke = Stroke(startPoint: lastPoint, endPoint: currentPoint, strokeColor: strokeColor)
strokes.append(stroke)
setNeedsDisplay()
lastPoint = nil
print(currentPoint)
}
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
context?.setLineWidth(5)
context?.setLineCap(.round)
for stroke in strokes {
context?.beginPath()
context?.move(to: stroke.startPoint)
context?.addLine(to: stroke.endPoint)
context?.setStrokeColor(stroke.strokeColor)
context?.strokePath()
}
}
func erase() {
strokes = []
strokeColor = UIColor.black.cgColor
setNeedsDisplay()
}
}
I have assigned the UIImage in storyboard to use custom class "DrawView" like my code above, but I don't know why the lines doesn't appear on my UIImage
Details
Xcode 10.2.1 (10E1001), Swift 5
Solution
import UIKit
// https://stackoverflow.com/a/41009006/4488252
class DrawnImageView: UIImageView {
private lazy var path = UIBezierPath()
private lazy var previousTouchPoint = CGPoint.zero
private lazy var shapeLayer = CAShapeLayer()
override func awakeFromNib() {
super.awakeFromNib()
setupView()
}
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func setupView(){
layer.addSublayer(shapeLayer)
shapeLayer.lineWidth = 4
shapeLayer.strokeColor = UIColor.white.cgColor
isUserInteractionEnabled = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
if let location = touches.first?.location(in: self) { previousTouchPoint = location }
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
if let location = touches.first?.location(in: self) {
path.move(to: location)
path.addLine(to: previousTouchPoint)
previousTouchPoint = location
shapeLayer.path = path.cgPath
}
}
}
// https://stackoverflow.com/a/40953026/4488252
extension UIView {
var screenShot: UIImage? {
let scale = UIScreen.main.scale
UIGraphicsBeginImageContextWithOptions(layer.frame.size, false, scale)
if let context = UIGraphicsGetCurrentContext() {
layer.render(in: context)
let screenshot = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return screenshot
}
return nil
}
}
Usage
Add DrawnImageView to your root (parent) view (drawing by touch will be enabled automatically)
To save UIImage use drawingImageView.screenShot
Full sample
Do not forget to add the solution code here
import UIKit
class ViewController: UIViewController {
fileprivate weak var savedImageView: UIImageView?
fileprivate weak var drawnImageView: UIImageView?
override func viewDidLoad() {
super.viewDidLoad()
let drawnImageView = addImageView(image: UIImage(named: "swift")) as DrawnImageView
drawnImageView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
drawnImageView.heightAnchor.constraint(equalToConstant: UIScreen.main.bounds.height/3).isActive = true
self.drawnImageView = drawnImageView
let button = UIButton()
button.setTitle("Save Image", for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitleColor(.blue, for: .normal)
view.addSubview(button)
button.topAnchor.constraint(equalTo: drawnImageView.bottomAnchor, constant: 60).isActive = true
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.heightAnchor.constraint(equalToConstant: 44).isActive = true
button.addTarget(self, action: #selector(saveImageButtonTouchUpInside), for: .touchUpInside)
let savedImageView = addImageView()
savedImageView.topAnchor.constraint(equalTo: button.bottomAnchor, constant: 60).isActive = true
savedImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
self.savedImageView = savedImageView
}
private func addImageView<T: UIImageView>(image: UIImage? = nil) -> T {
let imageView = T(frame: .zero)
imageView.contentMode = .scaleAspectFit
imageView.image = image
view.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
return imageView
}
#objc func saveImageButtonTouchUpInside(sender: UIButton) {
savedImageView?.image = drawnImageView?.screenShot
}
}
Results
You can put your UIImageView in background and UIView at top of it and then capture UIView as Image and merge it.
Refer this to capture UIView: How to capture UIView to UIImage without loss of quality on retina display
And then Merge it using :
func mergeImages (forgroundImage : UIImage, backgroundImage : UIImage, size : CGSize) -> UIImage {
let bottomImage = backgroundImage
UIGraphicsBeginImageContext(size)
let areaSize = CGRect(x: 0, y: 0, width: size.width, height: size.height)
bottomImage.draw(in: areaSize)
let topImage = forgroundImage
topImage.draw(in: areaSize, blendMode: .normal, alpha: 1.0)
let newImage:UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return newImage
}
This may help you . Refer the link
How do I draw on an image in Swift?
Make an image graphics context. (Before iOS 10, you would do this by calling UIGraphicsBeginImageContextWithOptions. In iOS 10 there's another way, UIGraphicsImageRenderer, but you don't have to use it if you don't want to.)
Draw (i.e. copy) the image into the context. (UIImage actually has draw... methods for this very purpose.)
Draw your line into the context. (There are CGContext functions for this.)
Extract the resulting image from the context. (For example, if you used UIGraphicsBeginImageContextWithOptions, you would use UIGraphicsGetImageFromCurrentImageContext.) Then close the context.
Related
Attached video here
I want create this before and after view for side by side image comparison image here
You can do this with a layer mask.
Essentially:
private let maskLayer = CALayer()
maskLayer.backgroundColor = UIColor.black.cgColor
leftImage.layer.mask = maskLayer
var r = bounds
r.size.width = bounds.width * pct
maskLayer.frame = r
That will "clip" the image view and show only the portion covered by the mask layer.
Here's a complete example...
Using these "before" (left-side) and "after" (right-side) images:
With this sample custom view:
class RevealImageView: UIImageView {
public var leftImage: UIImage? {
didSet {
if let img = leftImage {
leftImageLayer.contents = img.cgImage
}
}
}
public var rightImage: UIImage? {
didSet {
if let img = rightImage {
self.image = img
}
}
}
// private properties
private let leftImageLayer = CALayer()
private let maskLayer = CALayer()
private let lineView = UIView()
private var pct: CGFloat = 0.5 {
didSet {
updateView()
}
}
convenience init() {
self.init(frame: .zero)
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
// any opaque color
maskLayer.backgroundColor = UIColor.black.cgColor
leftImageLayer.mask = maskLayer
// the "reveal" image layer
layer.addSublayer(leftImageLayer)
// the vertical line
lineView.backgroundColor = .lightGray
addSubview(lineView)
isUserInteractionEnabled = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let t = touches.first
else { return }
let loc = t.location(in: self)
pct = loc.x / bounds.width
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let t = touches.first
else { return }
let loc = t.location(in: self)
pct = loc.x / bounds.width
}
private func updateView() {
// move the vertical line to the touch point
lineView.frame = CGRect(x: bounds.width * pct, y: bounds.minY, width: 1, height: bounds.height)
// update the "left image" mask to the touch point
var r = bounds
r.size.width = bounds.width * pct
// disable layer animation
CATransaction.begin()
CATransaction.setDisableActions(true)
maskLayer.frame = r
CATransaction.commit()
}
override func layoutSubviews() {
super.layoutSubviews()
leftImageLayer.frame = bounds
updateView()
}
}
and this simple example controller:
class RevealImageTestVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
guard let leftIMG = UIImage(named: "before"),
let rightIMG = UIImage(named: "after")
else {
return
}
let revealView = RevealImageView()
revealView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(revealView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
revealView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
revealView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
revealView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// give reveal view the same aspect ratio as the image
revealView.heightAnchor.constraint(equalTo: revealView.widthAnchor, multiplier: leftIMG.size.height / leftIMG.size.width),
])
revealView.leftImage = leftIMG
revealView.rightImage = rightIMG
}
}
We get this - the vertical line and the "viewable" portion of the images will move as you touch-and-drag:
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.
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
}
}
I'm working on a drawing app prototype and what I want to achieve is perfect rounding lines. I've implemented drawing via curves but still, my lines look a little bit pixelated.
Here is my result:
and here is what I'm trying to achieve:
class ViewController: UIViewController {
private var currentPoint: CGPoint?
private var previousPoint1: CGPoint?
private var previousPoint2: CGPoint?
private var lineColor = UIColor.black
private var lineWidth = CGFloat(10)
private var lineAlpha = CGFloat(1)
#IBOutlet private weak var imageView: UIImageView!
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
previousPoint1 = touch.previousLocation(in: self.view)
currentPoint = touch.location(in: self.view)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
previousPoint2 = previousPoint1
previousPoint1 = touch.previousLocation(in: self.view)
currentPoint = touch.location(in: self.view)
let mid1 = middlePoint(previousPoint1!, previousPoint2: previousPoint2!)
let mid2 = middlePoint(currentPoint!, previousPoint2: previousPoint1!)
draw(move: CGPoint(x: mid1.x, y: mid1.y), to: CGPoint(x: mid2.x, y: mid2.y), control: CGPoint(x: previousPoint1!.x, y: previousPoint1!.y))
}
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
touchesMoved(touches, with: event)
}
private func middlePoint(_ previousPoint1: CGPoint, previousPoint2: CGPoint) -> CGPoint {
return CGPoint(x: (previousPoint1.x + previousPoint2.x) * 0.5, y: (previousPoint1.y + previousPoint2.y) * 0.5)
}
func draw(move: CGPoint, to: CGPoint, control: CGPoint) {
UIGraphicsBeginImageContext(view.frame.size)
guard let ctx = UIGraphicsGetCurrentContext() else { return }
imageView.image?.draw(in: view.bounds)
ctx.move(to: move)
ctx.addQuadCurve(to: to, control: control)
ctx.setLineCap(.round)
ctx.setLineJoin(.round)
ctx.setLineWidth(lineWidth)
ctx.setStrokeColor(lineColor.cgColor)
ctx.setBlendMode(.normal)
ctx.setAlpha(lineAlpha)
ctx.strokePath()
let finalImage = UIGraphicsGetImageFromCurrentImageContext()
imageView.image = finalImage
UIGraphicsEndImageContext()
}
}
P.S. I want to draw via context, not using drawRect
The problem here is that the scale of imageContext you are creating is not correct. Rather than creating your context with UIGraphicsBeginImageContext try to create it with UIGraphicsBeginImageContextWithOptions and give scale value as 0. When you specify a scale value of 0, the scale factor is set to the scale factor of the device’s main screen. Updated code snippet would be like the one follows.
func draw(move: CGPoint, to: CGPoint, control: CGPoint) {
UIGraphicsBeginImageContextWithOptions(view.frame.size, false, 0)
guard let ctx = UIGraphicsGetCurrentContext() else { return }
imageView.image?.draw(in: view.bounds)
ctx.move(to: move)
ctx.addQuadCurve(to: to, control: control)
ctx.setLineCap(.round)
ctx.setLineJoin(.round)
ctx.setLineWidth(lineWidth)
ctx.setStrokeColor(lineColor.cgColor)
ctx.setBlendMode(.normal)
ctx.setAlpha(lineAlpha)
ctx.strokePath()
let finalImage = UIGraphicsGetImageFromCurrentImageContext()
imageView.image = finalImage
UIGraphicsEndImageContext()
}
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()
}
}