What is the best method in preventing this Optional-related crash?

134 views Asked by At

Context

I am beginner learning Swift and I'm trying to make sure I understand this bug "fix". I followed along a Youtube video showing how to create a timer in Xcode. I changed some things around in effort to learn some things but the issue that arose was from the original code.

The Bug

Every time I started the simulator, if I pressed the Stop or Reset button without first starting the timer, the app would crash and show this error on the same line as timer.invalidate():

Unexpectedly found nil while implicitly unwrapping an Optional value

My Fix

To fix the issue I added ? to make it timer?.invalidate() as seen in the code below. I tried this after some Googling to get a very loose understanding of what is going on.

Did this work because the timer did not exist yet and therefore it was "nil"?

Is this the best way to prevent the crash? It seems that I could also change the variable timer to use ? instead and have it work, but only after adding ? to the timer.invalidate() line in the step function as well as the others. Clearly I need to brush up on my understanding of Optionals.

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var label: UILabel!
    
    
    var timeRemaining: Int = 10
    var timer: Timer!
    

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        
    }

    @IBAction func start(_ sender: Any) {
        timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(step), userInfo: nil, repeats: true)
        
    }
    
    @IBAction func stop(_ sender: Any) {
        timer?.invalidate() //added ? to timer
    }
    
    @IBAction func reset(_ sender: Any) {
        timer?.invalidate() //added ? to timer
        timeRemaining = 10
        label.text = "\(timeRemaining)"
    }
    
    @objc func step() {
        if timeRemaining > 0 {
            timeRemaining -= 1
        } else {
            timer.invalidate()
        }
        label.text = "\(timeRemaining)"
    }
    

}
2

There are 2 answers

2
AnderCover On BEST ANSWER
var timer: Timer!

Means that you're telling the compiler that you are sure that timer will never be nil, so if you try to use the variable and it is nil you got a crash. This is probably what happened to you: a call to reset(_:) or stop(_:) before a start(_:).

timer?.invalidate() //added ? to timer

fix the crash because now you are accessing timer through optional chaining.

But it does not solve the potential issue everywhere so declaring:

var timer: Timer?

is safer

0
Duncan C On

Don't declare your timer as an implicitly unwrapped optional. Change

var timer: Timer! 

to

weak var timer: Timer?

(Replace the exclamation point with a question mark.)

Then yes, change your code to read timer?.invalidate()

Edit:

Note that your button code should invalidate the timer as well:

@IBAction func start(_ sender: Any) {
    timer?.invalidate()
    timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(step), userInfo: nil, repeats: true)        
}

If you don't invalidate your timer before creating a new one, you could have multiple instances of your timer running at the same time, and when you replace the one in the variable timer with a new one you don't have access to the previous one any more to stop it.