Cancel a smart alarm without any haptic feedback - swift

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.

Related

Setup custom Haptic with latest Swift for iPhone7 and up

After reading about it, I have some mess in my head.
This function is being called while user swipe his finger on some UI element :
func wasDragged() { signal here }
I would like to make small Haptic signals every time it's being called ( like a dates picker wheel )
How would I setup first time and make the signals of the Haptic Engine on call ?
Do I have to check for device kind ? I want it only on iPhone 7 and up.
Using latest Swift.
The documentation oh Haptic feedback is really descriptive.
But if you want some quick solution here it is.
var hapticGenerator: UISelectionFeedbackGenerator?
func wasDragged() {
hapticGenerator = UISelectionFeedbackGenerator()
haptiGenerator.selectionChanged()
hapticGeneraor = nil
}
Alternatively depending on the logic of the screen, you can initialize generator outside of wasDragged function and inside of it just call hapticGenerator.prepare() and selectionChanged(). In that case you should not assign nil to it after the dragging is complete because it won't get triggered again. As per documentation you have to release generator when no longer needed as Taptic Engine will wait and therefore consume system resources for another call.
Note that calling these methods does not play haptics directly.
Instead, it informs the system of the event. The system then
determines whether to play the haptics based on the device, the
application’s state, the amount of battery power remaining, and other
factors.
For example, haptic feedback is currently played only:
On a device with a supported Taptic Engine
When the app is running in the foreground
When the System Haptics setting is enabled
Documentation:
https://developer.apple.com/documentation/uikit/uifeedbackgenerator

Is state guaranteed to be reinstated when UIApplicationWillEnterForeground gets called

Is the state of my app guaranteed to be reinstated when UIApplicationWillEnterForeground notification gets called?
In other words, if I have a private boolean field set to true, will that variable still be true if I check its value in my UIApplicationWillEnterForeground notification method handler when transitioning from the background to the foreground?
No, When the App enters the background, it still stays in memory, however if memory runs low, the iOS begins to release these suspended apps to make room for those that must be loaded. So basically if you want to ensure that there is no data loss, you must save app data and settings in:
func applicationDidEnterBackground(_ application: UIApplication) {
}
func applicationWillTerminate(_ application: UIApplication) {
}
If you get told that your app is being suspended, (Via a call to applicationWillResignActive) you should save your app state. Your app may then be activated again while still in memory, or it may be terminated. If you get reactivated again, all of your app's variables will still have their previous values. If not, you will be launched "cold" and you will need to restore your saved state.
At the time you are told that you are going to switch to the inactive state you can't know if you will get reactivated or terminated. Thus the answer to your question is no. You might get killed, not reactivated.
EDIT:
Apple's docs say:
Handle Deactivation
When a foreground app moves to the background, UIKit first calls
the applicationWillResignActive: method to deactivate the app. When
deactivation occurs, do the following.
Save user data to disk and close any open files. Always save user
data in case your app needs to be terminated. Close files in case the
user locks the device.
Do only work that’s critical to preserving the user’s data. Pause
dispatch queues and operation queues, and don’t schedule any new tasks
for execution.
Invalidate any active timers.

How to make a repeating countdown timer app for an iphone in swift 3 that fires a notification every 'x' minutes even when in the background

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)

Is it possible to reopen a suspended app after a time delay in Swift?

I am using the following code to suspend the app.
UIControl().sendAction(Selector("suspend"), to: UIApplication.sharedApplication(), forEvent: nil)
do {
print("suspended")
} catch _ {
print("unable")
}
I was wondering is it possible to reopen the app after a certain time delay. Following code works for performing an action after time delay. But I don't know how to reopen the app at that time interval
let seconds = 300.0
let delay = seconds * Double(NSEC_PER_SEC) // nanoseconds per seconds
let dispatchTime = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(dispatchTime, dispatch_get_main_queue(), {
print("time delay")
})
It's is not possible and apps with such kind of "functionality" will be rejected by Appstore ;)
https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/StartingStopping.html
Always Be Prepared to Stop
An iOS app never displays a Close or Quit option
When people switch away from your app, iOS multitasking transitions it to the background and replaces its UI with the UI of the new app. To prepare for this situation, your app should:
Save user data as soon as possible and as often as reasonable. Do
this because an app in the background can be told to exit or
terminate at any time.
Save the current state when stopping at the finest level of detail possible. In this way, people don’t lose their
context when they switch back to your app. For example, if your app
displays scrolling data, save the current scroll position. You can
learn more about efficient ways to preserve and restore your app’s
state in Preserving Your App’s Visual Appearance Across Launches.
Some apps may need to keep running in the background while users run another app in the foreground. For example, users might want to keep listening to the song that’s playing in one app while they’re using a different app to check their to-do list or play a game. Learn how to handle multitasking correctly and gracefully in Multitasking.
Never quit an iOS app programmatically. People tend to interpret this as a crash. If something prevents your app from functioning as intended, you need to tell users about the situation and explain what they can do about it.

Is a Timer disabled automatically when app resigns active on the iphone?

Playing around with Timers, and trying to make them behave right when app is sent to background, killed etc.
Judging by the generated comments in xcode, you should disable/invalidate timers on resignActive/enterBackground events.
However, i havent done that yet, but checked what happens to my timer when i click the home button and then re-enter the app.
I basically have a method in my view that the timer triggers every second:
NSLog(#"workedTimTimerTick workedTime; %#", timeString);
And when i exit the app, the output stops, when i re-enter the app, the output starts again...
Since i'm not doing anything code-wise to the timer on those lifecycle events, how come it stops getting called?
Input appreciated!
Your app is suspended when it enters background mode (Application States and Transitions).
Your timer won't fire when the app is in background and the time spent in background isn't taken into account for the timer delay.