How to fix timer for drawing initiated stopwatch?

Hi there I am completing some research for Alzheimers. And have realised I need to make this app - i have very little experience. I want to be able to record the time it takes to complete a drawing. Both the time spend with apple pencil on tablet and the time spent overall to complete a drawing (time on tablet plus time in between strokes).

I have created this application so far but can't get the timer to work.

I have the drawing/scribble board down pat.

I have tried many different approaches but I am just not experienced enough in code to work out why it is not starting the timer when the apple pencil hits the tablet. The code below is for the ViewController script.

import UIKit

class ViewController: UIViewController {


@IBOutlet weak var canvasView: CanvasView!
override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
}

@IBAction func clearCanvas(_ sender: Any) {  //this button will clear canvas and clear timers!
    canvasView.clearCanvas()
    timer.invalidate()

    seconds = 0    //Here we manually enter the restarting point for the seconds, but it would be wiser to make this a variable or constant.
    timeDrawing.text = "\(seconds)"
}
@IBOutlet weak var timeDrawing: UILabel! //This is one of the label used to display the time drawing (i have yet to add the label to display the time inbetween strokes)

var seconds = 0 
var timer = Timer()
var isTimerRunning = false //This will be used to make sure only one timer is created at a time.
var resumeTapped = false
var touchPoint:CGPoint!
var touches:UITouch!

func runTimer() {
    timer = Timer.scheduledTimer(timeInterval: 1, target: self,   selector: (#selector(ViewController.updateTimer)), userInfo: nil, repeats: true)
}

@objc func updateTimer() {
        seconds += 1     
    timeDrawing.text = "\(seconds)" //This will update the label.
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let touch = touches.first else { return }
    let touches = touch.location(in: canvasView) // or maybe ...(in: self)

    if touch.type == .pencil  {
       if !isTimerRunning {
            runTimer()
        } else {
            timer.invalidate()
        }
}
}
}

I was hoping when the apple pencil touched the tablet it would start the timer. And then when the pencil left the tablet it would stop one of the timers. (i have yet to add another timer for the time in between strokes, any help with that would be appreciated too.)

Thank you so much guys, the world depends on it!

You don’t need a timer to measure the amount of time something takes. Rather, you need timestamps. That is, you take a timestamp at the start, take a timestamp at the end, and subtract the first from the second.

The easiest way to deal with timestamps is using Date. For example:

let start1 = Date()
NSLog("Hello Cruel World!")
let end1 = Date()
let start2 = Date()
print("Hello Cruel World!")
let end2 = Date()

print(end1.timeIntervalSince(start1))   // 0.0007849931716918945
print(end2.timeIntervalSince(start2))   // 0.0014280080795288086

[Incidentally, the fact that NSLog was faster than print, even if for only this single data point, was very surprising to me.]

The only problem with using Date is that you’ll see weird behaviours if the system clock changes. You can get around this using lower-level APIs — for example, clock_gettime with CLOCK_MONOTONIC_RAW, see its man page for details — but you can probably gloss over that point until you get more things working.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

You don’t need a timer to measure the amount of time something takes.

Hmmm, I re-read your question and it’s clear that the above, while true, is not the whole story. You will need a timer for updating your elapsed time label. My recommendation is that you disconnect this from your time accounting. That is, using timestamps, as discussed in my earlier post, to account for time, and then just using the timer to render the current elapsed time to a label.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

OK, you made me try it. In Xcode 11, in a Mac playground, I got:

0.0033289194107055664
0.0002410411834716797

In a Mac app in debug config, I got:

9.894371032714844e-05
3.3020973205566406e-05

:thinking:

ok so I have re done it with time labels but now how do I do what you said above to make it so that it will store the time drawing:

this is the stopwatch swift file

import Foundation

class Stopwatch {
    
    // MARK: Private Properties
    
    private let step: Double
    private var timer: Timer?
    
    //The time when counting was started
    private(set) var from: Date?
    //The time when counting was stopped
    private(set) var to: Date?
    
    // The time when user pause timer last one
    private var timeIntervalTimelapsFrom: TimeInterval?
    // The total time before user paused timer
    private var timerSavedTime: TimeInterval = 0
    
    typealias TimeUpdated = (_ time: Double)->Void
    let timeUpdated: TimeUpdated
    
    // MARK: Public Properties
    
    var isPaused: Bool {
        return timer == nil
    }
    
    //MARK: Initialization
    
    init(step: Double = 1.0, timeUpdated: @escaping TimeUpdated) {
        self.step = step
        self.timeUpdated = timeUpdated
    }
    
    deinit {
        print("⏱ Stopwatch successfully deinited")
        deinitTimer()
    }
    
    func toggle() {
        guard timer != nil else {
            initTimer()
            return
        }
        deinitTimer()
    }
    
    func stop() {
        deinitTimer()
        from = nil
        to = nil
        timerSavedTime = 0
        timeUpdated(0)
    }
    
    private func initTimer() {
        let action: (Timer)->Void = { [weak self] timer in
            guard let strongSelf = self else {
                return
            }
            let to = Date().timeIntervalSince1970
            let timeIntervalFrom = strongSelf.timeIntervalTimelapsFrom ?? to
            let time = strongSelf.timerSavedTime + (to - timeIntervalFrom)
            strongSelf.timeUpdated(round(time))
        }
        if from == nil {
            from = Date()
        }
        if timeIntervalTimelapsFrom == nil {
            timeIntervalTimelapsFrom = Date().timeIntervalSince1970
        }
        timer = Timer.scheduledTimer(withTimeInterval: step,
                                     repeats: true, block: action)
    }
    
    private func deinitTimer() {
        //Saving last timelaps
        if let timeIntervalTimelapsFrom = timeIntervalTimelapsFrom {
            let to = Date().timeIntervalSince1970
            timerSavedTime += to - timeIntervalTimelapsFrom
        }
        //Invalidating
        timer?.invalidate()
        timer = nil
        timeIntervalTimelapsFrom = nil
    }
}

this is the drawing view file:


import UIKit

/*
 TODO: choose what needed: drawwing without delay or smoothing
 */
class DrawingView: UIView {
    
    //MARK: Properties
    var lineColor: UIColor!
    var lineWidth: CGFloat! = 5 {
        didSet {
            bezierPath.lineWidth = lineWidth
        }
    }
    
    private var bufferImages: [UIImage] = []
    
    private var bezierPath: UIBezierPath!
    private var bufferImage: UIImage?
    private var bezierCurvePoints: [CGPoint] = []
    private var isCleanNeeded = false

    var timer: Stopwatch!

    //MARK: Outlets
    
    @IBOutlet private weak var undo: UIButton?

    //MARK: Init
    override init(frame: CGRect) {
        super.init(frame: frame)
        initialize()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        initialize()
    }
    
    private func initialize() {
//        backgroundColor = .clear
        isMultipleTouchEnabled = false
        bezierPath = UIBezierPath()
        //        bezierPath.usesEvenOddFillRule = true
        bezierPath.lineJoinStyle = .round
        bezierPath.lineCapStyle = .round
        bezierPath.lineWidth = lineWidth
        addGestureRecognizer()
    }
    
    //MARK: Draw
    
    private func addGestureRecognizer() {
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(viewDragged(_:)))
        addGestureRecognizer(panGesture)
    }
    
    override func draw(_ rect: CGRect) {
        guard !isCleanNeeded else {
            initialize()
            isCleanNeeded = false
            return
        }
        bufferImage?.draw(in: rect)
        drawLine()
    }
    
    private func drawLine() {
        lineColor?.setStroke()
        bezierPath.close()
        bezierPath.stroke()
    }
    
    @IBAction private func clean() {
        isCleanNeeded = true
        bufferImage = nil
        bufferImages = []
        self.setNeedsDisplay()
        undo?.isEnabled = false
    }
    
    @IBAction private func back() {
        if !bufferImages.isEmpty {
            initialize()
            bufferImages.removeLast()
            bufferImage = bufferImages.last
            setNeedsDisplay()
            
            if bufferImages.isEmpty {
                undo?.isEnabled = false
            }
        }
    }
    
    private var lastPoint: CGPoint!         // A point for storing the last position
    private var pointCounter: Int = 0       // A counter of points
    private let pointLimit: Int = 32       // A limit of the points
    
    @objc func viewDragged(_ sender: UIPanGestureRecognizer) {
        let point = sender.location(in: self)
        guard point.x > 0, point.x < frame.width, point.y > 0, point.y < frame.height else {
            return
        }
        switch sender.state {
        case .began:
            lastPoint = point
            pointCounter = 0
            timer.toggle()
        case .changed:
            bezierPath.move(to: lastPoint)
            bezierPath.addLine(to: point)
            lastPoint = point
            pointCounter += 1
            
            if pointCounter == pointLimit {
                pointCounter = 0
                saveBufferImage()
                setNeedsDisplay()
                bezierPath.removeAllPoints()
            }
            else {
                setNeedsDisplay()
            }
        case .ended:
            pointCounter = 0
           saveBufferImage()
            setNeedsDisplay()
            if let bufferImage = bufferImage {
                bufferImages.append(bufferImage)
                undo?.isEnabled = true
            }
          //  bezierPath.removeAllPoints()
            timer.stop()
        default:
            break
        }
    }
    
    /**
     Save line and update view
     */
    private func saveBufferImage() {
        UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
        if bufferImage != nil {
            bufferImage?.draw(in: bounds)
        }
        drawLine()
        bufferImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
    }
    
    func isEmpty() -> Bool {
        return bufferImage == nil && bezierPath.isEmpty
    }
}