Some App Actions Breaking TImer? - ios

I have a Timer that i run on my app, it uses a save and reload feature for when the user leaves the view and returns to the view. However if i go to a different tab on my app, the actions needed for that tab seem to break the timer.
Would this be due to thread usage in the app and the coredata of the other tab breaking the timers counting? Here is my timer code
Update 2: Here is my revised timer code in full...
// MARK: - SETTING UP SETS & TIMERS
func createStopTimeForRestFromUserTime(userTime: Int) -> Date {
let calendar = Calendar.current
let stopDate = calendar.date(byAdding: .second, value: userTime, to: Date())
return stopDate!
}
func createTimer(stopDate: Date) {
print("CONSTRUCTING A TIMER")
userDefaults.set(stopDate, forKey: "setStopDate")
restTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(RestController.updateRestTimer), userInfo: nil, repeats: true)
}
func updateRestTimer() {
let presentTime = Date()
let stoppedTime = UserDefaults.standard.object(forKey: "setStopDate") as? Date
if stoppedTime?.compare(presentTime) == ComparisonResult.orderedDescending {
restRemainingCountdownLabel.text = dateComponentsFormatter.string(from: presentTime, to: stoppedTime!)
} else {
self.stopTimer()
print("THE TIMER IS NOW FINISHED")
}
}
func stopTimer() {
self.restTimer.invalidate()
}
// MARK: - CONFIGURE TIMER ON OPEN / CLOSE
override func viewWillDisappear(_ animated: Bool) {
print("VIEWWILLDISAPPEAR WAS CALLED")
stopTimer()
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "ViewDidChangeNotification"), object: NSDate())
}
func handleResumedTime(disappTime: NSDate) {
let disappearedTime = disappTime
let resumeTime = NSDate()
if disappearedTime.compare(resumeTime as Date) == ComparisonResult.orderedDescending {
print("RESUMING THE TIMER")
self.createTimer(stopDate: disappearedTime as Date)
} else {
print("TIMER HAS FINISHED")
stopTimer()
}
}
func handleTimerCallback(notification: NSNotification) {
if let date = notification.object as? NSDate {
self.handleResumedTime(disappTime: date)
}
}
Code in an alternate VC id switch between which is causing breakage:
override func viewDidLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(RoutineController.handleTimerCallback(notification:)), name: NSNotification.Name(rawValue: "ViewDidChangeNotification"), object: nil)
}
override func viewWillDisappear(_ animated: Bool) {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "ViewDidChangeNotification"), object: NSDate())
}
func handleTimerCallback(notification: NSNotification) {
let date = notification.object
print("Object is \(date)")
}
EDIT: Added the updated handleResumed func and console print out when changing tab and changing back again...
func handleResumedTime(disappTime: NSDate) {
let disappearedTime = disappTime as Date
let resumeTime = NSDate() as Date
print("ATTEMPTING RESUME")
print(disappearedTime)
print(resumeTime)
if resumeTime.compare(disappearedTime) == ComparisonResult.orderedAscending {
print("RESUMING THE TIMER")
self.createTimer(stopDate: disappearedTime)
} else {
print("TIMER HAS FINISHED")
stopTimer()
}
}

Try comparing the timers in milliseconds using DateFormatter and passing data between controllers using Notifications.
This function is called every time you switch tabs. It invalidates the timer, stops the ticking, and saves the time in milliseconds.
override func viewWillDisappear(_ animated: Bool) {
restTimer.invalidate()
let df = DateFormatter()
df.dateFormat = "y-MM-dd H:m:ss.SSSS"
let disappearingDate = Date()
let disappearingDateInMilliseconds = df.string(from: disappearingDate)
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "kViewDidChangeExampleNotification"), object: disappearingDateInMilliseconds)
}
/* add your Notification observers in each view controller */
override func viewDidLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.handleTimerCallback(notification:)), name: NSNotification.Name(rawValue: "kViewDidChangeExampleNotification"), object: nil)
}
You need to define the function that will handle the notifications in each controller:
func handleTimerCallback(notification: NSNotification) {
if let dateString = notification.object as? String {
print("Object is an String: \(notification.object)")
resumeTimerWithDate(dateString)
} else {
print("Object is not of type String: \(notification.object)")
}
}
Quick example comparing Dates in Swift by milliseconds:
func resumeTimerWithDate(dateString: String) {
let df = DateFormatter()
df.dateFormat = "y-MM-dd H:m:ss.SSSS"
let secondDate = Date()
let secondDateToStr = df.string(from: secondDate)
if dateString < secondDateToStr {
print("RESUMING THE TIMER")
let resumeDate = df.date(from: dateString)
self.createTimer(resumeDate)
} else {
print("TIMER HAS FINISHED")
}
}
Other things to consider: You may want to add some print statements for debugging / sanity check because some functions may be firing before others. For example, rather than posting the notification in viewWillDisappear, you can try posting it in viewDidDisappear, or using prepare(for segue:).

Related

Why is my app not working in background mode but foreground it is working?

I want to create an app as when a particular time comes it calls an API. I have done in it the foreground, but when the app is in the background, it is not executing. How can I solve the issue?
My code is below:
#objc func runCode() {
print("runcode")
timeLabel.text = "Pls select time"
}
#IBAction func dateChange(_ sender: UIDatePicker) {
if (timer != nil) {
timer.invalidate()
}
print("print \(sender.date)")
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm E, d MMM y"
let somedateString = dateFormatter.string(from: sender.date)
print(somedateString)
timer = Timer(fireAt: sender.date, interval: 0, target: self, selector: #selector(runCode), userInfo: nil, repeats: false)
RunLoop.main.add(timer, forMode: .common)
timeLabel.text = "api will trigger at \(somedateString)"
}
#IBAction func switchAction(_ sender: UISwitch) {
if stateSwitch.isOn {
date.isHidden = false
print("The Switch is on")
timeLabel.text = "Pls select time"
} else {
date.isHidden = true
if (timer != nil) {
timer.invalidate()
}
timeLabel.text = "Timer not activated"
print("Timer not activated")
}
}
If you want to call api from background, the thread must be in background first.
Morever, you can register background fetch : https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/updating_your_app_with_background_app_refresh
You can't. You can register for background processing so that your app is allotted a little time in which to do some work. You cannot control when that time allotment will arrive though. It can be as frequent as every 20 minutes or so but it can also be much longer.

How to get Scrubbing to work on UISlider using AVAudioPlayer

So I'm using a rather unique version of audio player and everything works except for track scrubbing. The tracks, it should be noted, are buffered and collected from a website. I thought it might have to do with the observers I'm using. But I don't know if it's that really. I'm able to call the duration of the track and I'm able to get the actual UISlider to animate in conjunction with the track progression. But I can't scrub forward or backward. What ends up happening is I can move the slider but the track's progress doesn't move along with it. Here's what I've got:
override func viewDidLoad() {
audio.initialize(config: ["loop": true]) { (playerItem, change) in
var duration = CMTimeGetSeconds(playerItem.duration)
self.scrubSlider.maximumValue = Float(duration)
}
scrubSlider.setThumbImage(UIImage(named: "position_knob"), for: .normal)
}
override func viewDidAppear(_ animated: Bool) {
scrubSlider.value = 0.0
print(audio.seekDuration())
self.scrubSlider.maximumValue = audio.seekDuration()
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.trackAudio), userInfo: nil, repeats: true)
print(self.scrubSlider.maximumValue)
}
#objc func trackAudio() {
var currentTime = audio.seekCurrentTime()
print(currentTime)
scrubSlider.value = currentTime
var duration = audio.seekDuration()
let remainingTimeInSeconds = duration - currentTime
timeElapsedLabel.text = audio.createTimeString(time: currentTime)
timeRemainingLabel.text = audio.createTimeString(time: remainingTimeInSeconds)
}
#IBAction func scrubberValueChanged(_ sender: UISlider) {
print("Entro")
var currentTime = (CMTimeGetSeconds((audio.playerQueue?.currentItem!.currentTime())!))
currentTime = (Float64)((scrubSlider.value))
}
Here's the observer where I collect the duration:
/**
Setup observers to monitor playback flow
*/
private func setupObservers(completion: #escaping (AVPlayerItem, Any) -> ()) {
// listening for current item change
self.audioQueueObserver = self.playerQueue?.observe(\.currentItem, options: [.new]) { [weak self] (player, _) in
print("media item changed...")
print("media number ", self?.playerQueue?.items() as Any, self?.playerQueue?.items().count as Any, self?.playerQueue?.currentItem as Any)
// loop here if needed //
if self?.audioPlayerConfig["loop"] as! Bool == true && self?.playerQueue?.items().count == 0 && self?.playerQueue?.currentItem == nil {
self?.playerQueue?.removeAllItems()
self?.playerQueue?.replaceCurrentItem(with: nil)
for item:AVPlayerItem in (self?.AVItemPool)! {
item.seek(to: CMTime.zero)
self?.playerQueue?.insert(item, after: nil)
}
self?.playerQueue?.play()
}
}
// listening for current item status change
self.audioQueueStatusObserver = self.playerQueue?.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { (playerItem, change) in
guard let currentItemDuration = self.playerQueue?.currentItem?.duration else { return }
if playerItem.status == .readyToPlay {
print("current item status is ready")
print("media Queue ", self.playerQueue?.items() as Any, self.playerQueue?.items().count as Any)
print("item duration ", CMTimeGetSeconds(playerItem.duration))
print("itemD ", CMTimeGetSeconds(currentItemDuration))
print(currentItemDuration)
completion(playerItem, change)
}
})

Elapsed Timer not starting

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

Widget data doesn't update when scrolling - Swift

I have a widget with data. When I launch today extension my widget data is updating and showing in real time. But when I scroll the notification center and return to my widget, it doesn't update. I tried several different methods but they didn't help me. I wrote below the last method which I tried.
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "updateLabels", userInfo: nil, repeats: true)
}
func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)) {
// Perform any setup necessary in order to update the view.
// If an error is encountered, use NCUpdateResult.Failed
// If there's no update required, use NCUpdateResult.NoData
// If there's an update, use NCUpdateResult.NewData
completionHandler(.NewData)
}
func updateLabels() {
runtimeLabel.text = returnTime() + " " + returnDay()
}
func returnTimeInterval() -> NSTimeInterval {
let uptime = NSProcessInfo().systemUptime
return uptime
}
func returnTime() -> String {
dateFormatter.unitsStyle = .Short
dateFormatter.allowedUnits = [.Day, .Hour, .Minute, .Second]
dateFormatter.zeroFormattingBehavior = .Pad
let time = dateFormatter.stringFromTimeInterval(returnTimeInterval())!
return time
}
func returnDay() -> String {
dateFormatter.unitsStyle = .Short
dateFormatter.allowedUnits = [.Year, .Month, .Day]
dateFormatter.zeroFormattingBehavior = .Pad
let date = NSDate(timeInterval: -returnTimeInterval(), sinceDate: NSDate())
let formatter = NSDateFormatter()
formatter.locale = NSLocale.currentLocale()
formatter.dateStyle = .MediumStyle
let megaDate = formatter.stringFromDate(date)
return megaDate
}
I tried the same and it worked with the code below:
#IBOutlet weak var infoLabel: UILabel!
var timer = NSTimer()
var counter = 0
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("updateLabel"), userInfo: nil, repeats: true)
}
func updateLabel(){
counter += 1
myLabel.text = "Test \(counter)"
}
Update:
viewWillAppear and viewDidAppear should be called after when the widget is active and viewDidDisappear should be called when scrolling (leaving the widget). As for now the viewDidDisappear is working as expected but not viewWillAppear and viewDidAppear on scrolling.
It´s a known bug that this is not working properly, you can read more information in this post at Apples forum and check the bug status report here.
You should write your update code in widgetPerformUpdateWithCompletionHandler method.

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