Stopping timer at defined amount of time in Swift

后端 未结 1 1313
刺人心
刺人心 2021-01-25 07:21

I am trying to build a stopwatch which, for instance, will count to 3.0 seconds, stop, and then allow me to override the app\'s view with a new background/label. My issue is I c

1条回答
  •  梦谈多话
    2021-01-25 07:56

    Start by decoupling your expectations.

    A "clock" is a container for the period of time from which it was started to now. Additionally, it could be "restarted", so it may need to know how long each previous run cycle was, this would then be added into the overall duration of the "clock"

    The Timer is simply a way to run some code on periodical bases. Because a Timer only guarantees "at least" period, it should avoid been used for simple counter addition, as it can cause drift in your calculations (for a simple clock, it's probably not a big deal, but if you need any kind of precision, it's best to avoid it)

    SimpleClock

    import Foundation
    
    public class SimpleClock {
        
        internal var startedAt: Date? = nil
        internal var totalRunningTime: TimeInterval = 0 // Used for pause/resume
        
        var isRunning: Bool = false {
            didSet {
                if isRunning {
                    startedAt = Date()
                } else {
                    totalRunningTime += currentCycleDuration
                    self.startedAt = nil
                }
            }
        }
        
        // This is the amount of time that this cycle has been running,
        // that is, the amount of time since the clock was started to now.
        // It does not include other cycles
        internal var currentCycleDuration: TimeInterval {
            guard let startedAt = startedAt else {
                return 0
            }
            return Date().timeIntervalSince(startedAt)
        }
        
        func reset() {
            isRunning = false
            totalRunningTime = 0
        }
        
        // This is the "total" amount of time the clock has been allowed
        // to run for, excluding periods when the clock was paused
        var duration: TimeInterval {
            return totalRunningTime + currentCycleDuration
        }
        
    }
    

    Okay, this is pretty basic concept. It's just a container for recording when a "cycle" starts and stops and managing the "overall" duration (start/pause/resume cycles)

    That's all fine and good, but what we really want is some way to determine if the period has "timeout" or not.

    AlarmClock

    import Foundation
    
    class AlarmClock: SimpleClock {
        
        var timeout: TimeInterval = 0
        
        var hasExpired: Bool {
            return duration >= timeout
        }
        
        var timeRemaining: TimeInterval {
            return max(timeout - duration, 0)
        }
        
    }
    

    All this does is add a concept of a "timeout" period and provides some additional functionality that allows use to easily determine if the clock has expired and the amount of time remaining

    Example

    Okay, that's all nice a good, but how does this work (and help us)

    Okay, this is a really simple example. It has a label and two buttons. One button starts/pauses the clock and the other resets it.

    The label displays both the running time and the remaining time of the alarm clock. If he clock expires, it will automatically be reset.

    The class contains a Timer which periodically "ticks" and allows the code to inspect that current state of the alarm clock.

    import UIKit
    
    class ViewController: UIViewController {
        
        @IBOutlet weak var durationLabel: UILabel!
        @IBOutlet weak var cycleButton: UIButton!
        @IBOutlet weak var resetButton: UIButton!
        
        let alarmClock: AlarmClock = {
            let clock = AlarmClock()
            clock.timeout = 10.0
            return clock
        }()
        
        var timer: Timer? = nil
        
        var durationFormatter: DateComponentsFormatter {
            let formatter = DateComponentsFormatter()
            formatter.allowedUnits = [.minute, .second]
            formatter.unitsStyle = .abbreviated
            return formatter
        }
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
        }
    
        override func didReceiveMemoryWarning() {
            super.didReceiveMemoryWarning()
            // Dispose of any resources that can be recreated.
        }
    
        @IBAction func cycleClock(_ sender: Any) {
            alarmClock.isRunning = !alarmClock.isRunning
            if alarmClock.isRunning {
                timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(tick), userInfo: nil, repeats: true)
            } else {
                timer?.invalidate()
                timer = nil
            }
            updateDurationLabel()
            updateCycleButtonState()
        }
        
        @IBAction func restartClock(_ sender: Any) {
            timer?.invalidate()
            timer = nil
            
            alarmClock.reset()
            updateDurationLabel()
            updateCycleButtonState()
        }
        
        func updateCycleButtonState() {
            if alarmClock.isRunning {
                cycleButton.setTitle("Pause", for: [])
            } else {
                cycleButton.setTitle("Start", for: [])
            }
        }
        
        func updateDurationLabel() {
            durationLabel.text = "\(durationFormatter.string(from: alarmClock.duration)!)/\(durationFormatter.string(from: alarmClock.timeRemaining)!)"
        }
        
        @objc func tick() {
            print("click")
            updateDurationLabel()
            if alarmClock.hasExpired {
                restartClock(self)
            }
        }
    }
    

    Now, you could also add some kind of "internal" thread to periodically check the state of the clock and call a delegate which could then bee used to update the UI, but the intention here is the decoupling of the concerns, and this means you're not adding yet another thread to the system unnecessarily (not saying you couldn't do, but it's just one more level of complexity I didn't want to add ;))

    0 讨论(0)
提交回复
热议问题