Elapsed Timer not starting - ios

I am building an elapsed timer and while the code gives no errors the timer does not start.
I am using two ViewControllers, one called Stopwatch which has the start stop function in it under the class Stopwatch() and then a regular ViewController with the rest in it.
View Controller Code:
import UIKit
class ViewController: UIViewController {
let watch = Stopwatch()
#IBOutlet weak var elapsedTimeLabel: UILabel!
#IBAction func startButton(_ sender: Any) {
Timer.scheduledTimer(timeInterval: 0.1, 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 {
let minutes = Int (watch.elapsedTime/60)
let seconds = watch.elapsedTime.truncatingRemainder(dividingBy: 60)
let tenOfSeconds = (watch.elapsedTime * 10).truncatingRemainder(dividingBy: 10)
elapsedTimeLabel.text = String (format: "%02d:%02d:%02d", minutes, seconds, tenOfSeconds)
} else {
timer.invalidate()
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override var prefersStatusBarHidden: Bool {
return true
}
}
The Stopwatch View Controller 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
}
}
There is nothing at all coming in the debug window, so not sure what the issue is, I reconnected the buttons over and over so it's not that. I also get no other errors in the code as mentioned above.
Can anyone shed some light on this. Maybe I am using the wrong #selector or I am doing the updateElapsedTimeLabel minutes, seconds, tenOfSeconds calculations wrong. Not sure. Thanks for having a look.

If you Option-click on seconds and tenOfSeconds you will find that one is of type TimeInterval (i.e. Double) and the other is of type Double. So your format specifier of %02d was wrong. In C, a mismatch between the format specifier and the argument is undefined behavior. Swift doesn't say how it handles that but I guess it will ignore the argument.
To fix it, change your format specifier for the last 2 components to %02.f:
let minutes = Int(watch.elapsedTime/60)
let seconds = watch.elapsedTime.truncatingRemainder(dividingBy: 60)
let tenOfSeconds = (watch.elapsedTime * 100).truncatingRemainder(dividingBy: 100) // fixed the math here
elapsedTimeLabel.text = String(format: "%02d:%02.f:%02.f", minutes, seconds, tenOfSeconds)
But why not use a DateFormatter to make your life simpler:
class ViewController: UIViewController {
private let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "mm:ss:SS"
return formatter
}()
#objc func updateElapsedTimeLabel (timer : Timer) {
if watch.isRunning {
elapsedTimeLabel.text = formatter.string(from: Date(timeIntervalSince1970: watch.elapsedTime))
} else {
timer.invalidate()
}
}
}

Related

How to subtract current time from a target time?

I figured out how to display the live current time in "hh:mm:ss" (code below). I want the label to display the time left until a specific target time, like a countdown. I want it to count down until e.g. 3pm each day and then start over. I know I need to subtract my target time from the current time but I don't know how to do that.
(I am new to programming)
class ViewController: UIViewController {
#IBOutlet weak var Label: UILabel!
var timer = Timer()
override func viewDidLoad() {
super.viewDidLoad()
getCurrentTime()
}
private func getCurrentTime() {
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector:#selector(self.currentTime) , userInfo: nil, repeats: true)
}
#objc func currentTime() {
let formatter = DateFormatter()
formatter.dateFormat = "hh:mm:ss"
Label.text = formatter.string(from: Date())
}
}
Use timeIntervalSince to find TimeInterval between two dates. Like below
let secondsBetween: TimeInterval = targetDate.timeIntervalSince(currentDate)
self.counter = Int(secondsBetween)
func timerAction() {
counter -= 1
label.text = timeString(time: TimeInterval(counter))
}

Metronome ios swift beat visuals lag

I'm trying to create an metronome app by implementing the sample code provided by apple. Everything works fine but i'm seeing an delay in the beat visuals its not properly synchronised with the player time. Here is the sample code provided by apple
let secondsPerBeat = 60.0 / tempoBPM
let samplesPerBeat = Float(secondsPerBeat * Float(bufferSampleRate))
let beatSampleTime: AVAudioFramePosition = AVAudioFramePosition(nextBeatSampleTime)
let playerBeatTime: AVAudioTime = AVAudioTime(sampleTime: AVAudioFramePosition(beatSampleTime), atRate: bufferSampleRate)
// This time is relative to the player's start time.
player.scheduleBuffer(soundBuffer[bufferNumber]!, at: playerBeatTime, options: AVAudioPlayerNodeBufferOptions(rawValue: 0), completionHandler: {
self.syncQueue!.sync() {
self.beatsScheduled -= 1
self.bufferNumber ^= 1
self.scheduleBeats()
}
})
beatsScheduled += 1
if (!playerStarted) {
// We defer the starting of the player so that the first beat will play precisely
// at player time 0. Having scheduled the first beat, we need the player to be running
// in order for nodeTimeForPlayerTime to return a non-nil value.
player.play()
playerStarted = true
}
let callbackBeat = beatNumber
beatNumber += 1
// calculate the beattime for animating the UI based on the playerbeattime.
let nodeBeatTime: AVAudioTime = player.nodeTime(forPlayerTime: playerBeatTime)!
let output: AVAudioIONode = engine.outputNode
let latencyHostTicks: UInt64 = AVAudioTime.hostTime(forSeconds: output.presentationLatency)
//calcualte the final dispatch time which will update the UI in particualr intervals
let dispatchTime = DispatchTime(uptimeNanoseconds: nodeBeatTime.hostTime + latencyHostTicks)**
// Visuals.
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: dispatchTime) {
if (self.isPlaying) {
// send current call back beat.
self.delegate!.metronomeTicking!(self, bar: (callbackBeat / 4) + 1, beat: (callbackBeat % 4) + 1)
}
}
}
// my view controller class where i'm showing the beat number
class ViewController: UIViewController ,UIGestureRecognizerDelegate,Metronomedelegate{
#IBOutlet var rhythmlabel: UILabel!
//view did load method
override func viewDidLoad() {
}
//delegate method for getting the beat value from metronome engine and showing in the UI label.
func metronomeTicking(_ metronome: Metronome, bar: Int, beat: Int) {
DispatchQueue.main.async {
print("Playing Beat \(beat)")
//show beat in label
self.rhythmlabel.text = "\(beat)"
}
}
}
I think you are approaching this a bit too complex for no reason. All you really need is to set a DispatchTime when you start the metronome, and fire a function call whenever the DispatchTime is up, update the dispatch time based on the desired frequency, and loop as long as the metronome is enabled.
I prepared a project for you which implements this method so you can play with and use as you see fit: https://github.com/ekscrypto/Swift-Tutorial-Metronome
Good luck!
Metronome.swift
import Foundation
import AVFoundation
class Metronome {
var bpm: Float = 60.0 { didSet {
bpm = min(300.0,max(30.0,bpm))
}}
var enabled: Bool = false { didSet {
if enabled {
start()
} else {
stop()
}
}}
var onTick: ((_ nextTick: DispatchTime) -> Void)?
var nextTick: DispatchTime = DispatchTime.distantFuture
let player: AVAudioPlayer = {
do {
let soundURL = Bundle.main.url(forResource: "metronome", withExtension: "wav")!
let soundFile = try AVAudioFile(forReading: soundURL)
let player = try AVAudioPlayer(contentsOf: soundURL)
return player
} catch {
print("Oops, unable to initialize metronome audio buffer: \(error)")
return AVAudioPlayer()
}
}()
private func start() {
print("Starting metronome, BPM: \(bpm)")
player.prepareToPlay()
nextTick = DispatchTime.now()
tick()
}
private func stop() {
player.stop()
print("Stoping metronome")
}
private func tick() {
guard
enabled,
nextTick <= DispatchTime.now()
else { return }
let interval: TimeInterval = 60.0 / TimeInterval(bpm)
nextTick = nextTick + interval
DispatchQueue.main.asyncAfter(deadline: nextTick) { [weak self] in
self?.tick()
}
player.play(atTime: interval)
onTick?(nextTick)
}
}
ViewController.swift
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var bpmLabel: UILabel!
#IBOutlet weak var tickLabel: UILabel!
let myMetronome = Metronome()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
myMetronome.onTick = { (nextTick) in
self.animateTick()
}
updateBpm()
}
private func animateTick() {
tickLabel.alpha = 1.0
UIView.animate(withDuration: 0.35) {
self.tickLabel.alpha = 0.0
}
}
#IBAction func startMetronome(_: Any?) {
myMetronome.enabled = true
}
#IBAction func stopMetronome(_: Any?) {
myMetronome.enabled = false
}
#IBAction func increaseBpm(_: Any?) {
myMetronome.bpm += 1.0
updateBpm()
}
#IBAction func decreaseBpm(_: Any?) {
myMetronome.bpm -= 1.0
updateBpm()
}
private func updateBpm() {
let metronomeBpm = Int(myMetronome.bpm)
bpmLabel.text = "\(metronomeBpm)"
}
}
Note: There seems to be a pre-loading issue, the prepareToPlay() doesn't fully load the audio file before playing and it causes some timing issue with the first playback of the tick audio file. This issue will be left to the reader to figure out. The original question being synchronization, this should be demonstrated in the code above.

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 3 Xcode: How to display battery levels as an integer?

I am making an app to read battery percentage using Swift!
Right now my out is something like this:
61.0% or 24.0% or 89.0%
What I'm trying to fix is getting rid of the .0 so it's an Int.
This is my code so far:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var infoLabel: UILabel!
var batteryLevel: Float {
return UIDevice.current.batteryLevel
}
var timer = Timer()
func scheduledTimerWithTimeInterval(){
timer = Timer.scheduledTimer(timeInterval: 60, target: self, selector: #selector(self.someFunction), userInfo: nil, repeats: true)
}
func someFunction() {
self.infoLabel.text = "\(batteryLevel * 100)%"
}
override func viewDidLoad() {
super.viewDidLoad()
UIDevice.current.isBatteryMonitoringEnabled = true
someFunction()
scheduledTimerWithTimeInterval()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
I have tried something like this:
var realBatteryLevel = Int(batteryLevel)
However, I get this error
I have tried other method but none with any luck. Please, any solutions would be awesome! Thanks in advance!
EDIT
I was considering making the float batteryLevel into a String and then replacing ".0" with "" and I have seen this somewhere, however, I'm not sure how!
Try this instead:
func someFunction() {
self.infoLabel.text = String(format: "%.0f%%", batteryLevel * 100)
}
For future reference, all string format specifiers are listed here.
You just need to convert It inside your function :
func someFunction() {
self.infoLabel.text = "\(Int(batteryLevel * 100))%" }
Alternately, you could create an Int computed property for batteryLevel:
var batteryLevel: Int {
return Int(round(UIDevice.current.batteryLevel * 100))
}
Note that you might not be able to get the battery level. You should test for that and display a different string:
if UIDevice.current.batteryState == .unknown {
self.batteryLevelLabel.text = "n/a"
} else {
self.batteryLevelLabel.text = "\(self.batteryLevel)%"
}
Also note that rather than running a timer to fetch the battery level, you should subscribe to the .UIDeviceBatteryLevelDidChange notification. The "meat" of a view controller that handles all of this might look as follows:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var batteryLevelLabel: UILabel!
///Holds the notification handler for battery notifications.
var batteryNotificationHandler: Any?
///A computed property that returns the battery level as an int, using rounding.
var batteryLevel: Int {
return Int(round(UIDevice.current.batteryLevel * 100))
}
///A function to display the current battery level to a label,
////or the string "n/a" if the battery level can't be determined.
func showBatteryLevel() {
if UIDevice.current.batteryState == .unknown {
self.batteryLevelLabel.text = "n/a"
} else {
self.batteryLevelLabel.text = "\(self.batteryLevel)%"
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
///If we have a battery level observer, remove it since we're about to disappear
if let observer = batteryNotificationHandler {
NotificationCenter.default.removeObserver(observer: observer)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
showBatteryLevel() //display the battery level once as soon as we appear
//Create a notifiation handler for .UIDeviceBatteryLevelDidChange
//notifications that calls showBatteryLevel()
batteryNotificationHandler =
NotificationCenter.default.addObserver(forName: .UIDeviceBatteryLevelDidChange,
object: nil,
queue: nil, using: {
(Notification) in
self.showBatteryLevel()
})
}
override func viewDidLoad() {
super.viewDidLoad()
//Tell UIDevice that we want battery level notifications
UIDevice.current.isBatteryMonitoringEnabled = true
}
}

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