Timer disrupts when notification center is pulled down in iOS 12 - ios

So I am creating an app that has countdown timer. When the app quits I am using observers to know if the app is in background. If it is, I invalidate the timer and store the quit time in userDefaults. Then when the app comes back to foreground, I create a new timer and calculate the time that the app has been in background and subtract it from the total duration in order to get the elapsed time. When app goes to the background, I am storing the time in UserDefaults:
#objc func applicationDidEnterBackground() {
let defaults = UserDefaults.standard
let quitTime = Date()
defaults.set(quitTime, forKey: "quitTimeKey") //Storing the time of quit in UserDefaults
timer?.invalidate()
}
Then I create a new instance of timer when app enters foreground:
#objc func appEntersForeground() {
calculateTimeLeft()
if (timer == nil)
{
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(handleCountdown), userInfo: nil, repeats: true)
}
}
Then I check the elapsed time:
func checkElapsedTime() -> Double {
let currentTime = Date()
let appQuitTime = UserDefaults.standard.object(forKey: "quitTimeKey") as? Date ?? Date.distantFuture
let elapsedTime = currentTime.timeIntervalSince(appQuitTime)
return elapsedTime
}
I am also printing the time difference:
let timeDifference = checkElapsedTime()
print("timeDifference = \(timeDifference)")
Question: However, here is an issue. When I am using the app and I slide the notification center down and back up for not even a second, I get a timeDifference reading of few thousand seconds.
What could be the reason here? Is this iOS 12 bug? This only happens when I pull the notification center down when I am in the app.

Alright so I got it working. Basically when you are sliding down the notification center, you are calling applicationWillResignActive. So instead of calling applicationDidEnterBackground, I used applicationWillResignActive for the notification and it started working all fine!

Related

How to setup long running timers on iOS

I want to create a timer that should fire every hour. But from my research, an app gets suspended after 10 mins in the background. It also seems like the app gets suspended after the screen is locked.
I want to trigger this timer every 1 hour. I will be invalidating the timer when app goes in the background and restart it when the app foregrounds. So I have a few questions:
When the user backgrounds the app and comes back to it, will the timer fire immediately if it has been 1+ hour?
What happens if the user returns to the app after multiple (2+) hours, will the timer fire multiple times?
Are there any recommended ways to setup such longer running timers so they fire more consistently and not just once when they were setup?
You can do something like this without using background timer. It is only idea how you can achieve your requirement, add one or more hours condition as per your requirement.
var totalTime = Double()
override func viewDidLoad() {
super.viewDidLoad()
// MARK: - To Reset timer's sec if app is in background and foreground
NotificationCenter.default.addObserver(self, selector: #selector(self.background(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.foreground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
}
#objc func background(_ notification: Notification) {
if self.totalTime > 0{
user_default.setValue(self.totalTime, forKey: "TotalSecInBackground")
user_default.setValue(Date().timeIntervalSince1970, forKey: "OldTimeStamp")
LogInfo("total seconds left in background: \(self.totalTime)")
}
}
#objc func foreground(_ notification: Notification) {
let timerValue: TimeInterval = user_default.value(forKey: "TotalSecInBackground") as? TimeInterval ?? 0
let otpTimeStamp = user_default.value(forKey: "OldTimeStamp") as? TimeInterval ?? 0
let timeDiff = Date().timeIntervalSince1970 - otpTimeStamp
if timerValue > timeDiff{
LogInfo("total second & timeDiff:, \(Int(timerValue)),\(Int(timeDiff))")
let timeLeft = timerValue - timeDiff
self.totalTime = Int(timeLeft)
LogInfo("timeLeft: \(Int(timeLeft))") // <- This is what you need
}}

timer gets called after coming back from minimized state iOS

I build a real time clock after fetching current location and then shows current time from api response - I'm using these function to display current time.
func getCurrentTime() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.currentTimeAfterFetchedTime), userInfo: nil, repeats: true)
}
#objc func currentTimeAfterFetchedTime(currentTime : Int) {
print("Timer Function gets Called \(Date())")
let formatter = DateFormatter()
formatter.dateFormat = "MMM d, h:mm:ss a"
DispatchQueue.main.async {
self.presentDayDateNTime.text = formatter.string(from: Date(timeIntervalSince1970: TimeInterval(self.dynamicCurrentDateNTime)))
self.dynamicCurrentDateNTime += 1
}
}
Now I want to refetch api and show real time if user come back after minimized state. So I added this Notification observer to check if the app comes back from minimized state or not -
NotificationCenter.default.addObserver(self, selector: #selector(applicationComesInTheForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
In my viewDidLoad() and also define this to fetch location and call api again-
#objc func applicationComesInTheForeground() {
print("Called")
self.spinner.startAnimating()
fetchCurrentLocation()
}
My app works fine when starts first time but when it coming back from minimize state the currentTimeAfterFetchedTime gets called doubled in a seconds and my clock gets fast enough to count 1 minute in just 30 seconds.
I'm calling currentTimeAfterFetchedTime function from completionhandler of api call-
DispatchQueue.main.async {
print("In Dispathch",self.currentDayData)
// MARK: - Dynamic time representation afetr fetching data from API
self.dynamicCurrentDateNTime = self.currentDayData.dt
self.getCurrentTime()
}
So, My question is why my timer function gets called double in a seconds?
Add the following observer and invalidate the timer whenever the app goes to background or inactive state.
NotificationCenter.default.addObserver(self, selector: #selector(applicationWillBecomeInactive), name: UIApplication.willResignActiveNotification, object: nil)
#objc func applicationWillBecomeInactive() {
print("application inactive")
timer.invalidate()
}
Whenever the App becomes active you can start the timer (as done by you)
You should reset the old timer in getCurrentTime() before the start it.
timer.invalidate()

iOS CallKit Configurations in Detail

I'm using CallKit to connect Twilio RTC and I'd like to know more about CallKit.
There is a timer in the lock screen of the CallKit and I am trying to start the timer after I receive response from the server. Right now the timer would just start counting right after the "connecting" phase, in which my response haven't even started yet.
It seems like starting after calling [action fulfill], but how should I implement with the http call? or Twilio function?
If I'm calling from the call history of the native call app made by Apple, where should I store the mapping information for UUID to the user-id I'd like to call?
I know that the callee can save these information in CXUpdate and show without problem, but how about the caller? I can't tell from the examples of saving call information in CXHandle.
Thanks ahead for reading through :)
Intiate call timer after call connecting state to connected
var callTimer: Timer?
var timeDuration: TimeInterval = 0.0
//MARK:- CallTimer
func initiateCallTimer() {
if callTimer == nil {
self.callTimer = Timer.scheduledTimer(timeInterval: kRefreshTimeInterval, target: self, selector: #selector(CallViewController.refreshCallTimer(_:)), userInfo: nil, repeats: true)
}
}
func callTimerInvalidate() {
if callTimer != nil {
callTimer?.invalidate()
callTimer = nil
}
}
#objc func refreshCallTimer(_ sender: Timer) {
timeDuration += kRefreshTimeInterval
print(timeDuration)
self.lblConnectiong.text = string(withTimeDuration: timeDuration)
}
func string(withTimeDuration timeDuration: TimeInterval) -> String? {
let minutes = Int(timeDuration / 60)
let seconds = Int(timeDuration) % 60
let timeStr = String(format: "%ld:%02ld", minutes, seconds)
return timeStr
}
and invalidate after rejected or end
self.callTimerInvalidate()
for this call from recent call list
In your app's Info.plist, you must have INStartAudioCallIntent and/or INStartVideoCallIntent in the NSUserActivityTypes key, and your app delegate must implement the -application:continueUserActivity:restorationHandler: method to handle the start call intent.

Swift: Timer run in foreground/background

I'm learning how to create a Pomodoro app, and am able to send notifications. However, I am totally clueless as to how to allow my timer label to update itself on reloading the app. Which means the timer works only when the app is open and not when it's in the foreground/background. Hoping to find a tutorial to learn from or just a quick answer code. Thanks!
Edit: Just to clear some misunderstandings, my app's Notification works fine with the timer, for example if 30mins is selected, the app would notify the user after 30mins. However, the problem is that when the app reopens, it resumes for example 29:57 seconds left on the timer label while the 30mins should have passed already.
*Added in AppDelegate*
var seconds = 0 //Timer countdown seconds
var currentDate = NSDate()
var setDate: Int = 0
func pauseApp(){
viewC.timer.invalidate() //invalidate timer
UserDefaults.standard.set(seconds, forKey: "current") //error occurs here where "Cannot assign value of type NSDate to type Timer"
setDate = UserDefaults.standard.integer(forKey: "current")
}
func startApp(){
let difference = currentDate.timeIntervalSince(NSDate() as Date) as Double
seconds = Int(Double(setDate) - difference)
viewC.updateTimer()
}
What someone suggests from a different thread is cancel the timer and store a NSDate when the app goes to the background. He stated we can use this notification to detect the app going to the background:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "pauseApp", name: UIApplicationDidEnterBackgroundNotification, object: nil)
Then cancel the timer and store the date:
func pauseApp(){
self.stop() //invalidate timer
self.currentBackgroundDate = NSDate()
}
Use this notification to detect the user coming back:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "startApp", name: UIApplicationDidBecomeActiveNotification, object: nil)
Then calculate the difference from the stored date to the current date, update your counter and start the timer again:
func startApp(){
let difference = self.currentBackgroundDate.timeIntervalSinceDate(NSDate())
self.handler(difference) //update difference
self.start() //start timer }
However, I do not fully understand this code (namely, the difference between the "handler" and my own "seconds") as am new to programming... Hoping for an answer or helpful insight.
Solved: I managed to solve it myself from this video https://www.youtube.com/watch?v=V6ta24iBNBQ
Using this concept of timeDifference as well as UserDefaults.standard.set....
I managed to adapt it to my personal app with the code
You can call Timer to run the timmer when the view loads.
var runTimer : Timer?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
runTimer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(myFun), userInfo: nil, repeats: true)
}
func myFun(){
//do your logic
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
runTimer?.invalidate()
}

How to pass a Timer from Second View Controller to first?

So I have a timer that saves that saves the ending time using NSUserDefaults but I want to push that timer to the previous ViewController as well. The timer should be started on the second View Controller, and if you go back, or exit the app and reopen it, the timer should display. I have an idea of how do it with a Singleton DataService, but not quite sure how to put it all together. Here is my code as of now.
import UIKit
import UserNotifications
let stopTimeKey = "stopTimeKey"
class QOTDVC: UIViewController {
// TIMER VARIABLES
let timeInterval: Double = 89893
let defaults = UserDefaults.standard
var expirationDate = NSDate()
#IBOutlet weak var timerLabel: UILabel!
#IBAction func DoneWithQuestion(_ sender: AnyObject) {
self.dismiss(animated: true, completion: nil)
}
#IBOutlet weak var timerCounter: UILabel!
var timer: Timer?
var stopTime: Date?
override func viewDidLoad() {
super.viewDidLoad()
saveStopTime()
NotificationCenter.default.addObserver(self, selector: #selector(saveStopTime), name: NSNotification.Name(rawValue: "Date") , object: nil)
}
func alert(message: String, title: String = "") {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
let OKAction = UIAlertAction(title: "OK", style: .default) {
UIAlertAction in
self.registerForLocalNotifications()
StartTimerInitiated()
}
alertController.addAction(OKAction)
self.present(alertController, animated: true, completion: nil)
}
func registerForLocalNotifications() {
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound]) { (granted, error) in
if granted {
let content = UNMutableNotificationContent()
content.title = "Ready for the QOTD"
content.body = "You have 30 seconds to answer the question"
content.sound = UNNotificationSound.default()
let trigger = UNTimeIntervalNotificationTrigger.init(timeInterval: self.timeInterval , repeats: false)
let request = UNNotificationRequest(identifier: "myTrigger", content: content, trigger: trigger)
center.add(request)
}
}
}
func StartTimerInitiated() {
let time = Date(timeIntervalSinceNow: timeInterval)
if time.compare(Date()) == .orderedDescending {
startTimer(stopTime: time)
} else {
timerLabel.text = "timer date must be in future"
}
}
// MARK: Timer stuff
func startTimer(stopTime: Date) {
// save `stopTime` in case app is terminated
UserDefaults.standard.set(stopTime, forKey: stopTimeKey)
self.stopTime = stopTime
// start NSTimer
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(QOTDVC.handleTimer), userInfo: nil, repeats: true)
// start local notification (so we're notified if timer expires while app is not running)
}
func stopTimer() {
timer?.invalidate()
timer = nil
}
let dateComponentsFormatter: DateComponentsFormatter = {
let _formatter = DateComponentsFormatter()
_formatter.allowedUnits = [.hour, .minute, .second]
_formatter.unitsStyle = .positional
_formatter.zeroFormattingBehavior = .pad
return _formatter
}()
func handleTimer(timer: Timer) {
let now = Date()
if stopTime!.compare(now) == .orderedDescending {
timerLabel.text = dateComponentsFormatter.string(from: now, to: stopTime!)
} else {
stopTimer()
notifyTimerCompleted()
}
}
func notifyTimerCompleted() {
timerLabel.text = "Timer done!"
}
func saveStopTime() {
stopTime = UserDefaults.standard.object(forKey: stopTimeKey) as? Date
if let time = stopTime {
if time.compare(Date()) == .orderedDescending {
startTimer(stopTime: time)
} else {
notifyTimerCompleted()
}
}
stopTime = UserDefaults.standard.object(forKey: stopTimeKey) as? Date
}
Any help would be much appreciated. If you need any clarification, please let me know.
You are juggling several issues: Passing an object between view controllers, triggering code at some future time, and having a timer persist in the background.
As far as a timer that runs while your program is in the background, you can simply calculate the number of seconds between now and your target time and set a non-repeating timer for that number of seconds in the future. There's no reason to fire a repeating timer every second and do math to see if your time has passed yet. The way you're doing it will run the CPU hotter and drain your battery faster, so better to set up a single timer in the future.
Next, dealing with timers while in the background. The short answer is that you can't. Apps don't actually run in the background for very long. They quickly get suspended, which is a state where they are not getting any CPU time at all. You can ask for background time, but the system limits you to 3 minutes. You can play tricks to get more than 3 minutes of background time, but those tricks will cause Apple to reject your app, and would drain down the user's battery quite quickly if you did manage to sneak it by Apple. (When an app is running in the background the phone isn't able to go to sleep. THE CPU stays fully powered up, drawing a LOT more current than it does in the sleep state.
Finally, passing your timer from one view controller to the next. Yes, you can certainly use a singleton to make the timer a shared resource. You could also set up your two view controllers so that in the code that invokes the second from the first, you give the second view controller a delegate pointer back to the first, and set up a protocol that would let you pass the timer from the second view controller back to the first.
However, a timer calls a single target, so while you'll have access to the timer from either view controller using either the singleton pattern or the delegate pattern, the timer will still call the original target that you used when you set it up.
You could make your singleton the target of the timer, give the singleton a delegate, and have the singleton send a message to it's delegate when the timer fires. Then when you switch view controllers you could change the singleton's delegate to point to the new view controller.
Alternately you could record the "fire date" of your timer in your viewWillDisappear method, invalidate the timer, and create a new timer with that same fire date (actually you'd have to do some math to convert a fire date to a number of seconds, but it would be a single call.)
You could also use a local notification with a future fire date and set it up to play a sound. However, that won't invoke your program from the background unless the user responds to the notification.

Resources