I have a progress indicator in the Live Activity that should update every 15 minutes. I know that the way to go is using Push Notifications but they are not an option for me.
I have seen Apps on the Appstore and even the native timer update their live activities when push notifications are not enabled and even background App fetch is disabled.
In Apple's docs, it is mentioned that you can update or end a Live Activity from your app while it runs in the background — for example, by using Background Tasks.
Background Tasks do not work for me since they are unreliable. However, I have the Audio, Airplay, and Picture in Picture Background mode activated (For purposes other than the Live Activity) and so when I run this code:
timer = Timer(timeInterval: 1, repeats: true, block: { [weak self] (timer) in
guard let self = self else { return }
self.value += 1
let state = TempAttributes.ContentState(timer: self.value)
Task { #MainActor in
await deliveryActivity.update(using: state)
}
})
RunLoop.main.add(timer!, forMode: .common)
It technically should work and when I add a break point in the timer, it gets called and the value gets updated but the Live Activity UI does not.
This works on 16.2 but unfortunately it does not work on 16.1! Any ideas why?
Related
I have create a smart alarm which relies on WKExtendedRuntimeSession.
There should be a possibility to manually stop the background session by, for example navigating back in the view hierarchy.
class BackgroundSessionController {
private var session: WKExtendedRuntimeSession?
func start() {
guard session?.state != .running else { return }
if nil == session || session?.state == .invalid {
session = WKExtendedRuntimeSession()
}
print("session started")
session?.start(at: Date())
}
func stopManual() {
session?.invalidate()
}
func stopByAlarm() {
session?.notifyUser(hapticType: .stop)
session?.invalidate()
}
}
When firing the function stopManual, and so invalidating the session I receive the message:
App has been running in the background but failed to play a scheduled
alarm. Would you like to disable the app's ability to run background
tasks ...
Seems that manually invalidating a session requires a haptic notification as well ?
How can I invalidate the session without the haptic feedback ?
added example:
let's say i'm a terrorist and i'm making a secret bomb which fires when movement stops. So you need to keep moving or else a timer starts counting down.
I Activate the app, I need to enable background modes, else the sensors stop working when the app goes into the background.
When movement stops, a smart alarm timer will fire .start(at:) which
counts down from 10 minutes.
I'm using smart alarm as functionality, which allows me to use 30 minutes of background modes. When these 30 minutes are finished and the person is still moving, i want to invalidate and then restart the session without sending any haptic feedback (the person will notice something isn'tright and deactivates the bomb)
What to use in this case then? This example is a bit weird but almost the same functionality I want.
The documentation of WKExtendedRuntimeSession clearly states this behaviour:
For sessions started with start(at:), you can only call invalidate() when the app is active. For all other sessions, you can call invalidate() to end a session at any time.
Since you are passing the current date for start(at:), why don't you just use start() instead and then you can call invalidate() even while your app is inactive. If you actually call start(at:) with a future date, then you don't have any alternatives.
The docs also state that you must play a haptic during your session. So if your session has started, you cannot invalidate it without playing a haptic. If your session hasn't started yet, you can invalidate it.
During the session, your app must trigger the alarm by calling the session’s notifyUser(hapticType:repeatHandler:) method.
If you fail to play a haptic during the session, the system displays a warning and offers to disable future sessions.
Bear in mind, this is probably a designed feature of watchOS. Due to the battery constraints of Apple Watches, watchOS puts an even bigger emphasis on apps being energy efficient, so if your app uses a background mode and the system thinks it might be abusing it (by declaring a smart alarm background mode, but not playing an alarm), it will alert the user.
I am working on updating a drag racing app I had on the App Store. Previously the user would hit the "start" button, a countdown would begin and after the timer hit 0 the app would track the various things needed for racing. For instance how long the user takes to hit 60 MPH, 1/8 mile and a 1/4 mile. However I am adding a setting that allows the user to hit the start button, and then launch whenever they would like. When the app senses motion it begins to track the motion just as it would wisen the timer hit 0 in the previous version. However I am confused on the best method to use in order to "wait" until the app senses motion before tacking the information. I tried the following:
if(zeroTosixtyStyle == true){
Warninglabel.text = ("Launch when you are ready!")
repeat {
sleep(UInt32(0.01))
}while(locationManager.location?.speed ?? 0 < 2 );
processTwo()
}else{
timer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector:
#selector(ZerotoSixtyViewController.DecupdateCounter), userInfo: nil, repeats: true)
DecupdateCounter()
}
However this is obviously not a good solution. Ad it ramps up CPU usage, and makes certain parts of the app unresponsive. Using the method actually makes the "Warninglabel" I have not actually update to the string before the loop starts. What is a more efficient way to wait until the app senses motion in order to move onto "processTwo()"?
For more info the "zeroTosixtyStyle" is a boolean set by user settings. If its true the app waits for the user to launch, if it's false it runs the countdown. This works correctly.
I'm trying to make a simple app that fires off an alarm every 7 minutes (configurable of course) whether the app is in the foreground or not.
Although it was easy enough to just make a hardcoded repeating timer that does this, adding any kind of front end interface seems to throw a wrench into my strategy, suggesting to me that maybe I am taking the wrong approach entirely.
For example, if I just have a start / stop button, pausing an existing timer needs to cancel the background notification, but keep track of how much time is left. Then when it is resumed, a notification needs to be recreated with the remaining time. For example, if it is a 5 minute timer, that is paused with only 2 minutes left, when it is resumed, it is set up to be a 2 minute countdown timer. So if I put the app in the background, I will get a notification that the time has elapsed, but it won't automatically start a 5 minute countdown, it will instead just go back to 2 minutes. I need to be able to create notifications that are repeating, and yet switch to the full duration once the "remaining" time has expired.
I could create an action on the notification, which if the user pressed would restart the timer, but for this purpose, I need the timer to automatically restart immediately, even if the user ignores it.
I could easily accomplish this with foreground timers, but then I would need to force my phone to never go to sleep, and it would only work while this app was in the foreground, which is not always possible on a phone.
Other timer apps can pop up background notifications just fine. And the calendar app can schedule background notifications for arbitrary times. I doubt the calendar app schedules all future (including repeating) alerts the moment it is started, since it works even if the phone was restarted and the calendar app never started. Therefore the calendar app notification mechanism must be smart enough to fire off an alarm, and then schedule the next alarm, which is exactly the kind of mechanism I need here. I thought maybe the calendar app just uses server-based remote notifications, and the server handled the more complex logic needed, but that can't be true since the calendar app notifications work fine even without any internet connection.
I've done a fair bit of research on this, but can't seem to find what to do. If I could pass in a tiny bit of code to be executed when the notification is triggered, that would work, but I see no way of doing that.
This is what my notification creation code looks like at the moment:
let content = UNMutableNotificationContent()
content.title = "Context switch"
content.body = "\(Int(initialDuration / 60)) minutes have passed."
content.sound = UNNotificationSound.default()
content.categoryIdentifier = generalCatId
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: remaining, repeats: true)
let request = UNNotificationRequest(identifier: requestIdentifier, content: content, trigger: trigger)
let center = UNUserNotificationCenter.current()
center.delegate = self
center.add(request) { (error) in
if error != nil {
print(error!)
}
}
the five basic background modes available in iOS:
Play audio the app can continue playing and/or recording audio in the background.
Receive location updates the app can continue to get callbacks as the device’s location changes.
Perform finite-length tasks the generic “whatever” case, where the app can run arbitrary code for a limited amount of time.
Process Newsstand Kit downloads
specific to Newsstand apps, the app can download content in the background.
Provide Voice-over-IP (VoIP) services the app can run any arbitrary code in the background. Of course, Apple limits its use so that your app must provide VoIP service, too.
Ref
Sorry, you can run the timer in the background forever so you have to think to do something doesn't depend on Perform finite-length tasks background mode.
So you have 3 options:
register your app to receive location updates and this need Requesting Permission to Use Location Services requestAlwaysAuthorization()
check the device time every time you go to foreground and this will not work correctly if user changes the time manually so best thing to do is calling web service to get current time in UTC
http://www.timeapi.org/utc/now
register to local notification when your app sends to background
var localNotification = UILocalNotification()
localNotification.fireDate = NSDate(timeIntervalSinceNow: 5)
localNotification.alertBody = "Time out"
localNotification.timeZone = NSTimeZone.defaultTimeZone()
localNotification.applicationIconBadgeNumber = UIApplication.sharedApplication().applicationIconBadgeNumber + 1
then schedule this notification UIApplication.sharedApplication().scheduleLocalNotification(localNotification)
I want to update my watch app state in background from iPhone, using session.updateApplicationContext(applicationContext).
Sending an application contact while the app on the watch is active does work properly.
When I activate the home button on the watch, the watch app goes to the background, handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) is called, and a WKSnapshotRefreshBackgroundTask is provided.
So I don’t understand why a WKSnapshotRefreshBackgroundTask is triggered properly, but not a WKWatchConnectivityRefreshBackgroundTask.
Apple’s docs say „When you receive background data from the paired iPhone, the system launches your app in the background, instantiates a WKWatchConnectivityRefreshBackgroundTask object, and passes the task object to your extension delegate’s handleBackgroundTasks: method.“.
But this does not happen, neither on a device, nor on the simulator. What could be wrong?
Edit:
To check what might be wrong, I downloaded Apple’s demo project „QuickSwitch“ that can be downloaded here. Here is the code that should handle background tasks:
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
for backgroundTask in backgroundTasks {
if let wcBackgroundTask = backgroundTask as? WKWatchConnectivityRefreshBackgroundTask {
// store a reference to the task objects as we might have to wait to complete them
self.wcBackgroundTasks.append(wcBackgroundTask)
} else {
// immediately complete all other task types as we have not added support for them
backgroundTask.setTaskCompleted()
}
}
completeAllTasksIfReady()
}
There, the same happens:
I did set a breakponint in the line of the if statement and executed the app.
When the home button on the watch simulator is pressed, the breakpoint is reached with a WKSnapshotRefreshBackgroundTask. This is OK (see above).
However, if a different line is selected on the iPhone simulator, watchOS does not schedule a WKWatchConnectivityRefreshBackgroundTask, as expected. After all, this demo project should demo exactly this point.
Maybe somebody could try the demo project and confirm this problem or not.
What is wrong?
Update my answer
Conclusion first
Currently WKWatchConnectivityRefreshBackgroundTask is only called for sure on a watchOS Simulator when the watchOS extension's WCSession is in notActivated state and the extension is not running in foreground (in background or terminated).
In real devices, it won't be called in my tests. But Apple docs says it may. So you shouldn't rely on it won't be called until Apple changes its docs.
WCSession Cores
For WCSession, when it is activated, you can transfer userInfo, and when the counterpart is active, it can get the userInfo. The counterpart won't need to be in foreground to be activated, it can be in a high priority background.
Testing Results
Here are my testing results.
How to make WCSession notActivated?
Using Xcode terminate your watchOS Extension. Xcode will send a kill signal to your WKExtension.
Or don't run WCSession.activate() in your code on watchOS Extension side. As WCSession is notActivated by default.
-------------below are old post, you can ignore safely if you don't want to read.-------------------
Theory
Please watch the picture first then I will explain.
Because of the history of watchOS, there are both WCSessionDelegate receiving functions (start from watchOS 2.0) and WKExtensionDelegate.handle(_:) function (start from watchOS 3.0).
Although they all claims to be background dealing, the former only works immediately when your app is in foreground. The data will be queued if your app is not in foreground (in background or being terminated) and executed immediately later when your app becomes in foreground again.
WKExtensionDelegate.handle(_:) is really working in background. However, the WKExtensionDelegate.handle(_:) is optional although it is recommended and well-prepared if you use Xcode.
If you don't implement WKExtensionDelegate.handle(_:) by commenting it. You app works in a watchOS 2.0 way.
If you implement WKExtensionDelegate.handle(_:) but you don't have a WCSession in your watchOS app. The result is tricky. You won't get any data when you watchOS app is in foreground, as you don't has a WCSession. When your app is in background, it will be waken when data comes, but you can't get the data as you don't have a session.
If you implemented them both, which are in most situations, data comes will be dealt depending on the state of your watchOS app and never be queued.
How to proving it?
Create a new watchOS project. In iOS part, add a button, each time you clicked the button, send a userInfo to watchOS
session.transferUserInfo(["send test":""])
In your watchOS app, add a label in interface.storyboard, and drag it to viewController as #IBOutlet var label: WKInterfaceLabel!, and implement both WKExtensionDelegate.handle(_:) and func session(WCSession, didReceiveUserInfo: [String : Any] = [:]) appDelegate.
var total = 0
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
// Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
for task in backgroundTasks {
// Use a switch statement to check the task type
switch task {
case let backgroundTask as WKApplicationRefreshBackgroundTask:
// Be sure to complete the background task once you’re done.
backgroundTask.setTaskCompleted()
case let snapshotTask as WKSnapshotRefreshBackgroundTask:
// Snapshot tasks have a unique completion call, make sure to set your expiration date
snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)
case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
// Be sure to complete the connectivity task once you’re done.
total += 1
DispatchQueue.main.async {
if let viewController = WKExtension.shared().rootInterfaceController as? InterfaceController {
viewController.label.setText(String(self.total))
}
}
connectivityTask.setTaskCompleted()
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
// Be sure to complete the URL session task once you’re done.
urlSessionTask.setTaskCompleted()
default:
// make sure to complete unhandled task types
task.setTaskCompleted()
}
}
}
public func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
total += 4
DispatchQueue.main.async {
if let viewController = WKExtension.shared().rootInterfaceController as? InterfaceController {
viewController.label.setText(String(self.total))
}
}
}
If WKExtensionDelegate.handle(_:) runs, we add total by 1. If func session(WCSession, didReceiveUserInfo: [String : Any] = [:]) runs, we add total by 4.
Debug
In Xcode, choose product->scheme as WatchKit app so we can terminate the watchOS app in Xcode.
run the project.
when watchOS app shows, open iOS app manually.
clicked the button in iOS app. You can see the label in watchOS changes by 4.
in Xcode, click product->stop(or cmd+.). watchOS app will disappear.
click one or more times on iOS app's button. Then manually open the watchOS app. You will see this time the label changes by 1 multiply your clicks.
The step will be 4 again when watchOS app is in foreground.
To all who have the same problem:
I submitted the problem to Apple Developer Technical Support, and they confirmed (# 652471299) the problem in watchOS 3, and suggested to file a bug report, what I did (# 29284559).
So, one has to wait for a bug fix by Apple.
Update:
They answered my bug report, only 2 days later:
Well we get a ton of issues like this, usually it is some misunderstanding about timings or the app not suspending because it is being debugged or not in the dock so it won’t get discretionary tasks.
In this case, reading the description above I’m guessing the user is debugging via xcode while testing. Two task types: Watch Connectivity and URLSession only arrive as “launch” events. When debugging, xcode keeps the app running so it will never get these tasks. The best way to test this is to disconnect from xcode and test, make sure your app is in the dock as well — only docked apps will get discretionary tasks.
If you see this not working after trying that we’ll need a sysdiagnose to go further.
I think this statement is wrong. My reply was:
Thanks for the quick answer. However, something is wrong anyway:
The function
handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>)
should handle all background tasks, including WKWatchConnectivityRefreshBackgroundTask.
To check that this is not the case is easy:
Just let the app crash when such a background task is scheduled, i.e. insert in Apple’s QuickSwitch demo project an assert statement that is always false:
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
for backgroundTask in backgroundTasks {
if let wcBackgroundTask = backgroundTask as? WKWatchConnectivityRefreshBackgroundTask {
assert(false) // If the app comes here, it will crash
// store a reference to the task objects as we might have to wait to complete them
self.wcBackgroundTasks.append(wcBackgroundTask)
} else {
// immediately complete all other task types as we have not added support for them
backgroundTask.setTaskCompleted()
}
}
completeAllTasksIfReady()
}
Then run the app in foreground, in the dock, or in background, and select different codes on the iPhone.
The app will NOT crash, which proves that no WKWatchConnectivityRefreshBackgroundTask is scheduled.
Please do this test without Xcode control. Just run it on iPhone & watch devices.
Now, 1 week later, I did not get any more reply.
Maybe I am wrong, and somebody can give me a hint how to do it right.
I'm developing an app that switches music using a MPMusicPlayerController.systemMusicPlayer() from the MPMedia framework. I need the song to change anywhere between 30 seconds and 1 minute for up to 100 minutes when the app is in the background. In the foreground of course everything works exactly how it should but in the background I can get the app to do this for about 3 minutes which seems to be the limit imposed on background tasks in iOS8. Is there a way to extend the allowable time that app can operate in the background?
I'm currently using the following code to register the background task where background_task is of type :
var background_task: UIBackgroundTaskIdentifier = UIBackgroundTaskInvalid
func registerBackgroundTask()
{
println("Background task registered")
background_task = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler
{
[unowned self] in
self.endBackgroundTask()
}
}
and then this ends it:
func endBackgroundTask()
{
NSLog("Background task ended.")
UIApplication.sharedApplication().endBackgroundTask(background_task)
background_task = UIBackgroundTaskInvalid
}
You don't need a background task for that.
Use the "Audio and AirPlay" background mode.
As long as you are playing audio, your app won't be suspended (unless iOS really needs it).