How to make a timer reset after button gesture in a different view controller? - swift

I am developing a quiz app where the user is asked a question at random and must answer it on the initial view controller. If the user picks correctly, a second view controller appears which contains a button that pops the view controller off the navigation stack and goes back to the initial view controller to finish the other questions. However, I have a timer that I want to reset (start at 15s) every time that second view controller is popped and the initial view controller appears with the next question. How would I accomplish this task? I already have the countdown timer code in my swift file. I just need to know how to get it to start from scratch every time the second view controller is popped/removed.
Here's my code for the initial view controller:
import UIKit
extension ViewController: QuizCompletedDelegate {
func continueQuiz() {
randomQuestion()
}
}
class ViewController: UIViewController {
var questionList = [String]()
func updateCounter() {
counter -= 1
questionTimer.text = String(counter)
if counter == 0 {
timer.invalidate()
wrongSeg()
counter = 15
}
}
func randomQuestion() {
//random question
if questionList.isEmpty {
questionList = Array(QADictionary.keys)
}
let rand = Int(arc4random_uniform(UInt32(questionList.count)))
questionLabel.text = questionList[rand]
//matching answer values to go with question keys
var choices = QADictionary[questionList[rand]]!
questionList.remove(at: rand)
//create button
var button:UIButton = UIButton()
//variables
var x = 1
rightAnswerBox = arc4random_uniform(4)+1
for index in 1...4 {
button = view.viewWithTag(index) as! UIButton
if (index == Int(rightAnswerBox)) {
button.setTitle(choices[0], for: .normal)
} else {
button.setTitle(choices[x], for: .normal)
x += 1
}
randomImage()
}
}
let QADictionary = ["Who is Thor's brother?" : ["Atum", "Loki", "Red Norvell", "Kevin Masterson"], "What is the name of Thor's hammer?" : ["Mjolinr", "Uru", "Stormbreaker", "Thundara"], "Who is the father of Thor?" : ["Odin", "Sif", "Heimdall", "Balder"]]
//wrong view segue
func wrongSeg() {
performSegue(withIdentifier: "incorrectSeg", sender: self)
}
//proceed screen
func rightSeg() {
performSegue(withIdentifier: "correctSeg", sender: self)
}
//variables
var rightAnswerBox:UInt32 = 0
var index = 0
//Question Label
#IBOutlet weak var questionLabel: UILabel!
//Answer Button
#IBAction func buttonAction(_ sender: AnyObject) {
if (sender.tag == Int(rightAnswerBox)) {
rightSeg()
print ("Correct!")
}
if counter != 0 {
counter = 15
questionTimer.text = String(counter)
} else if (sender.tag != Int(rightAnswerBox)) {
wrongSeg()
print ("Wrong!")
timer.invalidate()
questionList = []
}
}
override func viewDidAppear(_ animated: Bool) {
randomQuestion()
}
//variables
var counter = 15
var timer = Timer()
#IBOutlet weak var questionTimer: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
timer = Timer.scheduledTimer(timeInterval: 1, target:self, selector: #selector(ViewController.updateCounter), userInfo: nil, repeats: true)
}
}
Here's the code to the second view controller:
class ContinueScreen: UIViewController {
var delegate: QuizCompletedDelegate?
//correct answer label
#IBOutlet weak var correctLbl: UILabel!
//background photo
#IBOutlet weak var backgroundImage: UIImageView!
func backToQuiz() {
delegate?.continueQuiz()
if let nav = self.navigationController {
nav.popViewController(animated: true)
} else {
self.dismiss(animated: true, completion: nil)
}
}
#IBAction func `continue`(_ sender: Any) {
backToQuiz()
}
override func viewDidLoad() {
super.viewDidLoad()
}
}

How to make countdown timer reset after button push on different view
controller?
So if you can get the reference of yourView controller where you have
setup the timer then you can access counter variable from previous
view controller inside your second viewController and when pushed the
button just reset to 0.
If above approach is not possible for you then another way is create
the static variable and access it anywhere and reset like this:-
static var counter = 0

if you are popping/removing the 2nd view controller and thus returning to the first, you could reset the timer in your viewDidAppearfunc in your 1st view controller. this way the timer restarts every time the view appears - the first time, and whenever the 2nd pops.

Related

Swift: another timer starting when application enters background

I've been struggling to figure this out for a few days: I need to create a timer that the user can't kill, so once they start it, even if they kill the app or it enters background, it will pick up where it left off, and to achieve this I am saving the date when the app terminated and then calculating the difference, so that part works fine.
The problem I keep running into however is that a second timer seems to start if I minimise the app and bring it back. I'm not sure how timers are managed in the background, but nothing of what I tried (calling timer.invalidate() when applicationWillResignActive gets called, calling it in deinit() ) seems to work. The behaviour I see after this is that the timer will count like this: 80 - 78 - 79 - 76 - 77..
There's also a problem where the timer will sometime run past the time it's supposed to run for after killing the app, but I can't find the exact cause for that because it doesn't always happen.
Any idea what I'm doing wrong?
Thanks a lot.
class Focus: UIViewController {
// MARK: Variables
var timer = Timer()
let timeToFocus = UserDefaults.standard.double(forKey: "UDTimeToFocus")
let currentFocusedStats = UserDefaults.standard.integer(forKey: "UDFocusStats")
// MARK: Outlets
#IBOutlet weak var progress: KDCircularProgress!
#IBOutlet weak var timeLabel: UILabel!
#IBOutlet weak var focusTimeLabel: UILabel!
#IBOutlet weak var stepNameLabel: UILabel!
#IBOutlet weak var focusAgain: UIButton!
#IBOutlet weak var allDone: UIButton!
#IBOutlet weak var help: UIButton!
#IBOutlet weak var dottedCircle: UIImageView!
// MARK: Outlet Functions
#IBAction func helpTU(_ sender: Any) { performSegue(withIdentifier: "ToFocusingHelp", sender: nil) }
#IBAction func helpTD(_ sender: Any) { help.tap(shape: .rectangle) }
#IBAction func allDoneTU(_ sender: Any) {
UserDefaults.standard.set(false, forKey: "UDFocusIsRunning")
UserDefaults.standard.set(false, forKey: "UDShouldStartFocus")
completeSession()
hero(destination: "List", type: .zoomOut)
}
#IBAction func allDoneTD(_ sender: Any) { allDone.tap(shape: .rectangle) }
#IBAction func focusAgainTU(_ sender: Any) {
UserDefaults.standard.set(currentFocusedStats + Int(timeToFocus), forKey: "UDFocusStats")
UserDefaults.standard.set(true, forKey: "UDShouldStartFocus")
initFocus()
}
#IBAction func focusAgainTD(_ sender: Any) { focusAgain.tap(shape: .rectangle) }
// MARK: Class Functions
#objc func initFocus() {
var ticker = 0.0
var angle = 0.0
var duration = 0.0
if UserDefaults.standard.bool(forKey: "UDShouldStartFocus") == true {
UserDefaults.standard.set(Date(), forKey: "UDFocusStartDate")
UserDefaults.standard.set(false, forKey: "UDShouldStartFocus")
ticker = timeToFocus
duration = timeToFocus
angle = 0.0
print("starting")
} else {
let elapsedTime = difference(between: UserDefaults.standard.object(forKey: "UDFocusStartDate") as! Date, and: Date())
let timeLeft = timeToFocus - elapsedTime
ticker = timeLeft
duration = timeLeft
angle = elapsedTime / (timeToFocus / 360)
}
// Timer
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
if ticker > 0 {
self.timeLabel.text = "\(Int(ticker))s"
ticker -= 1
}
}
timer.fire()
// Progress Circle
progress.animate(fromAngle: angle, toAngle: 360, duration: duration) { completed in
if completed { self.completeSession() }
}
// UI Changes
allDone.isHidden = true
focusAgain.isHidden = true
help.isHidden = false
}
func completeSession() {
// The timer gets fired every time, but this will invalidate it if it's complete
timer.invalidate()
timeLabel.text = "Done"
help.isHidden = true
allDone.isHidden = false
focusAgain.isHidden = false
}
// MARK: viewDidLoad
override func viewDidLoad() {
initFocus()
allDone.isHidden = true
focusAgain.isHidden = true
if timeToFocus < 3600 { focusTimeLabel.text = "Focusing for \(Int(timeToFocus/60)) minutes" }
else if timeToFocus == 3600 { focusTimeLabel.text = "Focusing for \(Int(timeToFocus/60/60)) hour" }
else { focusTimeLabel.text = "Focusing for \(Int(timeToFocus/60/60)) hours" }
stepNameLabel.text = UserDefaults.standard.string(forKey: "UDSelectedStep")
// This resumes the timer when the user sent the app in the background.
NotificationCenter.default.addObserver(self, selector: #selector(self.initFocus), name: NSNotification.Name(rawValue: "WillEnterForeground"), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.fadeProgress), name: NSNotification.Name(rawValue: "WillEnterForeground"), object: nil)
}
#objc func fadeProgress(){
// This function is called both when the view will enter foreground (for waking the phone or switching from another app) and on viewWillAppear (for starting the app fresh). It will fade the progress circle and buttons to hide a flicker that occurs.
timeLabel.alpha = 0
dottedCircle.alpha = 0
progress.alpha = 0
allDone.alpha = 0
focusAgain.alpha = 0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: {
UIButton.animate(withDuration: 0.5, animations: {
self.timeLabel.alpha = 1
self.dottedCircle.alpha = 1
self.progress.alpha = 1
self.allDone.alpha = 1
self.focusAgain.alpha = 1
})
})
}
// MARK: viewWillAppear
override func viewWillAppear(_ animated: Bool) { fadeProgress() }
}
It seems the problem is that you create a local timer variable inside initFocus() but you call invalidate inside completeSession for another timer defined there:
class Focus: UIViewController {
// MARK: Variables
var timer = Timer()

Swift Delegates not working for updating timer to receiver

I have created protocol as updateTime protocol to update timer to other view controllers conform to updateTime delegates but the timer not updating to other controllers.
Please refer below code and pin point where I did wrong?
protocol UpdateTime: class {
func updateTIme(count: Int)
}
class ViewController: UIViewController, UpdateTime {
#IBOutlet var timeLabel: UILabel!
let secVC = SecondViewController()
override func viewDidLoad() {
super.viewDidLoad()
secVC.delegate = self
// Do any additional setup after loading the view.
}
// must be internal or public.
func updateTIme(count: Int){
print("firstVC: \(count)")
DispatchQueue.main.async {
self.timeLabel.text = "\(count)"
}
}
}
class Counter {
var count = 0
func increment() {
count += 1
}
func increment(by amount: Int) -> Int{
count += amount
print(count)
return count
}
func reset() {
count = 0
}
}
This is seconviewcontroller:
class SecondViewController: UIViewController {
#IBOutlet var label: UILabel!
weak var delegate: UpdateTime?
var counter = Counter()
var timer = Timer()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
DispatchQueue.main.async {
self.timer.invalidate()
self.timer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(self.countDown), userInfo: nil, repeats: true)
}
}
#objc func countDown() -> Int{
// counter increments
counter.increment()
let count = counter.increment(by: 5)
DispatchQueue.main.async {
self.label.text = String(describing: count)
}
self.delegate?.updateTIme(count: count)
return count
}
}
The label in view controller not updating count down.
This looks like a lifecycle issue. Though you are creating an instance of SecondViewController, the timer from the second view controller only gets added to the run loop when that view controller's viewDidLoad method is called. Since you are programmatically creating it in the first view controller, there is nowhere in your example that would cause the second view controller's view to get loaded, so the timer would not be added to the runloop.
A better way of accomplishing this issue is to create the timer on the first view controller directly, or create a secondary object (not a second view controller) that manages the timer and have it push updates to the first view controller

How to random position from UIButtons has created by storyboard?[swift Xcode]

I created 12 UIButtons by storyboard, in my program, these buttons represent cards,I want to random the buttons' position for shuffling the cards.please do me a favor, give a way or idea to achieve it.
my buttons problem
import UIKit
class ViewController: UIViewController {
lazy var game = Concentration(numberOfPairsOfCards: numberOfPairOfCards) //引入model Concentration
var numberOfPairOfCards : Int {
return (cardButtons.count + 1) / 2 //read only computed properties可以省略get{}
}
var flipCount = 0 {
didSet {
flipsCountLabel.text = "Flips: \(flipCount)"
}
}
#IBOutlet var cardButtons: [UIButton]!
#IBOutlet weak var flipsCountLabel: UILabel!
#IBAction func touchCard(_ sender: UIButton) {
flipCount += 1
if let cardNumber = cardButtons.index(of: sender) {
game.chooseCard(at: cardNumber)
updateViewFromModel()
} else {
print("somthing wrong")
}
}
I have done the job.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
var buttons = [UIButton]()
for index in cardButtons.indices {
buttons.append(cardButtons[index])
}
for index in cardButtons.indices {
cardButtons[index] = buttons.remove(at: buttons.count.arc4random)
}
}
You could add the buttons to an array, shuffle the array and then loop through it and place the buttons. A better solution would be to create a UICollectionView instead though.

How to keep track of items answered on in a label?

In my quiz app, if I wanted to keep track of the question number the user was on and display it, how would I do so with my following code? So if the user has answered two questions when the third appears I want it to display "question number 3". Basically I want the user to know what number question they are on and it should be equivalent to the number of questions they've answered plus one.
Here's my code:
import UIKit
class ViewController: UIViewController {
var questionList = [String]()
func updateCounter() {
counter -= 1
questionTimer.text = String(counter)
if counter == 0 {
timer.invalidate()
wrongSeg()
}
}
func randomQuestion() {
//random question
if questionList.isEmpty {
questionList = Array(QADictionary.keys)
}
let rand = Int(arc4random_uniform(UInt32(questionList.count)))
questionLabel.text = questionList[rand]
//matching answer values to go with question keys
var choices = QADictionary[questionList[rand]]!
questionList.remove(at: rand)
//create button
var button:UIButton = UIButton()
//variables
var x = 1
rightAnswerBox = arc4random_uniform(4)+1
for index in 1...4
{
button = view.viewWithTag(index) as! UIButton
if (index == Int(rightAnswerBox))
{
button.setTitle(choices[0], for: .normal)
}
else {
button.setTitle(choices[x], for: .normal)
x += 1
}
}
randomImage()
}
let QADictionary = ["Who is Thor's brother?" : ["Atum", "Loki", "Red Norvell", "Kevin Masterson"], "What is the name of Thor's hammer?" : ["Mjolinr", "Uru", "Stormbreaker", "Thundara"], "Who is the father of Thor?" : ["Odin", "Sif", "Heimdall", "Balder"]]
//wrong view segue
func wrongSeg() {
performSegue(withIdentifier: "incorrectSeg", sender: self)
}
//proceed screen
func rightSeg() {
performSegue(withIdentifier: "correctSeg", sender: self)
}
//variables
var rightAnswerBox:UInt32 = 0
var index = 0
//Question Label
#IBOutlet weak var questionLabel: UILabel!
//Answer Button
#IBAction func buttonAction(_ sender: AnyObject) {
if (sender.tag == Int(rightAnswerBox))
{
rightSeg()
timer.invalidate()
print ("Correct!")
}
if counter != 0 {
counter = 15
}
else if (sender.tag != Int(rightAnswerBox)) {
wrongSeg()
print ("Wrong!")
timer.invalidate()
questionList = []
}
}
override func viewDidAppear(_ animated: Bool)
{
randomQuestion()
questionTimer.text = String(counter)
timer = Timer.scheduledTimer(timeInterval: 1, target:self, selector: #selector(ViewController.updateCounter), userInfo: nil, repeats: true)
}
//variables
var counter = 15
var timer = Timer()
#IBOutlet weak var questionTimer: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
Take counter for total number of question globally and update it in random question like
var answerdQuestion = 1;
func randomQuestion() {
//random question
if questionList.isEmpty {
questionList = Array(QADictionary.keys)
}
lblQuestionNumber.text = Strint(answerdQuestion)
answerdQuestion += 1
:
:
}

How to pass data backwards to a view controller after passing forward?

Im developing a quiz app and there is a second view controller that appears after the initial view controller where you are asked to answer a question. On the second view controller you the user must press a button to return to the initial view controller to be asked another question. However when I segue back from the second view controller I believe a new instance of the initial view controller is being created and the user is asked questions they have already answered. The code in the swift file for my initial view controller is constructed that when once a user is asked a question once that question is removed from an array by it's index. How could I make it to where a new instance of the initial view controller isn't created from segueing from the second view controller? Or is there a way the second view controller can access the same methods as the initial view controller?
Here is my code for the initial view controller:
import UIKit
class ViewController: UIViewController {
var questionList = [String]()
func updateCounter() {
counter -= 1
questionTimer.text = String(counter)
if counter == 0 {
timer.invalidate()
wrongSeg()
counter = 15
}
}
func randomQuestion() {
//random question
if questionList.isEmpty {
questionList = Array(QADictionary.keys)
questionTimer.text = String(counter)
}
let rand = Int(arc4random_uniform(UInt32(questionList.count)))
questionLabel.text = questionList[rand]
//matching answer values to go with question keys
var choices = QADictionary[questionList[rand]]!
questionList.remove(at: rand)
//create button
var button:UIButton = UIButton()
//variables
var x = 1
rightAnswerBox = arc4random_uniform(4)+1
for index in 1...4
{
button = view.viewWithTag(index) as! UIButton
if (index == Int(rightAnswerBox))
{
button.setTitle(choices[0], for: .normal)
}
else {
button.setTitle(choices[x], for: .normal)
x += 1
}
randomImage()
}
}
let QADictionary = ["Who is Thor's brother?" : ["Atum", "Loki", "Red Norvell", "Kevin Masterson"], "What is the name of Thor's hammer?" : ["Mjolinr", "Uru", "Stormbreaker", "Thundara"], "Who is the father of Thor?" : ["Odin", "Sif", "Heimdall", "Balder"]]
//wrong view segue
func wrongSeg() {
performSegue(withIdentifier: "incorrectSeg", sender: self)
}
//proceed screen
func rightSeg() {
performSegue(withIdentifier: "correctSeg", sender: self)
}
//variables
var rightAnswerBox:UInt32 = 0
var index = 0
//Question Label
#IBOutlet weak var questionLabel: UILabel!
//Answer Button
#IBAction func buttonAction(_ sender: AnyObject) {
if (sender.tag == Int(rightAnswerBox))
{
rightSeg()
print ("Correct!")
}
if counter != 0 {
counter = 15
questionTimer.text = String(counter)
}
else if (sender.tag != Int(rightAnswerBox)) {
wrongSeg()
print ("Wrong!")
timer.invalidate()
questionList = []
}
randomQuestion()
}
override func viewDidAppear(_ animated: Bool)
{
randomQuestion()
}
//variables
var counter = 15
var timer = Timer()
#IBOutlet weak var questionTimer: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
timer = Timer.scheduledTimer(timeInterval: 1, target:self, selector: #selector(ViewController.updateCounter), userInfo: nil, repeats: true)
}
Here's the code to the second view controller:
import UIKit
class ContinueScreen: UIViewController {
//correct answer label
#IBOutlet weak var correctLbl: UILabel!
//background photo
#IBOutlet weak var backgroundImage: UIImageView!
func backToQuiz() {
if let nav = self.navigationController {
nav.popViewController(animated: true)
}
else {
self.dismiss(animated: true, completion: nil)
}
}
#IBAction func `continue`(_ sender: Any) {
backToQuiz()
}
What you need, sir is a delegate or an unwind segue. I far prefer delegates because they're easier to understand and I think a bit more powerful.
In your ContinueScreen view controller define a protocol similar to this:
protocol QuizCompletedDelegate {
func completedQuiz()
func canceledQuiz()
}
Also in your ContinueScreen view controller you need to declare an optional delegate of type QuizCompletedDelegate
var delegate: QuizCompletedDelegate?
... and use that delegate to call the functions when appropriate:
#IBAction func didPressContinue(_ sender: Any) {
if allQuestionsAnswered == true {
delegate.completedQuiz()
} else {
delegate.cancelledQuiz()
}
}
In your initial view controller is where you implement the functions for the protocol:
extension ViewController: QuizCompletedDelegate {
func completedQuiz() {
//Handle quiz complete
}
func cancelledQuiz() {
//Handle quiz cancelled
}
}
Then the last thing you need to do is set the delegate of your ContinueScreen view controller in the prepareForSegue function of your initial view controller.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showContinueScreen" {
let continueVC = segue.destination as! ContinueScreen
continueVC.delegate = self
}
}
Remove the call to randomQuestion() on your initial view controller's ViewDidAppear, and you're in business!
It shouldn't create a new View Controller when you go back. If it does - it's very bad - and you should re-architect the flow of your app... To pass data forward, we should use Dependency Injection. To pass data to the previous view controller - use the iOS Delegation Pattern. I recommend to spend 30-40 mins to read (and understand this article), and it will make things more clear for you in the future: http://matteomanferdini.com/how-ios-view-controllers-communicate-with-each-other/.