I have a canvasImageView and draw something on it.
And I want to output an image just contain image region.
My output function is func saveBtnPressed(sender: UIButton).
Like following image, I want to output blue line region.
Have any idea to do this?
Thanks.
class CanvasViewController: UIViewController {
let canvasImageView: CanvasImageView = {
let ui = CanvasImageView()
ui.contentMode = .scaleAspectFit
ui.layer.backgroundColor = UIColor.clear.cgColor
ui.backgroundColor = UIColor.clear
ui.clipsToBounds = true
ui.isMultipleTouchEnabled = false
ui.isUserInteractionEnabled = true
return ui
}()
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Color Yourself"
let save = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveBtnPressed(sender:)))
let redo = UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(clearBtnPressed(sender:)))
let back = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(cancelBtnPressed(sender:)))
self.navigationItem.rightBarButtonItems = [save, redo]
self.navigationItem.leftBarButtonItem = back
self.setupView()
}
fileprivate func setupView() {
self.view.addSubview(canvasImageView)
if #available(iOS 11.0, *) {
self.canvasImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
} else {
self.canvasImageView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor, constant: 0).isActive = true
}
self.canvasImageView.snp.makeConstraints { (make) in
make.bottom.left.right.equalToSuperview()
}
}
//MARK: BUTTON ACTION
#objc func cancelBtnPressed(sender: UIBarButtonItem) {
self.dismiss(animated: false, completion: nil)
}
#objc func clearBtnPressed(sender: UIButton) {
canvasImageView.clearCanvas()
}
#objc func saveBtnPressed(sender: UIButton) {
//Here is output image.
UIGraphicsBeginImageContextWithOptions(self.canvasImageView.layer.bounds.size, true, 0.0)
self.canvasImageView.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
if let img = image {
UIImageWriteToSavedPhotosAlbum(img, self,#selector(image(_:didFinishSavingWithError:contextInfo:)), nil)
}
}
}
class CanvasImageView: UIImageView {
var lineColor = UIColor.red
var lineWidth: CGFloat = 10
var path: UIBezierPath?
var touchPoint: CGPoint!
var startingPoint: CGPoint!
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.startingPoint = touches.first?.location(in: self)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
self.touchPoint = touches.first?.location(in: self)
self.path = UIBezierPath()
path?.move(to: startingPoint)
path?.addLine(to: touchPoint)
self.startingPoint = touchPoint
self.draw()
}
func draw() {
let shapeLayer = CAShapeLayer()
shapeLayer.path = path?.cgPath
shapeLayer.strokeColor = lineColor.cgColor
shapeLayer.lineWidth = lineWidth
shapeLayer.fillColor = UIColor.clear.cgColor
self.layer.addSublayer(shapeLayer)
self.setNeedsDisplay()
}
func clearCanvas() {
guard let drawPath = path else { return }
drawPath.removeAllPoints()
self.layer.sublayers = nil
self.setNeedsDisplay()
}
}
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:
my app part is based on canvas.
#What I want to do?
I need dictionary to track and save particular player's key and value.because, I want to store player1 on key "1" and player2 on key "2", suppose user change player 2's number, then player 2's number change not key "2".
I can place multiple players (cashapelayers - with text),
on imageview. and can edit text label on double click for same player.
my issue is, whenever i click second time on same player, it shows wrong number, as well when click okay it creates new player.
what i want is to edit a player's label(number) with different number,
and don't create a new one till I click on imageview.
I have try to create playerview in touchedEnded but I'm fail, also try to search for the same issue on other resources.
I have added some images for reference.
class AddPlayerStruct {
var addPlayerViewStruct : AddPlayerView?
var addPlayerViewsArrStruct : [AddPlayerView] = []
var Label = UILabel()
}
import UIKit
class ViewController: UIViewController {
//MARK: AddPlayerView Variables
var addPlayerView : AddPlayerView?
var addPlayerViews: [AddPlayerView] = []
var draggedAddPlayer: AddPlayerView?
let addPlayerWidth : CGFloat = 40
var addPlayerDict : [String : AddPlayerStruct] = [:]
var playerCount : Int = 1
var label = UILabel()
var isDobuleClick : Bool = false
#IBOutlet weak var images: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let draggedAddPlayer = draggedAddPlayer, let point = touches.first?.location(in: images) else {
return
}
draggedAddPlayer.center = point
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Do nothing if a circle is being dragged
// or if we do not have a coordinate
guard draggedAddPlayer == nil, let point = touches.first?.location(in: images) else {
return
}
// Do not create new circle if touch is in an existing circle
// Keep the reference of the (potentially) dragged circle
if let draggedAddPlayer = addPlayerViews.filter({ UIBezierPath(ovalIn: $0.frame).contains(point) }).first {
self.draggedAddPlayer = draggedAddPlayer
return
}
// Create new circle and store in dict
let rect = CGRect(x: point.x - 20, y: point.y - 20, width: addPlayerWidth, height: addPlayerWidth)
addPlayerView = AddPlayerView(frame: rect)
addPlayerView?.backgroundColor = .white
addPlayerView?.isUserInteractionEnabled = true
addPlayerView?.image = UIImage(named: "player")
addPlayerView?.tintColor = .systemBlue
addPlayerViews.append(addPlayerView!)
images.addSubview(addPlayerView!)
// The newly created view can be immediately dragged
draggedAddPlayer = addPlayerView
//Add Player label as Number
// playerCount = addPlayerDict.count + 1
var addPlayerStruct = AddPlayerStruct()
label = UILabel(frame: CGRect(x: rect.width / 2 - 8, y: rect.height / 2 + 5, width: 16, height: 10))
if addPlayerStruct.Label.text == nil{
label.text = String(addPlayerDict.count + 1)
}
label.font = UIFont(name: "Helvetica" , size: 10)
label.textColor = UIColor.white
label.textAlignment = NSTextAlignment.center
label.isUserInteractionEnabled = true
addPlayerView!.addSubview(label)
debugPrint(addPlayerDict)
addPlayerStruct.addPlayerViewStruct = addPlayerView
addPlayerStruct.addPlayerViewsArrStruct.append(contentsOf: addPlayerViews)
addPlayerDict.updateValue(addPlayerStruct, forKey: String(addPlayerDict.count + 1))
debugPrint(addPlayerDict)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
draggedAddPlayer = nil
var selectedPlayerKey = String()
var addPlayerStruct = AddPlayerStruct()
var selectedPoint = CGPoint()
if isDobuleClick == true {
guard let point = touches.first?.location(in: images) else {
return
}
//TODO: check which dict key's playerview has same point
addPlayerDict.forEach { (key,value) in
debugPrint(key)
debugPrint(value)
// debugPrint("before \(addPlayerDict.count)")
//TODO: get selected dict key and get all data of it
guard let points = value.addPlayerViewStruct?.frame.contains(point) else { return }
if points{
selectedPlayerKey = key
selectedPoint = point
addPlayerViews.append(contentsOf: value.addPlayerViewsArrStruct)
debugPrint(addPlayerViews.last)
addPlayerView = value.addPlayerViewStruct //selected playerview
debugPrint(selectedPlayerKey)
// show data on alertview textfield
let alert = UIAlertController(title: "Player Number", message: "Enter new player Number", preferredStyle: .alert)
alert.addTextField { textData in
if addPlayerStruct.Label.text == nil{
textData.text = selectedPlayerKey
}else{
textData.text = addPlayerStruct.Label.text
}
}
// get changed data from textfield and save back to same dict key's playerview - Don't change key.
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
let textfield = alert.textFields?[0]
addPlayerStruct.Label.text = textfield?.text!
// let index = self.addPlayerDict.index(forKey: selectedPlayerKey)
addPlayerStruct.addPlayerViewStruct = self.addPlayerView
self.draggedAddPlayer = self.addPlayerView
self.addPlayerViews.removeLast()
self.addPlayerView?.removeFromSuperview() //selected playerview removed from iamgeview
//
// // Show on playerview
//
let rect = CGRect(x: selectedPoint.x - 20, y: selectedPoint.y - 20, width: self.addPlayerWidth, height: self.addPlayerWidth)
self.addPlayerView = AddPlayerView(frame: rect)
self.addPlayerView?.backgroundColor = .white
self.addPlayerView?.isUserInteractionEnabled = true
self.addPlayerView?.image = UIImage(named: "player")
self.addPlayerView?.tintColor = .systemBlue
self.addPlayerViews.append(self.addPlayerView!)
self.images.addSubview(self.addPlayerView!)
self.label = UILabel(frame: CGRect(x: rect.width / 2 - 8, y: rect.height / 2 + 5, width: 16, height: 10))
self.label.text = textfield?.text!
self.label.font = UIFont(name: "Helvetica" , size: 10)
self.label.textColor = UIColor.white
self.label.textAlignment = NSTextAlignment.center
self.label.isUserInteractionEnabled = true
self.addPlayerView!.addSubview(self.label)
debugPrint(self.addPlayerDict.count)
self.addPlayerDict.updateValue(addPlayerStruct, forKey: selectedPlayerKey)
debugPrint(addPlayerStruct.addPlayerViewStruct?.frame)
debugPrint(self.addPlayerDict.count)
}))
self.present(alert, animated: false)
}
}
}
isDobuleClick = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.isDobuleClick = false
}
}
}
#AddPlayerView
class AddPlayerView : UIImageView{
var shapeLayer = CAShapeLayer()
var addPlayerPath = UIBezierPath()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup(){
self.backgroundColor = .clear
// let shapeLayer = CAShapeLayer()
addPlayerPath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2, y: frame.size.height / 2), radius: 20, startAngle: 0, endAngle: 360, clockwise: true)
shapeLayer.path = addPlayerPath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
self.layer.addSublayer(shapeLayer)
}
}
You have a decent approach, but a couple tips...
Your class and var naming is rather confusing. For example, instead of naming your image view images it makes much more sense to name it something like playingFieldImageView. And, instead of AddPlayerView (which sounds like an action), just name it PlayerView.
You don't need to keep a separate array of player views or a dictionary of player structs... When you add a PlayerView as a subview of playingFieldImageView, you can track it with the .subviews property of playingFieldImageView.
You can move a lot of the logic you're using to manage the player view into the player view class... along these lines:
class PlayerView : UIImageView {
public var playerNumber: Int = 0 {
didSet {
playerLabel.text = "\(playerNumber)"
}
}
private let shapeLayer = CAShapeLayer()
private let playerLabel: UILabel = {
let v = UILabel()
v.font = UIFont(name: "Helvetica" , size: 10)
v.textColor = .white
v.textAlignment = NSTextAlignment.center
v.text = "0"
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
self.isUserInteractionEnabled = true
self.backgroundColor = .white
self.tintColor = .systemBlue
if let img = UIImage(named: "player") {
self.image = img
} else {
if let img = UIImage(systemName: "person.fill") {
self.image = img
} else {
self.backgroundColor = .green
}
}
// if using as a mask, can be any opaque color
shapeLayer.fillColor = UIColor.black.cgColor
// assuming you want a "round" view
//self.layer.addSublayer(shapeLayer)
self.layer.mask = shapeLayer
// add and constrain the label as a subview
addSubview(playerLabel)
NSLayoutConstraint.activate([
playerLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
playerLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
playerLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -5.0),
])
}
override func layoutSubviews() {
// update the mask path here (we have accurate bounds)
shapeLayer.path = UIBezierPath(ovalIn: bounds).cgPath
}
}
Now you can add a new PlayerView and assign its .playerNumber ... instead of all of the set image, add label, etc. And, when the user wants to change the "player number" you can update the .playerNumber property instead of removing / re-creating views.
When trying to track touches and taps (particularly double-taps), it is much easier to add a double-tap gesture recognizer to the subview itself.
Here's that same PlayerView class, but with a gesture recognizer -- and a closure to tell the controller that it was double-tapped:
class PlayerView : UIImageView {
// closure to tell the controller this view was double-tapped
public var gotDoubleTap: ((PlayerView) -> ())?
public var playerNumber: Int = 0 {
didSet {
playerLabel.text = "\(playerNumber)"
}
}
private let shapeLayer = CAShapeLayer()
private let playerLabel: UILabel = {
let v = UILabel()
v.font = UIFont(name: "Helvetica" , size: 10)
v.textColor = .white
v.textAlignment = NSTextAlignment.center
v.text = "0"
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
self.isUserInteractionEnabled = true
self.backgroundColor = .white
self.tintColor = .systemBlue
if let img = UIImage(named: "player") {
self.image = img
} else {
if let img = UIImage(systemName: "person.fill") {
self.image = img
} else {
self.backgroundColor = .green
}
}
// if using as a mask, can be any opaque color
shapeLayer.fillColor = UIColor.black.cgColor
// assuming you want a "round" view
//self.layer.addSublayer(shapeLayer)
self.layer.mask = shapeLayer
// add and constrain the label as a subview
addSubview(playerLabel)
NSLayoutConstraint.activate([
playerLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
playerLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
playerLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -5.0),
])
// add a double-tap gesture recognizer
let g = UITapGestureRecognizer(target: self, action: #selector(doubleTapHandler(_:)))
g.numberOfTapsRequired = 2
addGestureRecognizer(g)
}
override func layoutSubviews() {
// update the mask path here (we have accurate bounds)
shapeLayer.path = UIBezierPath(ovalIn: bounds).cgPath
}
#objc func doubleTapHandler(_ g: UITapGestureRecognizer) {
// tell the controller we were double-tapped
gotDoubleTap?(self)
}
}
You can now simplify all of your touch code to handle only adding new or moving the player views.
Here's an example controller class, using the above PlayerView class:
class AddPlayerViewController: UIViewController {
//MARK: AddPlayerView Variables
// we can declare this as a standard UIView
var draggedPlayerView: UIView?
let playerViewWidth : CGFloat = 40
#IBOutlet var playingFieldImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
playingFieldImageView.isUserInteractionEnabled = true
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let draggedAddPlayer = draggedPlayerView, let point = touches.first?.location(in: playingFieldImageView) else {
return
}
draggedAddPlayer.center = point
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Do nothing if a PlayerView is being dragged
// or if we do not have a coordinate
guard draggedPlayerView == nil, let point = touches.first?.location(in: playingFieldImageView) else {
return
}
// Do not create new PlayerView if touch is in an existing circle
// Keep the reference of the (potentially) dragged circle
// filter only subviews which are PlayerView class (in case we've added another subview type)
if let draggedAddPlayer = playingFieldImageView.subviews.filter({ $0 is PlayerView }).filter({ UIBezierPath(ovalIn: $0.frame).contains(point) }).first {
self.draggedPlayerView = draggedAddPlayer
return
}
// Create new PlayerView
let rect = CGRect(x: point.x - 20, y: point.y - 20, width: playerViewWidth, height: playerViewWidth)
let newPlayerView = PlayerView(frame: rect)
// give the new player the lowest available number
// for example, if we've created numbers:
// "1" "2" "3" "4" "5"
// we want to assign "6" to the new guy
// but... if the user has changed the 3rd player to "12"
// we'll have players:
// "1" "2" "12" "4" "5"
// so we want to assign "3" to the new guy
let nums = playingFieldImageView.subviews.compactMap{$0 as? PlayerView}.compactMap { $0.playerNumber }
var newNum: Int = 1
for i in 1..<nums.count + 2 {
if !nums.contains(i) {
newNum = i
break
}
}
newPlayerView.playerNumber = newNum
// set the double-tap closure
newPlayerView.gotDoubleTap = { [weak self] pv in
guard let self = self else { return }
self.changePlayerNumber(pv)
}
playingFieldImageView.addSubview(newPlayerView)
// The newly created view can be immediately dragged
draggedPlayerView = newPlayerView
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
draggedPlayerView = nil
}
// called when a PlayerView is double-tapped
func changePlayerNumber(_ playerView: PlayerView) {
// show data on alertview textfield
let alert = UIAlertController(title: "Player Number", message: "Enter new player Number", preferredStyle: .alert)
alert.addTextField { textData in
textData.text = "\(playerView.playerNumber)"
}
// get changed data from textfield and update the playerNumber for the PlayerView
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
guard let tf = alert.textFields?.first,
let newNumString = tf.text,
let newNumInt = Int(newNumString),
newNumInt != playerView.playerNumber
else {
// user entered something that was not a number, or
// tapped OK without changing the number
return
}
// don't allow a duplicate player number
let nums = self.playingFieldImageView.subviews.compactMap{$0 as? PlayerView}.compactMap { $0.playerNumber }
if nums.contains(newNumInt) {
let dupAlert = UIAlertController(title: "Duplicate PLayer Number", message: "Somebody else already has number: \(newNumInt)", preferredStyle: .alert)
dupAlert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
self.changePlayerNumber(playerView)
}))
self.present(dupAlert, animated: true)
} else {
playerView.playerNumber = newNumInt
}
}))
self.present(alert, animated: false)
}
}
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.
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()
}
}
I have a problem with xcode: I created a draw app where you can draw on the pictures, when I open a picture and I click to draw, the image stretches,
the same thing if I change content mode (aspectFit, aspectFill)
I forget something?
how to set up the content mode correctly?
This is my code:
import UIKit
import Photos
class ViewController: UIViewController {
#IBOutlet var imageView: UIImageView!
#IBOutlet var toolIcon: UIButton!
#IBOutlet var tempImage: UIImageView!
var lastPoint = CGPoint.zero
var swiped = false
var red: CGFloat = 0.0
var green: CGFloat = 0.0
var blue: CGFloat = 0.0
var brushWidth:CGFloat = 10.0
var opacity:CGFloat = 1.0
var tool: UIImageView!
var isDrawing = true
var selectedImage: UIImage!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
imageView.contentMode = UIViewContentMode.scaleAspectFill
tempImage.contentMode = UIViewContentMode.scaleAspectFill
tool = UIImageView()
tool.frame = CGRect(x:self.view.bounds.size.width, y: self.view.bounds.size.height, width: 38, height: 38)
//tool.image = #imageLiteral(resourceName: "Pen")
self.view.addSubview(tool)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?){
swiped = false
if let touch = touches.first {
lastPoint = touch.location(in: self.view)
}
}
func drawLines(fromPoint:CGPoint, toPoint:CGPoint){
UIGraphicsBeginImageContext(self.view.frame.size)
tempImage.image?.draw(in: CGRect(x:0, y:0, width: self.view.frame.width,height: self.view.frame.height))
let context = UIGraphicsGetCurrentContext()
context?.move(to:CGPoint(x:fromPoint.x,y:fromPoint.y))
context?.addLine(to: CGPoint(x:toPoint.x, y: toPoint.y))
tool.center = toPoint
context?.setBlendMode(CGBlendMode.normal)
context?.setLineCap(CGLineCap.round)
context?.setLineWidth(brushWidth)
context?.setStrokeColor(UIColor(red: red, green: green, blue: blue, alpha: 1.0).cgColor)
context?.strokePath()
tempImage.image = UIGraphicsGetImageFromCurrentImageContext()
tempImage.alpha = opacity
UIGraphicsEndImageContext()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?){
swiped = true
if let touch = touches.first {
let currentPoint = touch.location(in: self.view)
drawLines(fromPoint: lastPoint, toPoint: currentPoint)
lastPoint = currentPoint
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if !swiped {
drawLines(fromPoint: lastPoint, toPoint: lastPoint)
}
// Merge tempImageView into mainImageView
UIGraphicsBeginImageContext(imageView.frame.size)
imageView.image?.draw(in: CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height), blendMode: CGBlendMode.normal, alpha: 1.0)
tempImage.image?.draw(in: CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height), blendMode: CGBlendMode.normal, alpha: opacity)
imageView.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
tempImage.image = nil
}
#IBAction func reset(_ sender: Any) {
self.imageView.image = nil
}
#IBAction func colorPicked(_ sender: UIButton) {
if sender.tag == 0 {
(red,green,blue) = (1,0,0)
}else if sender.tag == 1 {
(red,green,blue) = (0,1,0)
}else if sender.tag == 2{
(red,green,blue) = (0,0,1)
}else if sender.tag == 3 {
(red,green,blue) = (1,0,1)
}else if sender.tag == 4{
(red,green,blue) = (1,1,0)
}else if sender.tag == 5 {
(red,green,blue) = (0,1,1)
}else if sender.tag == 6{
(red,green,blue) = (1,1,1)
}else if sender.tag == 7 {
(red,green,blue) = (0,0,0)
}
}
#IBAction func save(_ sender: Any) {
let actionSheet = UIAlertController(title: "Pick your options", message: "", preferredStyle: .actionSheet)
actionSheet.addAction(UIAlertAction(title: "Pick an image", style: .default, handler: { (_) in
let imagePicker = UIImagePickerController()
imagePicker.sourceType = .photoLibrary
imagePicker.allowsEditing = false
imagePicker.delegate = self
self.present(imagePicker, animated: true, completion: nil)
}))
actionSheet.addAction(UIAlertAction(title: "Save your drawing", style: .default, handler: {(_) in
if let image = self.imageView.image {
UIImageWriteToSavedPhotosAlbum(image, nil,nil,nil)
}
}))
actionSheet.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil))
present(actionSheet, animated: true, completion: nil)
}
#IBAction func erase(_ sender: Any) {
if (isDrawing) {
(red,green,blue) = (1,1,1)
tool.image = #imageLiteral(resourceName: "Gomma")
toolIcon.setImage(#imageLiteral(resourceName: "Pen"), for: .normal)
}else {
(red,green,blue) = (0,0,0)
tool.image = nil
toolIcon.setImage(#imageLiteral(resourceName: "Gomma"), for: .normal)
}
isDrawing = !isDrawing
}
#IBAction func setting(_ sender: Any) {
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
let settingsVc = segue.destination as! SettingViewController
settingsVc.delegate = self
settingsVc.red = red
settingsVc.green = green
settingsVc.blue = blue
settingsVc.brush = brushWidth
settingsVc.opacity = opacity
}
}
extension ViewController:UINavigationControllerDelegate,UIImagePickerControllerDelegate,SettingsVCDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let imagePicked = info[UIImagePickerControllerOriginalImage] as? UIImage {
self.selectedImage = imagePicked
self.imageView.image = selectedImage
self.tempImage.image = selectedImage
dismiss(animated: true, completion: nil)
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismiss(animated: true, completion: nil)
}
func settingsViewControllerDidFinish(_settingsVc: SettingViewController) {
self.red = _settingsVc.red
self.green = _settingsVc.green
self.blue = _settingsVc.blue
self.brushWidth = _settingsVc.brush
self.opacity = _settingsVc.opacity
}
}