How can I adapt TouchTracker to draw freeform lines?

basically I want touch tracker to draw freeform lines you can select and pan. how can I do this?

To do this involves combining together a lot of smaller lines. I adapted DrawView to accommodate this. This code is a modified version of what we did in the book up until the end of chapter 19 and it borrows from some posters’ solutions to the challenges and adds in this freeform capability. A bit messy, but it works!

DrawView.:

import UIKit

class DrawView: UIView, UIGestureRecognizerDelegate {
// MARK: - Properties
var currentLine = Line
var currentLines = NSValue:[Line]
var finishedLines = [Line]
var selectedLineArrayIndex: Int? {
didSet {
if selectedLineArrayIndex == nil {
let menu = UIMenuController.shared
menu.setMenuVisible(false, animated: true)
moveRecognizer.cancelsTouchesInView = false
}
}
}
var moveRecognizer: UIPanGestureRecognizer!

override var canBecomeFirstResponder: Bool {
return true
}

// MARK: - @IBInspectables
@IBInspectable var finishedLineColor: UIColor = UIColor.black {
didSet {
setNeedsDisplay()
}
}

@IBInspectable var currentLineColor: UIColor = UIColor.red {
didSet {
setNeedsDisplay()
}
}

@IBInspectable var lineThickness: CGFloat = 10 {
didSet {
setNeedsDisplay()
}
}

// MARK: - Gesture recognition
// Set up the gesture recognizers
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)

let doubleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(DrawView.doubleTap(_:)))
doubleTapRecognizer.numberOfTapsRequired = 2
doubleTapRecognizer.delaysTouchesBegan = true
addGestureRecognizer(doubleTapRecognizer)

let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(DrawView.tap(_:)))
tapRecognizer.numberOfTapsRequired = 1
tapRecognizer.delaysTouchesBegan = true
tapRecognizer.require(toFail: doubleTapRecognizer)
addGestureRecognizer(tapRecognizer)

let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(DrawView.longPress(_:)))
addGestureRecognizer(longPressRecognizer)

moveRecognizer = UIPanGestureRecognizer(target: self, action: #selector(DrawView.moveLine(_:)))
moveRecognizer.delegate = self
moveRecognizer.cancelsTouchesInView = false
addGestureRecognizer(moveRecognizer)

}

// Implement the Double Tap action
func doubleTap(_ gestureRecognizer: UIGestureRecognizer) {
print(“Recognized a double tap”)

selectedLineArrayIndex = nil
currentLine.removeAll()
currentLines.removeAll()
finishedLines.removeAll()

setNeedsDisplay()

}

// Implement the Tap action
func tap(_ gestureRecognizer: UIGestureRecognizer) {
print(“Recognized a tap”)

let point = gestureRecognizer.location(in: self)
selectedLineArrayIndex = indexOfLine(at: point)

let menu = UIMenuController.shared

if selectedLineArrayIndex != nil {
  becomeFirstResponder()
  
  // Disable drawing
  moveRecognizer.cancelsTouchesInView = true
  
  let deleteItem = UIMenuItem(title: "Delete", action: #selector(DrawView.deleteLine(_:)))
  menu.menuItems = [deleteItem]
  
  let startLineIndex = finishedLines[selectedLineArrayIndex!].startIndex
  let selectedLineArray = finishedLines[selectedLineArrayIndex!]
  
  let targetRect = CGRect(x: selectedLineArray[startLineIndex].begin.x, y: selectedLineArray[startLineIndex].begin.y, width: 2, height: 2)
  menu.setTargetRect(targetRect, in: self)
  menu.setMenuVisible(true, animated: true)
} else {
  menu.setMenuVisible(false, animated: true)
}

setNeedsDisplay()

}

// Implement the Long Press action
func longPress(_ gestureRecognizer: UIGestureRecognizer) {
print(“Recognized a long press”)

if gestureRecognizer.state == .began {
  let point = gestureRecognizer.location(in: self)
  selectedLineArrayIndex = indexOfLine(at: point)
  
  if selectedLineArrayIndex != nil {
    currentLines.removeAll()
  }
} else if gestureRecognizer.state == .ended {
  selectedLineArrayIndex = nil
}

setNeedsDisplay()

}

// Implement the Pan Gesture Recognizer
func moveLine(_ gestureRecognizer: UIPanGestureRecognizer) {
print(“Recognized a pan”)

if let arrayIndex = selectedLineArrayIndex {
  if gestureRecognizer.state == .changed {
    let translation = gestureRecognizer.translation(in: self)
    for (index, var line) in finishedLines[arrayIndex].enumerated() {
      line.begin.x += translation.x
      line.begin.y += translation.y
      line.end.x += translation.x
      line.end.y += translation.y
      
      let newLine = Line(begin: line.begin, end: line.end)
      finishedLines[arrayIndex].remove(at: index)
      finishedLines[arrayIndex].insert(newLine, at: index)
      
      gestureRecognizer.setTranslation(CGPoint.zero, in: self)
      
    }
    
    let startLineMenuLocationArray = finishedLines[arrayIndex]
    let startLineMenuLocationIndex = startLineMenuLocationArray.startIndex
    UIMenuController.shared.setTargetRect(CGRect(x: startLineMenuLocationArray[startLineMenuLocationIndex].begin.x, y: startLineMenuLocationArray[startLineMenuLocationIndex].begin.y, width: 2, height: 2), in: self)
    setNeedsDisplay()
  }
} else {
  return
}

}

// Implement the deleteLine function
func deleteLine(_ sender: UIMenuController) {
if let index = selectedLineArrayIndex {
finishedLines.remove(at: index)
selectedLineArrayIndex = nil

  setNeedsDisplay()
}

}

// Implement the gesture recognizer delegate method to allow simultaneous gesture recognition
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) → Bool {
return true
}

// Function to return the index of a line in the finished lines array
func indexOfLine(at point: CGPoint) → Int? {
for (index, lineArray) in finishedLines.enumerated() {
let begin = lineArray.first?.begin
let end = lineArray.last?.end

  // Check  a few points on the line
  for t in stride(from: CGFloat(0), to: 1.0, by: 0.05) {
    let x = (begin?.x)! + (((end?.x)! - (begin?.x)!) * t)
    let y = (begin?.y)! + (((end?.y)! - (begin?.y)!) * t)
    
    // If the tapped point is within 20 points, return this line
    if  hypot(x - point.x, y - point.y) < 20.0 {
      return index
    }
  }
}

// If nothing is close enough to the tapped point, then no line is selected
return nil

}

// MARK: - Drawing methods
func stroke(_ line: Line) {
let path = UIBezierPath()
path.lineWidth = lineThickness
path.lineCapStyle = .round

path.move(to: line.begin)
path.addLine(to: line.end)
path.stroke()

}

override func draw(_ rect: CGRect) {

// Draw lines
for lineArray in finishedLines {
  for line in lineArray {
    line.color.setStroke()
    stroke(line)
  }
}

for(_, lineArray) in currentLines {
  for line in lineArray {
    line.color.setStroke()
    stroke(line)
  }
}

// Color the selected line gray
if let index = selectedLineArrayIndex {
  UIColor.lightGray.setStroke()
  let selectedLineArray = finishedLines[index]
  for line in selectedLineArray {
    stroke(line)
  }
}

}

// MARK: - Touch events
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
// Log statement to see the order of events
print(#function)

for touch in touches {
  let location = touch.location(in: self)
  
  let newLine = Line(begin: location, end: location)
  currentLine.append(newLine)
  let key = NSValue(nonretainedObject: touch)
  currentLines[key] = currentLine
}
setNeedsDisplay()

}

override func touchesMoved(_ touches: Set, with event: UIEvent?) {
// Log statement to see the order of events
print(#function)

for touch in touches {
  let key = NSValue(nonretainedObject: touch)
  
  if var lineArray = currentLines[key] {
    let lastLineIndex = (lineArray.endIndex) - 1
    let newLine = Line(begin: lineArray[lastLineIndex].end, end: touch.location(in: self))
    currentLine.append(newLine)
    currentLines[key] = currentLine
  }
}
setNeedsDisplay()

}

override func touchesEnded(_ touches: Set, with event: UIEvent?) {
// Log statement to see the order of events

for touch in touches {
  let key = NSValue(nonretainedObject: touch)
  if var lineArray = currentLines[key] {
    let lastLineIndex = (currentLines[key]?.endIndex)! - 1
    let newLine = Line(begin: lineArray[lastLineIndex].end, end: touch.location(in: self))
    lineArray.append(newLine)
    
    
    finishedLines.append(lineArray)
    currentLines.removeValue(forKey: key)
    currentLine.removeAll()
  }
}
setNeedsDisplay()

}

override func touchesCancelled(_ touches: Set, with event: UIEvent?) {
// Log statement to see the order of events
print(#function)

currentLines.removeAll()
currentLine.removeAll()

setNeedsDisplay()

}
}

There is a very simple way. In override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) comment out or delete this line:
finishedLines.append(line)
We are not appending the line at the end of the touch any more.
In override func draw(_ rect: CGRect) add the same line inside of the for loop for currentLines:

for (_, line) in currentLines {
            stroke(line)
            finishedLines.append(line)
        }

Finally, in func stroke(_ Line: Line) change the line path.move(to: Line.begin) to:
path.move(to: Line.end)

We are creating single points. When the user moves over the screen slowly with the finger, a solid path will be created. When the movement is fast the line is broken up. By this way one can produce different patterns,
touchimage

Hello.

I used your solution to make continuous line drawings, but instead of getting drawings made of connected line segments like yours, my lines are made of strings of dots.

Also, each line begins with a single dot that is slightly separated from the rest of the line, as if the “pen” draws a single point, then gets picked up and put back down to make the rest of the line.
I can only select a line if I select this starting dot.

Your code seemed to be referencing:

var currentLine = Line <---- (your code)
as if it were a Line array
[Line]() <---- (my change)

var currentLines = NSValue:[Line] <---- (your code)
as if it were a dictionary whose values were Line arrays and not single Lines
[NSValue:[Line]]() <---- (my change)

var finishedLines = [Line] <---- (your code)
as if it were an array of Line arrays [[Line]]
[[Line]]() <---- (my change)

so I made those changes in order to make the code work.

I fixed this bug. Had to use a nested for loop in the func indexOfLineAtPoint to account for the array of line arrays. Now any of the dots that make up the line are selectable.

Still have not figured out how to make drawings out of connected line segments instead of dot-paths.