My code is trying to call a class which controlls all the drawing properties and nothing is showing up on the uiview. It's like the class is not being called but I know its being called. I don't know what to do. It is important that canvas covers a specific area and not the entire view thats why I programmed constraint it.
var canvas = Canvas()
override func viewDidLoad() {
super.viewDidLoad()
canvas.backgroundColor = .black
setupLayout()
canvas.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(canvas)
NSLayoutConstraint.activate ([
canvas.trailingAnchor.constraint(equalTo: view.centerXAnchor, constant :150),
canvas.topAnchor.constraint(equalTo: view.centerYAnchor, constant : 0),
canvas.widthAnchor.constraint(equalToConstant: 300),
canvas.heightAnchor.constraint(equalToConstant: 300),
])
}
class Canvas: UIView {
// public function
func undo() {
_ = lines.popLast()
setNeedsDisplay()
}
func clear() {
lines.removeAll()
setNeedsDisplay()
}
fileprivate var lines = [[CGPoint]]()
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else { return }
context.setStrokeColor(UIColor.red.cgColor)
context.setLineWidth(5)
context.setLineCap(.butt)
lines.forEach { (line) in
for (i, p) in line.enumerated() {
if i == 0 {
context.move(to: p)
} else {
context.addLine(to: p)
}
}
}
context.strokePath()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
lines.append([CGPoint]())
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let point = touches.first?.location(in: nil) else { return }
guard var lastLine = lines.popLast() else { return }
lastLine.append(point)
lines.append(lastLine)
setNeedsDisplay()
}}
Change the below line
guard let point = touches.first?.location(in: nil) else { return }
to this
guard let point = touches.first?.location(in: self) else { return }
The points stored in lines array are corresponding to the superview to which your CanvasView was attached to. You need to store the points corresponding to CanvasView. That's exactly what the self parameter does in place of nil above.
Related
I'd like to move a simple, small UIView around the screen and 'drop' it off at any location on the screen, but accurately. Simply lifting your finger from the screen does not have the desired effect as there is always some movement upon lifting your finger, resulting in the object not being in the required location.
What I'm looking for is some way to count down a specified number of milliseconds AFTER holding/pausing at the desired location and then have some mechanism ENDING my touches/gesture so the object is placed EXACTLY where I want it.
I've been reasonably successful with touchesBegan/Moved/Ended, but even though I called the touchesEnded method the touches never really end as I can still drag around the object on the screen and relocate it - not what I want.
import UIKit
class ViewController: UIViewController {
let greenDot : UIView = {
let greenDot = UIView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
greenDot.backgroundColor = .green
greenDot.layer.cornerRadius = greenDot.bounds.height / 2
return greenDot
}()
var timer : Timer?
var lastTouch = Set<UITouch>()
var lastTouchEvent: UIEvent?
override func viewDidLoad() {
super.viewDidLoad()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
print(touch.location(in: view))
greenDot.center = touch.location(in: view)
view.addSubview(greenDot)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
print(touch.location(in: view))
if timer != nil { timer?.invalidate() }
greenDot.center = touch.location(in: view)
view.addSubview(greenDot)
lastTouch = touches
lastTouchEvent = event
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(stopTouches), userInfo: nil, repeats: false)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
timer?.invalidate()
print("Touches Ended #: \(touch.location(in: view))")
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
print("Cancelling touch")
}
#objc func stopTouches() {
touchesEnded(lastTouch, with: lastTouchEvent)
// touchesCancelled(lastTouch, with: lastTouchEvent)
// view.resignFirstResponder()
}
}
With UIGestures I have tried with the UILongPressGestureRecognizer, but I don't know how to 'end' with a long-press. Do I use 2 long-presses in sequence - one to start the movement and another to end the movement? I like the longPress since it is continuous and I can thus pan with it.
import UIKit
class ViewController: UIViewController {
let greenDot : UIView = {
let dot = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
dot.backgroundColor = .green
dot.layer.cornerRadius = dot.frame.width/2
return dot
}()
override func viewDidLoad() {
super.viewDidLoad()
let longPress1 = UILongPressGestureRecognizer(target: self, action: #selector(press1))
longPress1.minimumPressDuration = 0.2
view.addGestureRecognizer(longPress1)
}
#objc func press1(_ sender: UILongPressGestureRecognizer) {
let newLocation = sender.location(in: view)
greenDot.backgroundColor = .green
greenDot.frame = CGRect(x: 0, y: 0, width: 10, height: 10)
greenDot.center = CGPoint(x: newLocation.x, y: newLocation.y - 40)
if sender.state == .ended {
greenDot.frame = CGRect(x: 0, y: 0, width: 20, height: 20)
greenDot.center = CGPoint(x: newLocation.x, y: newLocation.y - 40)
greenDot.backgroundColor = .purple
}
view.addSubview(greenDot)
}
}
So in conclusion: I'm looking for a method to place an object, which I am moving around on the screen with my finger, accurately.
Any help in this regard will be greatly appreciated.
Intriguing how often you end up answering your own question after placing it before others.
So after many days of fumbling around I stumbled across this pointer from Apple:
https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/implementing_a_custom_gesture_recognizer/implementing_a_discrete_gesture_recognizer
In short, I have the basics of what I was looking for by creating a custom Gesture Recognizer. Thanks Apple.
So for anyone else who might be interested, following is the 'basic' code for grabbing an object, moving it around and letting a timer release it for you so that the drop is as accurate as needed. Releasing before the timer expires also works, but then some shifting might/will occur during release. Once the timer has fired and your finger is still on the screen, you will be unable to accidentally drag the object out of place.
Obviously there is still a lot of work needed before implementing in an app to mitigate errors. 'Small steps'
import UIKit
import UIKit.UIGestureRecognizerSubclass
class ObjectDropGestureRecognizer: UIGestureRecognizer {
var trackedTouch : UITouch? = nil
var intervalTime : Double = 1.0
var dropTimer : Timer?
var counter : Int = 0
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
print(touches.count)
if touches.count != 1 {
self.state = .failed
}
if self.state == .possible {
self.state = .began
print("began...")
}
if self.trackedTouch == nil {
self.trackedTouch = touches.first
} else {
for touch in touches {
if touch != self.trackedTouch {
self.ignore(touch, for: event)
}
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
guard let newTouch = touches.first else { return }
self.state = .changed
counter += 1
print("Changed...\(counter)")
if self.dropTimer != nil { dropTimer?.invalidate() }
dropTimer = Timer.scheduledTimer(withTimeInterval: intervalTime, repeats: false) { timer in
print("Timer fired")
print(newTouch.location(in: self.view))
self.state = .ended
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
if self.dropTimer != nil {
self.dropTimer?.invalidate()
self.state = .ended
}
self.state = .recognized
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
self.trackedTouch = nil
self.state = .cancelled
}
override func reset() {
super.reset()
self.trackedTouch = nil
self.counter = 0
}
}
class ViewController: UIViewController {
let greenDot : UIView = {
let dot = UIView(frame: CGRect(x: 50, y: 200, width: 20, height: 20))
dot.backgroundColor = .green
dot.layer.cornerRadius = dot.frame.width/2
return dot
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(greenDot)
let objectMoveGesture = ObjectDropGestureRecognizer(target: self, action: #selector(tapAction(_:)))
objectMoveGesture.intervalTime = 0.8
view.addGestureRecognizer(objectMoveGesture)
}
#objc func tapAction(_ touch: ObjectDropGestureRecognizer){
let newLocation = touch.location(in: view)
greenDot.center = CGPoint(x: newLocation.x, y: newLocation.y)
}
}
My swifts codes goal below is to be able to draw a straight line. When the uiview is touch the user can only draw 90 degrees above the initial point. The direction has already ben decided. So the user can just draw the line above the point that is touched. You can the gif below the line on the left is what my code does below. The line on the right is what I would like to acheive.
import UIKit
class ViewController: UIViewController{
var draw = Canvas()
override func viewDidLoad() {
super.viewDidLoad()
[draw].forEach {
view.addSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false}
}
override func viewDidLayoutSubviews() {
draw.backgroundColor = .clear
NSLayoutConstraint.activate ([
draw.bottomAnchor.constraint(equalTo: view.bottomAnchor),
draw.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.77, constant: 0),
draw.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1, constant: 0),
draw.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant : 0),
])
}
}
struct ColoredLine {
var color = UIColor.black
var points = [CGPoint]()
var width = 5
}
class Canvas: UIView {
var strokeColor = UIColor.red
var strokeWidth = 5
func undo() {
_ = lines.popLast()
setNeedsDisplay()
}
func clear() {
lines.removeAll()
setNeedsDisplay()
}
var lines = [ColoredLine]()
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else { return }
lines.forEach { (line) in
for (i, p) in line.points.enumerated() {
if i == 0 {
context.move(to: p)
} else {
context.addLine(to: p)
}
}
context.setStrokeColor(line.color.cgColor)
context.setLineWidth(CGFloat(line.width))
context.strokePath()
context.beginPath()
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
var coloredLine = ColoredLine()
coloredLine.color = strokeColor
coloredLine.width = strokeWidth
lines.append(coloredLine)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let point = touches.first?.location(in: self) else { return }
guard var lastLine = lines.popLast() else { return }
lastLine.points.append(point)
lines.append(lastLine)
setNeedsDisplay()
}
}
It is OK to manufacture the coordinates, Points consists of x and y.
Keeping the y, and done.
keep the first point for one touch event
var firstPt: CGPoint?
// get the first point
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let first = touches.first?.location(in: self) else { return }
// store first as property
firstPt = first
}
manufacture the rest points, the x of new get points is irrelevant
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let point = touches.first?.location(in: self), let first = firstPt else { return }
let pointNeeded = CGPoint(x: first.x , y: point.y)
// ... , do as usual
}
and in touchesEnd and touchesCancell, firstPt = nil
I am creating a doodle page with an "X" as the close/dismiss button.
I also have a "clear" button to remove the doodles that have been made.
My issue is that when I press clear, even the X button disappears, how do I prevent this from happening?
This is my current app.
This is my Doodle class which allows me to draw on the view.
import UIKit
class DoodleView: UIView {
var lineColor:UIColor!
var lineWidth:CGFloat!
var path:UIBezierPath!
var touchPoint:CGPoint!
var startingPoint:CGPoint!
override func layoutSubviews() {
self.clipsToBounds = true
self.isMultipleTouchEnabled = false
lineColor = UIColor.white
lineWidth = 10
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
startingPoint = touch?.location(in: self)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
touchPoint = touch?.location(in: self)
path = UIBezierPath()
path.move(to: startingPoint)
path.addLine(to: touchPoint)
startingPoint = touchPoint
drawShapeLayer()
}
func drawShapeLayer() {
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() {
path.removeAllPoints()
self.layer.sublayers = nil
self.setNeedsDisplay()
}
}
This is the class controlling the view controller.
import UIKit
class DoodlePageViewController: UIViewController {
#IBOutlet weak var doodleView: DoodleView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func clearDoodle(_ sender: Any) {
doodleView.clearCanvas()
}
#IBAction func CloseDoodlePage(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
}
Without really seeing how you declared the buttons, I can only guess that when you call the function clearCanvas(), the X button is part of the sublayer in doodle view since you are setting self.layer.sublayers = nil hence making it also dissapear.
Make sure that the X button is on top of the doodle view when you create it.
You are clearing all the subviews out of your view when you call self.layer.sublayers = nil
you either need to re-add the 'x' view back into the hierarchy. I am assuming using your function drawShapeLayer() is the 'x' ??
so it would be
func clearCanvas() {
path.removeAllPoints()
self.layer.sublayers = nil
drawShapeLayer()
self.setNeedsDisplay()
}
My code below is in 2 separate classes. From class gooGoo I want to be able to call func change to be used as a undo button to what was drawn on the uiview. Most of the uiview properties are subclassed into classDrawingView.
I have not seen any prior questions on this using a UIBezierPath.
class gooGoo : UIViewController{
var myLayer = CALayer()
var path = UIBezierPath()
var startPoint = CGPoint()
var touchPoint = CGPoint()
var drawPlace = DrawingView()
func Change(){
drawPlace.drawColor = UIColor.black
}
}
class DrawingView: UIView {
var drawColor = UIColor.black
var lineWidth: CGFloat = 5
private var lastPoint: CGPoint!
private var bezierPath: UIBezierPath!
private var pointCounter: Int = 0
private let pointLimit: Int = 128
private var preRenderImage: UIImage!
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
initBezierPath()
}
func takeSnapshotOfView(view:UIView) -> UIImage? {
UIGraphicsBeginImageContext(CGSize(width: view.frame.size.width, height: view.frame.size.height))
view.drawHierarchy(in: CGRect(x: 0, y: 0.0, width: view.frame.size.width, height: view.frame.size.height), afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initBezierPath()
}
func initBezierPath() {
bezierPath = UIBezierPath()
bezierPath.lineCapStyle = CGLineCap.round
bezierPath.lineJoinStyle = CGLineJoin.round
}
// MARK: - Touch handling
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch: AnyObject? = touches.first
lastPoint = touch!.location(in: self)
pointCounter = 0
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch: AnyObject? = touches.first
let newPoint = touch!.location(in: self)
bezierPath.move(to: lastPoint)
bezierPath.addLine(to: newPoint)
lastPoint = newPoint
pointCounter += 1
if pointCounter == pointLimit {
pointCounter = 0
renderToImage()
setNeedsDisplay()
bezierPath.removeAllPoints()
}
else {
setNeedsDisplay()
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
pointCounter = 0
renderToImage()
setNeedsDisplay()
bezierPath.removeAllPoints()
}
override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
touchesEnded(touches!, with: event)
}
// MARK: - Pre render
func renderToImage() {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0.0)
if preRenderImage != nil {
preRenderImage.draw(in: self.bounds)
}
bezierPath.lineWidth = lineWidth
drawColor.setFill()
drawColor.setStroke()
bezierPath.stroke()
preRenderImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
// MARK: - Render
override func draw(_ rect: CGRect) {
super.draw(rect)
if preRenderImage != nil {
preRenderImage.draw(in: self.bounds)
}
bezierPath.lineWidth = lineWidth
drawColor.setFill()
drawColor.setStroke()
bezierPath.stroke()
}
// MARK: - Clearing
func clear() {
preRenderImage = nil
bezierPath.removeAllPoints()
setNeedsDisplay()
}
// MARK: - Other
func hasLines() -> Bool {
return preRenderImage != nil || !bezierPath.isEmpty
}
}
I am really struggling to find tutorials online as well as already answered questions (I have tried them and they don't seem to work). I have a UIImageView that I have in the centre of my view. I am currently able to tap and drag this wherever I want on screen. I want to be able to pinch to scale and rotate this view. How do I achieve this? I have tried the code for rotation below but it doesn't seem to work? Any help will be a massive help and marked as answer. Thank you guys.
import UIKit
class DraggableImage: UIImageView {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.backgroundColor = .blue
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.backgroundColor = .green
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let position = touch.location(in: superview)
center = CGPoint(x: position.x, y: position.y)
}
}
}
class CVController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let rotateGesture = UIRotationGestureRecognizer(target: self, action: #selector(rotateAction(sender:)))
firstImageView.addGestureRecognizer(rotateGesture)
setupViews()
}
func rotateAction(sender: UIRotationGestureRecognizer) {
let rotatePoint = sender.location(in: view)
let firstImageView = view.hitTest(rotatePoint, with: nil)
firstImageView?.transform = (firstImageView?.transform.rotated(by: sender.rotation))!
sender.rotation = 0
}
let firstImageView: DraggableImage = {
let iv = DraggableImage()
iv.backgroundColor = .red
iv.isUserInteractionEnabled = true
return iv
}()
func setupViews() {
view.addSubview(firstImageView)
let firstImageWidth: CGFloat = 50
let firstImageHeight: CGFloat = 50
firstImageView.frame = CGRect(x: (view.frame.width / 2) - firstImageWidth / 2, y: (view.frame.height / 2) - firstImageWidth / 2, width: firstImageWidth, height: firstImageHeight)
}
}
You have a some problems in your code. First you need to add the UIGestureRecognizerDelegate to your view controller and make it your gesture recognizer delegate. You need also to implement shouldRecognizeSimultaneously method and return true. Second when applying the scale you need to save the transform when the pinch begins and apply the scale in top of it:
class DraggableImageView: UIImageView {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
backgroundColor = .blue
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
backgroundColor = .green
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let position = touches.first?.location(in: superview){
center = position
}
}
}
class ViewController: UIViewController, UIGestureRecognizerDelegate {
var identity = CGAffineTransform.identity
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupViews()
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(scale))
let rotationGesture = UIRotationGestureRecognizer(target: self, action: #selector(rotate))
pinchGesture.delegate = self
rotationGesture.delegate = self
view.addGestureRecognizer(pinchGesture)
view.addGestureRecognizer(rotationGesture)
}
let firstImageView: DraggableImageView = {
let iv = DraggableImageView()
iv.backgroundColor = .red
iv.isUserInteractionEnabled = true
return iv
}()
func setupViews() {
view.addSubview(firstImageView)
let firstImageWidth: CGFloat = 50
let firstImageHeight: CGFloat = 50
firstImageView.frame = CGRect(x: view.frame.midX - firstImageWidth / 2, y: view.frame.midY - firstImageWidth / 2, width: firstImageWidth, height: firstImageHeight)
}
#objc func scale(_ gesture: UIPinchGestureRecognizer) {
switch gesture.state {
case .began:
identity = firstImageView.transform
case .changed,.ended:
firstImageView.transform = identity.scaledBy(x: gesture.scale, y: gesture.scale)
case .cancelled:
break
default:
break
}
}
#objc func rotate(_ gesture: UIRotationGestureRecognizer) {
firstImageView.transform = firstImageView.transform.rotated(by: gesture.rotation)
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}