I need to translate my iOS application from obj-c to swift. I have a NStimer in ViewController that loads metadata from shoutcast every 30 seconds, but when application resign active it stops, when enter foreground it runs again.
Edit: OK. Problem solved! I added two observers in viewDidLoad with name UIApplicationWillResignActiveNotification and UIApplicationWillEnterForegroundNotification, like below:
override func viewDidLoad() {
NSLog("System Version is \(UIDevice.currentDevice().systemVersion)");
super.viewDidLoad()
self.runTimer()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "invalidateTimer", name: UIApplicationWillResignActiveNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "runTimer", name: UIApplicationWillEnterForegroundNotification, object: nil)
}
and I made two functions. First one for run timer:
func runTimer(){
loadMetadata()
myTimer.invalidate()
NSLog("timer run");
myTimer = NSTimer.scheduledTimerWithTimeInterval(30.0, target: self, selector: "loadMetadata", userInfo: nil, repeats: true)
let mainLoop = NSRunLoop.mainRunLoop()
mainLoop.addTimer(myTimer, forMode: NSDefaultRunLoopMode)
}
and second to stop it:
func invalidateTimer(){
myTimer.invalidate()
NSLog("timer invalidated %u", myTimer);
}
I hope this can help someone. :)
I suggest you use the appropriate system for your task: https://developer.apple.com/library/ios/documentation/iphone/conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html#//apple_ref/doc/uid/TP40007072-CH4-SW56
Apps that need to check for new content periodically can ask the
system to wake them up so that they can initiate a fetch operation for
that content. To support this mode, enable the Background fetch option
from the Background modes section of the Capabilities tab in your
Xcode project. (You can also enable this support by including the
UIBackgroundModes key with the fetch value in your app’s Info.plist
file.)...
When a good opportunity arises, the system wakes or launches your app
into the background and calls the app delegate’s
application:performFetchWithCompletionHandler: method. Use that method
to check for new content and initiate a download operation if content
is available.
Related
I have an app that does a certain task.
While performing this task, the app also listens for when the user receives a phone call.
If the user receives a phone call, this task needs to be interrupted.
On iOS 13 we listen to willResignActiveNotification for when the incoming call and to didBecomeActiveNotification for when the call ends (this for when the user has the app open before the phone call).
On iOS 14 this also works if the call setting is set to Full Screen.
But when this setting is changed to Banner these notifications are never triggered.
I can't identify the app state for when the setting is set to Banner. My guess is that it is still in the active state.
The problem is that although the notifications are not called, the UI is interrupted as if the app was placed in the background when the user has the Banner setting on.
Note
I also conform to CXCallObserverDelegate and implement callObserver(_ callObserver: CXCallObserver, callChanged call: CXCall) method to know when the user receives a call and when the call ends.
So a solution is to just resume the task when the call ends and this method is triggered.
But I want to understand the app lifecycle in this case which is not making sense to me.
Code Sample
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(appDidEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
timer.resume()
updateUI()
}
override func viewWillDisappear(_ animated: Bool) {
timer.invalidate()
pauseUI()
NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)
super.viewWillDisappear(animated)
}
#objc private func appDidEnterBackground(_ notification: Notification) {
timer.invalidate()
pauseUI()
}
#objc private func appWillEnterForeground(_ notification: Notification) {
timer.resume()
updateUI()
}
Since iOS 10 our app uses callObserver:callChanged: from CXCallObserverDelegate to detect incoming or answered phone calls like:
#import CallKit;
#property (nonatomic) CXCallObserver *callObserver;
#pragma mark - Application lifecycle
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Register Call Observer to detect incoming calls:
_callObserver = [[CXCallObserver alloc] init];
[_callObserver setDelegate:self
queue:nil];
}
#pragma mark - CXCallObserverDelegate
- (void)callObserver:(CXCallObserver *)callObserver callChanged:(CXCall *)call {
// Based on state:
if (call.hasEnded) {
NSLog(#"CXCallObserver: Call has ended");
}
else if (call.hasConnected) {
NSLog(#"CXCallObserver: Call has connected");
}
else if (!call.isOutgoing && !call.hasConnected && !call.hasEnded) {
NSLog(#"CXCallObserver: Call is incoming");
}
else {
NSLog(#"CXCallObserver: None of the conditions");
}
}
Since the default behaviour on iOS 14 is to show a banner for incoming calls, we indeed saw that CXCallObserverDelegate was not called anymore.
When we changed the Phone setting in the iOS app to Full Screen it did work again. Obviously, just like in iOS 13 and before.
However, when we flipped the setting back to Banner, our app did call CXCallObserverDelegate event on an incoming banner call.
Based on the numerous issues with Local Network permission in iOS 14, it's only an estimated guess that here a similar root cause applies, where settings are only handed over to the native functionality once actively set by the user, and not on a default system setting.
I hope that explicitly changing the setting will solve your issue, or that we were accidentally lucky.
Note: the observations are done on iPhone SE 2nd gen with iOS 14.6.
I am making a music player, and for some reason when I add the notification center observer and set the queue and play the song it gets called twice. I commented out the play method and it's only called once. I'm not sure how to fix this or whether this is the issue.
didLoad
NotificationCenter.default.addObserver(self, selector: #selector(change), name: .MPMusicPlayerControllerNowPlayingItemDidChange, object: nil)
musicPlayer.beginGeneratingPlaybackNotifications()
change function
#objc func change() {
print(musicPlayer.nowPlayingItem?.title) //called twice
}
function to queue and play
musicPlayer.setQueue(with: queueArr)
musicPlayer.play()
II have an NSTimer set to fire each second. Using the Simulator this will call the eachSecond() method correctly after I navigate away from the app by pressing the home button. However, on a real iPhone 6, once I navigate away eachSeconds is not called again until the app is open again.
Why might this behave differently on a real device? Is there anyway to ensure it will fire each second in this use case?
Its a fitness app, so needs to record duration when the phone locks or user navigates away momentarily.
Thanks,
lazy var timer = NSTimer()
fun init() {
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: #selector(RunViewController.eachSecond(_:)), userInfo: nil, repeats: true)
}
func eachSecond() {
// update UI etc.
}
An NSTimer will not fire when your app is suspended. Background execution is different when running under XCode; background execution limits do not apply.
In order to determine how much time has elapsed while your app is suspended, you can store a timestamp in applicationDidEnterBackground and calculate the elapsed time based on the difference between the saved timestamp and current time in applicationWillEnterForeground
You need to add the timer to the main run loop.
func init() {
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: #selector(RunViewController.eachSecond(_:)), userInfo: nil, repeats: true)
NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
}
When my watchKit app goes to background it fires the delegate method applicationWillResignActive. Method documentation says it can be used to pause ongoing tasks.
I have an ongoing method that i want to be stopped or broken by the use of the external method. How do i do that?
Example
func method1(){
// performing some actions
}
func breakMethod1(){
// running this method can stop (break) the execution of method1
}
This is, of course, assuming that your app has been architected so that breakMethod1() will definitely cancel the action occurring in method1().
You should set up an observer for an NSNotification at the beginning of method1() like so:
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(self, selector: "breakMethod1", name: UIApplicationWillResignActiveNotification, object: nil)
And for the sake of cleanup, you should also remove this observer after it's been triggered like so:
notificationCenter.removeObserver(self, name: UIApplicationWillResignActiveNotification, object: nil)
I'm trying to use local notification but something is not working.
I have a class notification that handle all the code related to the notifications.
It's apparently working. What is not working is the way I try to trigger my notification.
When the user clicks on the home button, I call my notification class that starts a NSTimer. It repeats every second, and each 10 seconds I call a webservice.
Everything works great on my simulator, but it doesn't work on my real iPhone.
Here the code:
//as a class variable
let notif = Notification()
func applicationDidEnterBackground(application: UIApplication) {
notif.triggerTimer()
}
The notification class
class Notification: NSObject, WsOrderStatusProtocol, WsPinRequestProtocol {
var timer = NSTimer()
var time = 0
var sendNotification:Bool = true
var wsos = WsOrderStatus()
var wsoc = PinRequest()
override init() {
super.init()
self.wsos.delegate = self
self.wsoc.delegate = self
}
func triggerTimer() {
print("log INFO : class Notification, methode : triggerTimer")
NSNotificationCenter.defaultCenter().addObserver(self, selector:"orderCoupon:", name: "actionOrderCouponPressed", object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector:"cancelTimer:", name: "actionCancelTimerPressed", object: nil)
timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: Selector("launchNotification"), userInfo: nil, repeats: true)
}
func launchNotification() {
print("log INFO : class Notification, methode : launchNotification")
time += 1
print("time \(time)")
if time % 10 == 0 {
print("modulo 10")
wsos.getOrderStatus()
}
}
}
In the simulator, I see the logs et the logs that counts to 10 etc, but with my real iphone, I only see the first log "print("log INFO : class Notification, methode : triggerTimer")" then nothing...
Do you know why ?
As Paul says in his comment, your app only spends a very brief time in the background before being suspended. Suspended means that your code doesn't run at all any more, so timers stop.
The simulator doesn't always follow the same rules. When its behavior is different than that of a device then ignore it. It lies.
If you want to have more time to do work in the background, you can ask for it using the method beginBackgroundTaskWithExpirationHandler. Make that call in your applicationDidEnterBackground method.
From testing I've found that that gives you 3 minutes of extra time. After that your expiration handler block gets executed and then you get suspended.
Apple does not want your app running indefinitely from the background. It drains the battery.
I've found that it is possible to lie and tell the system that you are an app that plays sounds from the background, and write your expiration handler to play a short "sound of silence" and then ask for another background task using beginBackgroundTaskWithExpirationHandler. However, doing that will get you rejected from the app store.