I have a TimerManager class that I would like to access in multiple ViewControllers but I can't figure out a good way to do it. My code is as follows:
class TimerManager {
private var timer: NSTimer
private var timeRemaining: Int
init(initialTime: Int) {
self.timer = NSTimer()
self.timeRemaining = initialTime
}
func startTimer() {
self.timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: #selector(TimerManager.update), userInfo: nil, repeats: true)
}
func endTimer() {
self.timer.invalidate()
}
func getTimeRemaining() -> Int {
return self.timeRemaining
}
#objc func update() {
if self.timeRemaining > 0 {
self.timeRemaining = self.timeRemaining - 1
}
else {
endTimer()
}
}
}
In my ViewController I would like to be able to access my update() function to update a timer (which is a UILabel) on my actual page, but since my startTimer() function calls it every second, I don't know how to access update() every time it is called. I briefly looked into protocols but I'm not really sure how they work or if that would be useful in my case.
Any help would be appreciated!
As #sschale suggested, you can do this by using a singleton to ensure that you will be accessing the same instance anywhere in your code. To do this, you need to set the init to private and provide a static member variable to access your single instance.
class TimerManager
{
static let sharedInstance = TimerManager()
private var timer: NSTimer
private var timeRemaining: Int
private init()
{
let initialTime = 1
self.timer = NSTimer()
self.timeRemaining = initialTime
}
private init(initialTime: Int)
{
self.timer = NSTimer()
self.timeRemaining = initialTime
}
...
}
Then in your ViewControllers you can just call it like this:
TimerManager.sharedInstance.startTimer()
class TimerManager {
private var timer: NSTimer
private var timeRemaining: Int
private var intervalBlock: (TimerManager -> ())?
init(initialTime: Int) {
self.timer = NSTimer()
self.timeRemaining = initialTime
}
func startTimer(intervalBlock: (TimerManager -> ())? = nil) {
self.intervalBlock = self
self.timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: #selector(TimerManager.update), userInfo: nil, repeats: true)
}
func endTimer() {
self.intervalBlock = nil
self.timer.invalidate()
}
func getTimeRemaining() -> Int {
return self.timeRemaining
}
#objc func update() {
if self.timeRemaining > 0 {
self.timeRemaining = self.timeRemaining - 1
intervalBlock()
}
else {
intervalBlock()
endTimer()
}
}
}
Below is one of the best implementations of Timer on the background queue I found from this article
class RepeatingTimer {
let timeInterval: TimeInterval
init(timeInterval: TimeInterval) {
self.timeInterval = timeInterval
}
private lazy var timer: DispatchSourceTimer = {
let t = DispatchSource.makeTimerSource()
t.schedule(deadline: .now() + self.timeInterval, repeating: self.timeInterval)
t.setEventHandler(handler: { [weak self] in
self?.eventHandler?()
})
return t
}()
var eventHandler: (() -> Void)?
private enum State {
case suspended
case resumed
}
private var state: State = .suspended
deinit {
timer.setEventHandler {}
timer.cancel()
resume()
eventHandler = nil
}
func resume() {
if state == .resumed {
return
}
state = .resumed
timer.resume()
}
func suspend() {
if state == .suspended {
return
}
state = .suspended
timer.suspend()
}
}
Usage: -
In any of your ViewControllers
For example: -
class MyViewController: UIViewController {
// MARK: - Properties
var timer: RepeatingTimer!
// MARK: - ViewController LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
timer = RepeatingTimer(timeInterval: 1)
timer.eventHandler = {
print("Timer called")
}
}
Related
I want to set a timer of 30 seconds to send an OTP to a phone number. And while the timer is running, the resend OTP button should be disabled. Once the timer ends, the resend OTP button should get enabled and the timer label should be hidden. Onclick of the resend OTP button, the same process should continue.
In the code that I have written, the timer label is hidden the entire time and is constantly going into the if loop where it is constantly printing "invalidated".
Below is the code that I have written.
Updated Code:
#IBOutlet weak var timerLabel: UILabel!
#IBOutlet weak var resendOTPBtn: UIButton!
var countdownTimer: Timer!
var totalTime = 30
override func viewDidLoad() {
super.viewDidLoad()
if AFWrapper.isConnectedToInternet() {
timerLabel.text = ""
sendOTPCode()
sendOTPAPICall()
resendOTPBtn.isEnabled = false
} else {
self.textPopupAlert(title: ALERT_TITLE, message: OFFLINE_MSG)
}
}
// in case user closed the controller
deinit {
countdownTimer.invalidate()
}
#objc func updateTimerLabel() {
totalTime -= 1
timerLabel.text = "\(timeFormatted(totalTime))"
if totalTime == 0 {
timerLabel.text = ""
countdownTimer.invalidate()
resendOTPBtn.isEnabled = true
}
}
#IBAction func resendOTPBtnClicked(_ sender: Any) {
if AFWrapper.isConnectedToInternet() {
sendOTPAPICall()
timerLabel.text = ""
totalTime = 31
resendOTPBtn.isEnabled = false
sendOTPCode()
}
else {
self.textPopupAlert(title: ALERT_TITLE, message: OFFLINE_MSG)
}
}
func sendOTPCode() {
self.countdownTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.updateTimerLabel), userInfo: nil, repeats: true)
}
Initial Code:
#IBAction func resendOTPBtnClicked(_ sender: Any) {
if AFWrapper.isConnectedToInternet() {
sendOTPAPICall()
countDownTime()
}
else {
self.textPopupAlert(title: ALERT_TITLE, message: OFFLINE_MSG)
}
}
func countDownTime() {
DispatchQueue.main.asyncAfter(deadline: .now() + 60) {
self.timerLabel.isHidden = false
self.resendOTPBtn.isEnabled = false
if self.timerLabel.isHidden == false {
self.startTimer()
} else {
self.countdownTimer.invalidate()
self.resendOTPBtn.isEnabled = true
self.timerLabel.isHidden = true
}
}
}
// Method to start the timer when resend OTP button is clicked.
func startTimer() {
countdownTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateTime), userInfo: nil, repeats: true)
}
#objc func updateTime() {
DispatchQueue.main.async(){
self.timerLabel.text = self.timeFormatted(self.totalTime)
if self.totalTime != 0 {
self.totalTime -= 1
} else {
print("Invalidated")
self.endTimer()
}
}
}
func timeFormatted(_ totalSeconds: Int) -> String {
let seconds: Int = totalSeconds % 60
let minutes: Int = (totalSeconds / 60) % 60
// let hours: Int = totalSeconds / 3600
return String(format: "%02d:%02d", minutes, seconds)
}
Any solutions would be appreciated. Thank you!
#IBOutlet weak var resendCodeTimerLabel: UILabel!
#IBOutlet weak var resendCodeButton: UIButton!
var resendCodeCounter = 30
var resendCodeTimer = Timer()
override func viewDidLoad() {
super.viewDidLoad()
resendCodeTimerLabel.text = ""
sendOTPCode()
}
// in case user closed the controller
deinit {
resendCodeTimer.invalidate()
}
#objc func updateTimerLabel() {
resendCodeCounter -= 1
resendCodeTimerLabel.text = "Resend code in \(resendCodeCounter) seconds."
if resendCodeCounter == 0 {
resendCodeButton.isEnabled = true
resendCodeTimer.invalidate()
}
}
#IBAction func resendAgainButtonClicked(_ sender: UIButton) {
OTPTextField.text = ""
resendCodeCounter = 31
resendCodeButton.isEnabled = false
sendOTPCode()
}
func sendOTPCode() {
//Whatever your api logic
if otpSent {
self.resendCodeTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.updateTimerLabel), userInfo: nil, repeats: true)
}
}
I want to run timer in tableViewHeader part, the tableView header is containing textField ,label and button. To manage the timer I have created singleton class and it's working fine in the initial but when I tap on textField the timer getting stopped. I don't why. Should I manage it using separate thread?
//singleton class
class WorkoutTimerManager {
static let shared = WorkoutTimerManager()
private var sec = 0
private var min = 0
private var timer = Timer()
private var date: Date? = Date()
var timerLbl:UILabel?
private init() {
}
func startTimer() {
timer.invalidate()
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
}
func stopTimer() {
timer.invalidate()
}
#objc func updateTimer() {
if let date = self.date {
let elapsedSec = abs(Int(date.timeIntervalSinceNow))
self.sec = elapsedSec % 60
self.min = elapsedSec / 60
print("sdfs1 \(sec)")
if let timerLbl = timerLbl {
timerLbl.text = "\(sec)"
print("sdfs2 \(sec)")
}
}
}
}
// table view headerview delegate method
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let name = "EmptyWorkoutHeader"
guard
let nib = Bundle.main.loadNibNamed(name, owner: nil, options: nil)
else { fatalError("missing expected nib named: \(name)") }
guard
let headerView = nib.first as? EmptyWorkoutHeader
else { fatalError("view of type \(name) not found in \(nib)") }
WorkoutTimerManager.shared.timerLbl = headerView.timerLbl
WorkoutTimerManager.shared.startTimer()
return headerView
}
I'm building an app in Swift 3. When I press start the first time my timer begins, but when I pause it and try to press start again, the timer does not budge. To give context, the timer, with an amount of time attached to it, is selected from a table. each time the timer load, the start button works initially.
protocol TimerViewControllerDelegate: class {
func viewController(_ controller: ViewController, didFinishEditing item: TaskData)
}
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var timerLabel: UILabel!
#IBOutlet weak var pauseButton: UIButton!
#IBOutlet weak var startButton: UIButton!
#IBOutlet weak var timerTaskName: UILabel!
#IBOutlet weak var timerTimeSetting: UILabel!
#IBOutlet weak var progressView: UIProgressView!
weak var delegate: TimerViewControllerDelegate?
var timerTask: TaskData?
var timer: Timer?
var progressViewSpeed: Double = 0.0
#IBAction func cancel(_ sender: Any) {
timer?.invalidate()
dismiss(animated: true, completion: nil)
delegate?.viewController(self, didFinishEditing: timerTask!)
}
#IBAction func startButtonTapped(_ sender: Any) {
timerTask?.startTime = Date()
runTimer()
if timerTask?.isTaskRunning == true {
runTimer()
self.startButton.isEnabled = false
self.pauseButton.isEnabled = true
} else {
//retrieve start time and run
timerTask?.startTime = Date()
runTimer()
self.startButton.isEnabled = false
self.pauseButton.isEnabled = true
}
}
func runTimer() {
guard timer == nil else {
return
}
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(ViewController.updateTimer)), userInfo: nil, repeats: true)
}
#IBAction func pauseButtonTapped(_ sender: UIButton) {
if timerTask?.isTaskRunning == true {
timer?.invalidate()
if let timerTask = timerTask, timerTask.isTaskRunning {
// Calculate the difference between now and when the timerTask was started
let difference = Int(Date().timeIntervalSince(timerTask.startTime!))
timerTask.taskRemaining -= difference
if timerTask.taskRemaining == 0 {
// Do something when there's no time remaining on the task?
}
timerTask.startTime = nil
}
}
else {
timerTask?.startTime = Date()
runTimer()
self.pauseButton.setTitle("Pause",for: .normal)
}
self.startButton.isEnabled = true
self.pauseButton.isEnabled = false
}
/*
#IBAction func resetButtonTapped(_ sender: Any) {
timer.invalidate()
seconds = 60
self.timerLabel.text = timeString(time: TimeInterval(seconds))
if self.resumeTapped == true {
self.resumeTapped = false
self.pauseButton.setTitle("Pause",for: .normal)
}
isTimerRunning = false
pauseButton.isEnabled = false
startButton.isEnabled = true
}
*/
func updateTimer() {
guard let timerTask = timerTask else {
return
}
if timerTask.taskRemaining < 1 {
timer?.invalidate()
timer = nil
//Send alert to indicate "time's up!"
} else {
updateTime()
}
progressViewSpeed = 1 / Double(timerTask.taskRemaining)
progressView.progress += Float(progressViewSpeed)
}
func timeString(time:TimeInterval) -> String {
let hours = Int(time) / 3600
let minutes = Int(time) / 60 % 60
let seconds = Int(time) % 60
return String(format:"%02i:%02i:%02i", hours, minutes, seconds)
}
override func viewDidLoad() {
super.viewDidLoad()
guard let timerTask = timerTask else {
return
}
if timerTask.isTaskRunning {
startButton.isEnabled = false
pauseButton.isEnabled = true
runTimer()
} else {
startButton.isEnabled = true
pauseButton.isEnabled = false
}
timerTaskName.text = timerTask.task
updateTime()
self.progressView.transform = CGAffineTransform.identity.rotated(by: CGFloat.pi / 2).scaledBy(x: 1, y: 150)
}
func updateTime() {
guard let timerTask = timerTask else {
return
}
if let startTime = timerTask.startTime {
// Calculate the difference between now and when the timerTask was started
let difference = Int(Date().timeIntervalSince(startTime))
if timerTask.taskRemaining == difference {
// Do something when there's no time remaining on the task
timer?.invalidate()
timer = nil
}
timerLabel.text = timeString(time: TimeInterval(timerTask.taskRemaining - difference))
} else {
timerLabel.text = timeString(time: TimeInterval(timerTask.taskRemaining))
}
}
}
Once you've invalidated an NSTimer, you can't use it again. You should create the new object.
See here for more From NSTimer Docs
Calling this method requests the removal of the timer from the current run loop; as a result, you should always call the invalidate method from the same thread on which the timer was installed. Invalidating the timer immediately disables it so that it no longer affects the run loop. The run loop then removes and releases the timer, either just before the invalidate method returns or at some later point. Once invalidated, timer objects cannot be reused.
You need to invalidate it and recreate it. "isPaused" bool to keep track of the state
var isPaused = true
var timer: Timer?
#IBAction func pauseResume(sender: AnyObject) {
if isPaused{
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(ViewController.updateTimer)), userInfo: nil, repeats: true)
pauseButton.isHidden = false
startButton.isHidden = true
isPaused = false
} else {
pauseButton.isHidden = true
startButton.isHidden = false
timer.invalidate()
isPaused = true
}
}
i have code where i add one constant every second to my array, how can i update my progress bar every second when array is changed?
var array: [Int] = []
override func viewWillAppear(_ animated: Bool) {
upgradeArray()
}
func upgradeArray() {
for i in 0...10 {
sleep(1)
array.append(i)
print(i)
let percentProgress = Float(Float(self.array.count)*100.0/10.0)
progressBar.setProgress(percentProgress, animated: true)
}
}
Use a timer. Assuming your progress view is 0 to 1 and incrementing by 0.1 each second...
var timer: Timer?
var array = [String]()
func doStuff() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { timer in
self.array.append("foo")
let change: Float = 0.1
self.progressView.progress = self.progressView.progress + (change)
if self.progressView.progress >= 1.0 {
self.timer?.invalidate()
}
})
}
done with DispatchQueue.global(priority: .default).async
func upgradeArray() {
for i in 0...10 {
DispatchQueue.global(priority: .default).async {
//sleep(1)
self.array.append(i)
print(i)
DispatchQueue.main.async(execute: {
let percentProgress = Float(Float(self.array.count)*100.0/10.0)
self.progressBar.setProgress(percentProgress, animated: true)
})
}
}
}
I've built a CountdownTimer which i would like to refactor into a separate class so i can reuse it in the MainViewController. How would i go about doing that?
This is my code:
var startTime = NSTimeInterval()
var time:Double = 4
var timer = NSTimer()
/* Outlets */
#IBOutlet weak var timerLabel: UILabel!
/* CountdownTimer function */
func updateTime() {
var currentTime = NSDate.timeIntervalSinceReferenceDate()
var elapsedTime = currentTime - startTime
var seconds = time - elapsedTime
if seconds > 0 {
elapsedTime -= NSTimeInterval(seconds)
timerLabel.text = "\(Int(seconds))"
} else {
timer.invalidate()
timerLabel.fadeOut()
}
}
func startTimer () {
if !timer.valid {
let aSelector : Selector = "updateTime"
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: aSelector, userInfo: nil, repeats: true)
startTime = NSDate.timeIntervalSinceReferenceDate()
}
}
You can use a closure:
class MyTimer: NSObject {
var startTime: NSTimeInterval! = NSTimeInterval()
var time: Double! = 4
var timer: NSTimer! = NSTimer()
var timerEndedCallback: (() -> Void)!
var timerInProgressCallback: ((elapsedTime: Double) -> Void)!
func startTimer(timerEnded: () -> Void, timerInProgress: ((elapsedTime: Double) -> Void)!) {
if !timer.valid {
let aSelector : Selector = "updateTime"
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: aSelector, userInfo: nil, repeats: true)
startTime = NSDate.timeIntervalSinceReferenceDate()
timerEndedCallback = timerEnded
timerInProgressCallback = timerInProgress
}
}
func updateTime() {
var currentTime = NSDate.timeIntervalSinceReferenceDate()
var elapsedTime = currentTime - startTime
var seconds = time - elapsedTime
if seconds > 0 {
elapsedTime -= NSTimeInterval(seconds)
timerInProgressCallback(elapsedTime: elapsedTime)
} else {
timer.invalidate()
timerEndedCallback()
}
}
}
Usage, in the class you want to use your generic timer:
MyTimer().startTimer({ () -> Void in
timerLabel.fadeOut()
}, timerInProgress: { (elapsedTime) -> Void in
timerLabel.text = "\(Int(elapsedTime))"
})
swift 4 version of tbaranes's answer:
class MyTimer: NSObject {
var startTime: TimeInterval! = TimeInterval()
var time: Double! = 4
var timer: Timer! = Timer()
var timerEndedCallback: (() -> Void)!
var timerInProgressCallback: ((_ elapsedTime: Double) -> Void)!
func startTimer(timerEnded: #escaping () -> Void, timerInProgress: ((_ elapsedTime: Double) -> Void)!) {
if !timer.isValid {
let aSelector : Selector = #selector(MyTimer.updateTime)
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: aSelector, userInfo: nil, repeats: true)
startTime = NSDate.timeIntervalSinceReferenceDate
timerEndedCallback = timerEnded
timerInProgressCallback = timerInProgress
}
}
#objc func updateTime() {
var currentTime = NSDate.timeIntervalSinceReferenceDate
var elapsedTime = currentTime - startTime
var seconds = time - elapsedTime
if seconds > 0 {
elapsedTime -= TimeInterval(seconds)
timerInProgressCallback(elapsedTime)
} else {
timer.invalidate()
timerEndedCallback()
}
}
}
Based on #tbaranes's answer and because the edit queue is full, here's an updated version that works on SWIFT 5 with some tweaks
protocol CountdownTimerProtocol {
func stopCountdown()
func startCountdown(totalTime: Int, timerEnded: #escaping () -> Void, timerInProgress: #escaping (Int) -> Void)
}
class CountdownTimer: NSObject, CountdownTimerProtocol {
private var timer: Timer?
private var timeRemaining = 0
var timerEndedCallback: (() -> Void)?
var timerInProgressCallback: ((Int) -> Void)?
deinit {
stopCountdown()
}
func stopCountdown() {
timer?.invalidate()
}
func startCountdown(totalTime: Int, timerEnded: #escaping () -> Void, timerInProgress: #escaping (Int) -> Void) {
timeRemaining = totalTime
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(step), userInfo: nil, repeats: true)
timerEndedCallback = timerEnded
timerInProgressCallback = timerInProgress
}
#objc func step() {
if timeRemaining > 0 {
timeRemaining -= 1
timerInProgressCallback?(timeRemaining)
} else {
stopCountdown()
timerEndedCallback?()
}
}
}
How to call it, totalTime is measured in seconds, also remember to invalidate() the timer on deinit {} of this class 👇🏽
CountdownTimer().startCountdown(
totalTime: 30,
timerEnded: {
print("Countdown is over")
}, timerInProgress: { elapsedTime in
print(elapsedTime)
}
)
To use your countdownTimer in a separate class, create a new class and implement the class in your MainViewController and then access the methods.
Alternatively just make your functions global.
XCode->File->New->Source->Cocoa Touch Class extends from NSObject
import UIKit
class TimerTest: NSObject {
var myTimer:NSTimer?
override init() {
super.init()
print("init worked")
myTimer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "timerFuncTriggered:", userInfo: nil, repeats: true)
}
func timerFuncTriggered(timer:NSTimer) {
print("timer started")
}
}
you can use your timer class anytime like:
let myTimer = TimerTest()