Kevin Vugts Kevin Vugts - 1 month ago 7
Swift Question

Start Timer in other viewcontroller in swift

I have a problem with my timer functions. I start a timer when clicking on a btn (startBtn) and I show a new viewcontroller as a popover when hitting another btn (restBtn) Right now it start the timer correctly and opens the new viewcontroller as popover with the time counting down.

The problem is when in the new viewcontroller it hits 0 and dismisses hisself. It should start the timer in the first viewcontrolle automatically.

Here is my code on mainVC:

var timeLeft = 0
var myTimer: Timer!


@IBAction func startBtnPressed(_ sender: AnyObject) {
timeLeft = 0
myTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(workoutStartVC.timerRunning), userInfo: nil, repeats: true)
}


@IBAction func restBtnPressed(_ sender: AnyObject) {
print("rest mode button is pressed and i am showing a overlay right now with data count down")
myTimer.invalidate()

// show popup when user interacts with restbtn
let popOpVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "sbPopUpID") as! PopUpVC
self.addChildViewController(popOpVC)
popOpVC.view.frame = self.view.frame
popOpVC.view.backgroundColor = BLUE_COLOR.withAlphaComponent(0.75)
self.view.addSubview(popOpVC.view)
popOpVC.didMove(toParentViewController: self)

timeLbl.text = ""
}

func timerRunning() {
timeLeft = timeLeft + 1
print("the timeLeft is: \(timeLeft)")

**timeLbl.text = "\(timeLeft)"** (HERE I AM GETTING ON THE SECOND TIME WHEN DISMISSING THE POPUPVC A FOUND NILL)

if timeLeft == 30 {
myTimer.invalidate()
timeLbl.text = ""
}
}


Here is my code on the PopUpVC:

var timeLeft = 0
var myTimer: Timer!

override func viewDidLoad() {
super.viewDidLoad()

timeLeft = 10
myTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(PopUpVC.timerRunning), userInfo: nil, repeats: true)
}


func timerRunning() {
timeLeft = timeLeft - 1
restModeTimer.text = "\(timeLeft)"

if timeLeft == 0 {
myTimer.invalidate()
restModeTimer.text = ""
self.view.removeFromSuperview()

let worskoutStartVC = workoutStartVC()
worskoutStartVC.myTimer = nil
worskoutStartVC.timeLeft = 0

worskoutStartVC.myTimer = Timer.scheduledTimer(timeInterval: 1.0, target: worskoutStartVC, selector: #selector(worskoutStartVC.timerRunning), userInfo: nil, repeats: true)
}
}


Can anybody explains why I get a found nil while unwrapping an option value at the part that I made bold in my code below?

Thank you very much!

Answer

From what I am seeing, it seems like you are initializing a new View Controller... in the line:

let worskoutStartVC = workoutStartVC()

So when this gets created, the outlet to the label is not connected in the same way that that it would when the view is initialized from a Storyboard.

So I see 2 things you might want to try:

  1. (Probably the best option) Pass in a reference to the original view controller to the second view controller, so that you don't have to create it again. And then you use that reference to recreate the timer. Because what you are doing now is essentially creating a new view controller every time that the PopUpVC's timer ends. More details at the end of the post

  2. (This would result in multiple copies of the initial VC, so it is more for helping understand what is happening) You could create another copy of the view controller using UIStoryboard() like you did in the code above and that should have the outlet connected properly.

As your code says:

// show popup when user interacts with restbtn
    let popOpVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "sbPopUpID") as! PopUpVC
    self.addChildViewController(popOpVC)

Again, option 1 is IMO the best choice, although there are likely other patterns to consider. And the reason the label is nil, is because it isn't connected to a label object when you create the view with workoutStartVC().

EDIT: Add info on how to pass in the reference:

The PopUpVC code might change to having a reference to the original VC:

var timeLeft = 0
var myTimer: Timer!
var parentVC: workoutStartVC?

override func viewDidLoad() {
    super.viewDidLoad()

    timeLeft = 10
    myTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(PopUpVC.timerRunning), userInfo: nil, repeats: true)
}


func timerRunning() {
    timeLeft = timeLeft - 1
    restModeTimer.text = "\(timeLeft)"

    if timeLeft == 0 {
        myTimer.invalidate()
        restModeTimer.text = ""
        self.view.removeFromSuperview()

        if let worskoutStartVC = workoutStartVC {
            worskoutStartVC.myTimer = nil
            worskoutStartVC.timeLeft = 0

            worskoutStartVC.myTimer = Timer.scheduledTimer(timeInterval: 1.0, target: worskoutStartVC, selector: #selector(worskoutStartVC.timerRunning), userInfo: nil, repeats: true)
        }
    }
}  

And you would have to set this property when you transition into this view: (Fragment)

        // show popup when user interacts with restbtn
        let popOpVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "sbPopUpID") as! PopUpVC
        self.addChildViewController(popOpVC)
        popOpVC.parentVC = self
        popOpVC.view.frame = self.view.frame
        popOpVC.view.backgroundColor = BLUE_COLOR.withAlphaComponent(0.75)
        self.view.addSubview(popOpVC.view)
        popOpVC.didMove(toParentViewController: self)

NOTE: Tried my best to adjust class names to what I assume they are from your code, but some things might need to be adjusted.

Comments