Why timer is getting stopped after tapping on the textfield in swift? - ios

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
}

Related

How to change images in UI according to time duration in Swift

I am doing iWatch application. In that, User has to walk for 6 minutes. In that, I am showing timer. According to that, I have to change walking images. I have change 8 images.
like image1.png, image2.png, etc to image8.png
How to change image according to time duration.
I am new to Swift language.
#IBOutlet weak var walkingImage: WKInterfaceImage!
let walkingImagesArray = ["walking1.png", "walking2.png", "walking3.png","walking4.png", "walking5.png","walking6.png", "walking7.png", "walking8.png"]
override func awake(withContext context: Any?) {
super.awake(withContext: context)
self.startTimer()
}
func startTimer() {
self.countdownTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.updateTime), userInfo: nil, repeats: true)
}
#objc func updateTime() {
count = count + 1
if(count < 361) {
timerLabel.setText(String(timeString(time: TimeInterval(count))))
//here I have to use switch case to change image
} else {
print("Workout completed")
countdownTimer.invalidate()
session.end()
builder.endCollection(withEnd: Date()) { (success, error) in
self.builder.finishWorkout { (workout, error) in
DispatchQueue.main.async() {
self.session = nil
self.builder = nil
}
}
}
}
print("\(count)")
}
Any suggestions?
In case of less than 60 seconds resulting image is named "walking0.png"
Solution:
func image(time: Int) -> String {
let sekInt = time / 60
let sek = String(sekInt)
return "walking\(sek).png"
}
self.walkingImage.setImage(UIImage(contentsOfFile: image(time:count)))
Unit Tests:
func testExample() {
XCTAssertEqual(image(time: 360), "walking6.png")
XCTAssertEqual(image(time: 359), "walking5.png")
XCTAssertEqual(image(time: 301), "walking5.png")
XCTAssertEqual(image(time: 300), "walking5.png")
XCTAssertEqual(image(time: 299), "walking4.png")
XCTAssertEqual(image(time: 61), "walking1.png")
XCTAssertEqual(image(time: 60), "walking1.png")
XCTAssertEqual(image(time: 59), "walking0.png")
XCTAssertEqual(image(time: 1), "walking0.png")
XCTAssertEqual(image(time: 0), "walking0.png")
}
You can start timer for 6 minutes and change your image every 6 minutes.
var timer: Timer?
var currentIndex: Int = 0
let walkingImagesArray = ["walking1.png", "walking2.png", "walking3.png","walking4.png", "walking5.png","walking6.png", "walking7.png", "walking8.png"]
func startTimer() {
if let timer = timer {
self.timer = timer
} else {
Timer.scheduledTimer(withTimeInterval: 360, repeats: true) { timer in
//Change your image here
self.currentIndex = self.currentIndex + 1
self. walkingImage.image = self.walkingImagesArray[self.currentIndex]
}
}
}
func stopTimer() {
self.timer?.invalidate()
self.timer = nil
}

Carrying Elapsed Time over to another ViewController

I have a small elapsed timer in my game and it works very well. However I am trying to figure out how to save the elapsed time when you die so I can carry it over to the Game Over Screen where the Score and High Score is displayed.
I tired a few things but none of them seem to work. I guess it's because the time is not being saved anywhere when the it's game over, but rather just reset to 00:00:00 when the game restarts.
I use two view Controllers for this timer. One is called Stopwatch the other code is in the GameScene. Here are the codes.
I wanna bring it into a label like for example:
let timeLabel = SKLabelNode(fontNamed: "Planer")
timeLabel.text = "Time: \(savedTimer)"
timeLabel.fontSize = 100
timeLabel.fontColor = SKColor.white
timeLabel.zPosition = 2
timeLabel.position = CGPoint (x: self.size.width/2, y: self.size.height * 0.5)
self.addChild(timeLabel)*/
Stopwatch.swift code
import Foundation
class Stopwatch {
private var startTime : Date?
var elapsedTime: TimeInterval {
if let startTime = self.startTime {
return -startTime.timeIntervalSinceNow
} else {
return 0
}
}
var isRunning: Bool {
return startTime != nil
}
func start() {
startTime = Date()
}
func stop() {
startTime = nil
}
}
And the code I got speed out through my Game Scene:
import UIKit
class ViewController: UIViewController {
private let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "mm:ss:SS"
return formatter
}()
let watch = Stopwatch()
#IBOutlet weak var elapsedTimeLabel: UILabel!
#IBAction func startButton(_ sender: Any) {
Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.updateElapsedTimeLabel), userInfo: nil, repeats: true)
watch.start()
}
#IBAction func stopButton(_ sender: Any) {
watch.stop()
}
#objc func updateElapsedTimeLabel (timer : Timer) {
if watch.isRunning {
elapsedTimeLabel.text = formatter.string(from: Date(timeIntervalSince1970: watch.elapsedTime))
} else {
timer.invalidate()
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
override var prefersStatusBarHidden: Bool {
return true
}
}
What I understand is that you're trying to save the elapsedTime of your watch after the user taps the stop button. If that's the case, in your stopButton function you are calling watch.stop(), which in turn resets the startTime = nil. So you might want to edit it like so:
// Create a new class variable to store the time
var savedTime: TimeInterval? = nil
#IBAction func stopButton(_ sender: Any) {
savedTime = watch.elapsedTime
// Use the savedTime here to pass to the game over function
watch.stop()
}
If you don't need to save the time in your ViewController class, you can move the savedTime variable to a local one in the stopButton function.

Swift call timer from ViewController

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")
}
}

Updating UILabel.text with a variable belonging to a Singleton

I have the following Singleton
class SharingManager{
var smallBigText : String = "KLANG!"
static let sharedInstance = SharingManager()
}
I use it to set the text of the following UILabel
#IBOutlet weak var kleinGrossLabel: UILabel!
I initialize its text here, in my ViewController:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.kleinGrossLabel.text = SharingManager.sharedInstance.smallBigText
}
I reset the SharingManager.sharedInstance.smallBigText in an instance method of my SoundEvent class:
class SoundEvent {
var text:String
var duration:Double
init(text: String, duration: Double){
self.text = text
self.duration = duration
}
func startEvent(){
SharingManager.sharedInstance.smallBigText = self.text
}
func getDuration() -> Double{
return self.duration
}
}
When I run the app, the UILabel text remains as "KLANG" and is never changed.
It should be changed when I call startEvent in the following function:
func playEvent(eventIndex : Int){
if (eventIndex < 2){
let currEvent = self.eventArray[eventIndex]
currEvent?.startEvent()
let nextIndex = eventIndex + 1
//NSTimer.scheduledTimerWithTimeInterval(0.4, target: SomeClass.self, selector: Selector("someClassMethod"), userInfo: nil, repeats: true)
NSTimer.scheduledTimerWithTimeInterval((currEvent?.duration)!, target: self, selector: Selector("playEvent:"), userInfo: NSNumber(integer: nextIndex), repeats: false)
}
else if (eventIndex==2){
self.eventArray[eventIndex]?.startEvent()
NSTimer.scheduledTimerWithTimeInterval(0.4, target: self, selector: Selector("sentenceDidFinish"), userInfo: nil, repeats: false)
}
else{
//Do Nothing
}
}
Which I call here in my ViewController
var s1:Sentence = Sentence(type: "S3")
s1.start()
Which in the Sentence class does this:
func start(){
self.playEvent(0)
}
Somehow it breaks this flow of logic, or if the expected sequence of events IS executing, then it follows that I am not actually changing the UILabel's text when I change the shared Singleton resource var smallBigText
For clarity here are all the main .swift
https://gist.github.com/anonymous/07542f638fc5b9a3c4e9
https://gist.github.com/anonymous/10f5f0deb03f9adc354c
https://gist.github.com/anonymous/94fda980836dc057b05b

Running a timer when the phone is sleeping

I'm building an app and I need a timer to run if the user sends the screen to the background, or if they put the phone in sleep and open it again. I need the timer to still be going.
I tried recording the time when I exit the and enter it again, subtracting the two and adding that to the running count, and it seems to work fine on the Xcode simulator but when I run it on my phone it doesn't work. Any ideas?
Here is the code for reference.
And the timer starts with a button I didn't include that part but it's just a simple IBAction that calls the timer.fire() function.
var time = 0.0
var timer = Timer()
var exitTime : Double = 0
var resumeTime : Double = 0
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
exitTime = Date().timeIntervalSinceNow
}
override func awakeFromNib() {
super.awakeFromNib()
resumeTime = Date().timeIntervalSinceNow
time += (resumeTime-exitTime)
timer.fire()
}
func startTimer() {
if !isTimeRunning {
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector:
#selector(WorkoutStartedViewController.action), userInfo: nil, repeats: true)
isTimeRunning = true
}
}
func pauseTimer() {
timer.invalidate()
isTimeRunning = false
}
#objc func action()
{
time += 0.1
timerLabel.text = String(time)
let floorCounter = Int(floor(time))
let hour = floorCounter/3600
let minute = (floorCounter % 3600)/60
var minuteString = "\(minute)"
if minute < 10 {
minuteString = "0\(minute)"
}
let second = (floorCounter % 3600) % 60
var secondString = "\(second)"
if second < 10 {
secondString = "0\(second)"
}
if time < 3600.0 {
timerLabel.text = "\(minuteString):\(secondString)"
} else {
timerLabel.text = "\(hour):\(minuteString):\(secondString)"
}
}
You do have the right idea but the first problem I see is that viewWillDissapear is only called when you leave a view controller to go to a new viewController - It is not called when the app leaves the view to enter background (home button press)
I believe the callback functions you are looking for are UIApplication.willResignActive (going to background) and UIApplication.didBecomeActive (app re-opened)
You can access these methods in the AppDelegate or you can set them up on a view controller heres a mix of your code and some changes to produce a working sample on one initial VC:
import UIKit
import CoreData
class ViewController: UIViewController {
#IBOutlet weak var timerLabel: UILabel!
var time = 0.0
var timer = Timer()
var exitTime : Date? // Change to Date
var resumeTime : Date? // Change to Date
var isTimeRunning = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
startTimer()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self,
selector: #selector(applicationDidBecomeActive),
name: UIApplication.didBecomeActiveNotification,
object: nil)
// Add willResign observer
NotificationCenter.default.addObserver(self,
selector: #selector(applicationWillResign),
name: UIApplication.willResignActiveNotification,
object: nil)
}
override func viewWillDisappear(_ animated: Bool) {
// Remove becomeActive observer
NotificationCenter.default.removeObserver(self,
name: UIApplication.didBecomeActiveNotification,
object: nil)
// Remove becomeActive observer
NotificationCenter.default.removeObserver(self,
name: UIApplication.willResignActiveNotification,
object: nil)
}
func startTimer() {
if !isTimeRunning {
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector:
#selector(self.action), userInfo: nil, repeats: true)
isTimeRunning = true
}
}
#objc func action() {
time += 0.1
timerLabel.text = String(time)
let floorCounter = Int(floor(time))
let hour = floorCounter/3600
let minute = (floorCounter % 3600)/60
var minuteString = "\(minute)"
if minute < 10 {
minuteString = "0\(minute)"
}
let second = (floorCounter % 3600) % 60
var secondString = "\(second)"
if second < 10 {
secondString = "0\(second)"
}
if time < 3600.0 {
timerLabel.text = "\(minuteString):\(secondString)"
} else {
timerLabel.text = "\(hour):\(minuteString):\(secondString)"
}
}
#objc func applicationDidBecomeActive() {
// handle event
lookForActiveTimers()
}
func lookForActiveTimers() {
var timers = [NSManagedObject]()
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Timers")
//3
do {
timers = try managedContext.fetch(fetchRequest)
print("timers: \(timers)")
var activeTimer: NSManagedObject?
for timer in timers {
if let active = timer.value(forKey: "active") as? Bool {
if active {
activeTimer = timer
}
}
}
if let activeTimer = activeTimer {
// Handle active timer (may need to go to a new view)
if let closeDate = activeTimer.value(forKey: "appCloseTime") as? Date {
if let alreadyTimed = activeTimer.value(forKey: "alreadyTimed") as? Double {
let now = Date()
let difference = now.timeIntervalSince(closeDate)
// Handle set up again here
print("App opened with a difference of \(difference) and already ran for a total of \(alreadyTimed) seconds before close")
time = alreadyTimed + difference
startTimer()
}
}
} else {
print("We dont have any active timers")
}
// Remove active timers because we reset them up
for timer in timers {
managedContext.delete(timer)
}
do {
print("deleted")
try managedContext.save() // <- remember to put this :)
} catch {
// Do something... fatalerror
}
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
}
#objc func applicationWillResign() {
// handle event
saveActiveTimer()
}
func saveActiveTimer() {
if isTimeRunning {
// Create a new alarm object
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let context = appDelegate.persistentContainer.viewContext
if let entity = NSEntityDescription.entity(forEntityName: "Timers", in: context) {
let newTimer = NSManagedObject(entity: entity, insertInto: context)
newTimer.setValue(true, forKey: "active")
let now = Date()
newTimer.setValue(now, forKey: "appCloseTime")
newTimer.setValue(self.time, forKey: "alreadyTimed")
do {
try context.save()
print("object saved success")
} catch {
print("Failed saving")
}
}
}
}
}
EDIT - Here is the full tested and working code on xCode 11.3 and a physical device iOS 13.2 - You have to figure out how to start and stop the timer according to your buttons - but this example simply starts the timer when the app is first opened and never stops or resets it.
You can reproduce this by creating a new single-view xCode project and replacing the code in the first view controller that it creates for you with the code above. Then create a label to attach to the outlet timerLabel on the VC
Also make sure to enable CoreData in your project while creating your new project * Then set up the entities and attributes in the xcdatamodel file:
Hope this helps

Resources