iOS: all local notifications disappear instead of the ones I list - ios

My app uses a lot of scheduled local notifications and on certain events I reschedule the notifications and want to clear some of the delivered notifications, not all of them.
Rough pseudo code:
// Clear pending notifications that haven't been delivered yet
notificationCenter.removeAllPendingNotificationRequests()
// Get the delivered notifications (async), filter out the ones that should be removed
// and remove them
notificationCenter.getDeliveredNotifications() { notifications in
let notificationsToRemove = notifications.filter { some boolean operation }
let identifiersToRemove = notificationsToRemove.map { $0.identifier }
notificationCenter.removeDeliveredNotificationsWithIdentifiers(identifiersToRemove)
}
// Schedule the next set of notifications
let nextBatchOfNotifications = notificationGenerator.generate()
for notification in nextBatchOfNotifications) {
notificationCenter.schedule(notification)
}
But when I do this the vast majority of the time results in all delivered notifications being cleared. And in very rare cases it results in only some of the delivered notifications I ask to be removed being removed (or maybe none).

At least in my case it transpires that the async nature of all the functions related to querying and scheduling notifications was the problem and the fact removing pending/delivered notifications and trying to schedule new ones meant that iOS would get in a bit of a mess and not do what I asked properly.
The solution appears to be to wait for the delivered notifications to be returned, remove them, wait a little more and then schedule the new ones. Since I've made this change I've not seen any issues so far!
Rough pseudo code updated with the waits
// Clear pending notifications that haven't been delivered yet
notificationCenter.removeAllPendingNotificationRequests()
// Get the delivered notifications (async), filter out the ones that should be removed
var identifiersToRemove
var semaphore
notificationCenter.getDeliveredNotifications() { notifications in
let notificationsToRemove = notifications.filter { some boolean operation }
identifiersToRemove = notificationsToRemove.map { $0.identifier }
semaphore.signal()
}
semaphore.wait()
notificationCenter.removeDeliveredNotificationsWithIdentifiers(identifiersToRemove)
Thread.sleep(0.1)
// Schedule the next set of notifications
let nextBatchOfNotifications = notificationGenerator.generate()
for notification in nextBatchOfNotifications) {
notificationCenter.schedule(notification)
}
I'm not sure if there's a better way of doing the 0.1s wait after requesting for the delivered notifications to be removed or not... there's no callback to let me know that it's been done so it's the best I could come up with for now!
(apologies for the pseudo code if it's difficult to follow, my code is somewhat legacy so is still in Objective-C and I didn't think that was particularly appropriate to share in this day and age!)

Related

Image from local notification attachment (UNNotificationAttachment) is not showing sometimes

I have an iOS app where I use only local notifications.
One of the features for this app is that I can create several notifications at once (the number of notifications can be from 1 to 20). Notification trigger is time stamp only.
I always add an attachment to each notification - a picture that is always being presented in the application database.
When a certain time comes, the notification trigger is triggered and this notification is shown to the user, usually once a day.
Sometimes (about 20% of cases) a picture is not displayed in the notification (does not matter if the screen is locked or not). This behavior is present in iOS12 - iOS14.
I did the following checks, which were successful:
no errors occurred while creating and adding an notification to UNUserNotificationCenter
check if a temporary URL image exists before creating UNNotificationAttachment
check if the picture exceeds 10Mb
check if an attachment exists for each pending Notification Requests that has already been added to the notification center
check if there is an access to the attachment of each pending Notification Requests that has already been added to the notification center
I've investigated an interesting case on iOS 13, after restarting the device, no one previously generated notification will show pictures. The debug shows that all UNNotificationAttachments in pending Notification Requests are present, but I do not have access to them. Obviously, not only I but also the OS do not have access. Very strange undocumented behavior.
I assume that over time the OS loses access to the UNNotificationAttachment files, but how to understand this? OS is not coping with its own security?
// how I check access for files in attachments
private func checkSavedAttachments() {
UNUserNotificationCenter.current().getPendingNotificationRequests {
(allScheduled) in
let attachments = allScheduled.map{ $0.content.attachments }.reduce([], +)
var accessDeniedCount = 0
attachments.forEach {
(attach) in
if attach.url.startAccessingSecurityScopedResource() {
print(attach.url)
}
else {
accessDeniedCount += 1
}
attach.url.stopAccessingSecurityScopedResource()
}
if accessDeniedCount > 0 {
fatalError()
}
}
}
Thanks for any help!

Xamarin Forms: Have I Covered the bases on iOS Push Notifications?

Issue: Different Behavior In 3 Different Contexts
Ok so Ok, in iOS it seems three different things can happen regarding Push Notifications:
When a Push Notification is received when the app is not in the foreground
something shows up in Notification Center
if the app is opened by tapping the notification, either AppDelegate.DidReceiveRemoteNotification(...) or AppDelegate.ReceivedRemoteNotification(...) is called, apparently depending on which one is implemented (??).
if the app is opened without tapping the notification, only AppDelegate.WillEnterForeground(...), is called, without any explicit mention of the notification, and nothing else happens to acknowledge that a notification was received.
When a Push Notification is received when the app is in the foreground it causes the UNUserNotificationCenterDelegate, if there is one, to execute UNUserNotificationCenterDelegate.WillPresentNotification(...).
Approach: Routing To One Method From All Contexts
So to cover all bases with Push I need to implement something in all three methods: AppDelegate.DidReceiveRemoteNotification(...) / AppDelegate.ReceivedRemoteNotification(...), AppDelegate.WillEnterForeground(...), and UNUserNotificationCenterDelegate .WillPresentNotification(...).
Here are some stubs to show my approach to all this.
First, I created a custom UNUserNotificationCenterDelegate, with a Shared static member:
public class IncomingNotificationHandler : UNUserNotificationCenterDelegate
{
public static IncomingNotificationHandler Shared = new IncomingNotificationHandler();
...
}
Second, inside that class I made a handler that I can route to in every case (again, this is just a stub for debugging purposes):
//sets all parameters to null by default, so it can be called from methods
//that don't know anything about notifications:
public void HandleNotificationsIfAny(UIApplication application = null,
NSDictionary userInfo = null,
Action<UIBackgroundFetchResult> completionHandler = null)
{
//checks if userInfo is null, and logs its conclusions about that:
if (userInfo == null)
{
//In the null case, we can get pending notifications from
//UNUserNotificationCenter:
UNNotification[] pendingNotifications = new UNNotification[] { };
UNUserNotificationCenter.Current.GetDeliveredNotifications(returnedValue => pendingNotifications = returnedValue);
//Then we log the number of pending notifications:
Debug.WriteLine("IncomingNotificationHandler: HandleNotificationsIfAny(...): delivered notification count: " + pendingNotifications.Length);
//And make note of where this was probably called from:
Debug.WriteLine("IncomingNotificationHandler: HandleNotificationsIfAny(...): may have been called from this.WillPresentNotification(...) OR AppDelegate.WillEnterForeground(...)");
return;
});
}
else
{
//In the non-null case, we log the userInfo
Debug.WriteLine("IncomingNotificationHandler: HandleNotificationsIfAny(...): just got info: " + userInfo);
//And make note of where this was probably called from:
Debug.WriteLine("IncomingNotificationHandler: HandleNotificationsIfAny(...): may have been called from AppDelegate.DidReceiveRemoteNotification(...)");
}
}
Third, inside the same class, I implemented the single method that's required by UNUserNotificationCenterDelegate, and I routed to the handler from it:
public override void WillPresentNotification(UNUserNotificationCenter center, UNNotification notification, Action<UNNotificationPresentationOptions> completionHandler)
{
HandleNotificationsIfAny();
}
Fourth, and last, inside AppDelegate, I routed to the same handler from both relevant methods:
//I prefer using DidReceiveRemoteNotification because in my experience
//the other one is sometimes not reliable:
public override void DidReceiveRemoteNotification(UIApplication application,
NSDictionary userInfo,
Action<UIBackgroundFetchResult> completionHandler)
{
//Simply passing on all the parameters called in this method:
IncomingNotificationHandler.Shared.HandleNotificationsIfAny(application, userInfo, completionHandler);
}
//WillEnterForeground also calls the handler without any parameters
//because it doesn't automatically know anything about notifications:
public override void WillEnterForeground(UIApplication application)
{
IncomingNotificationHandler.Shared.HandleNotificationsIfAny();
}
With that, as it stands, I think I'm handling a notification event in the same way no matter how my app is alerted about it, and even when it's not alerted at all.
Does anyone know if I now have it covered, or if there's some other cases I need to handle?
For the first scenario: AppDelegate.ReceivedRemoteNotification
It reflects the objective c method: application:didReceiveRemoteNotification:, but this event has been deprecated since iOS 10: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623117-application?language=objc. So I think there's no need to handle this event.
For the second scenario: AppDelegate.DidReceiveRemoteNotification
You can still utilize it to handle notifications now if you haven't implemented UNUserNotificationCenter and please notice it is only valid after iOS 7+. Moreover, this event will be triggered when app is on the foreground and if your app is on the background, this event only fires when the user clicks the notification to open your application. And there's no way to access the notification's information if the user clicks the icon to open the app.
I don't think handling AppDelegate.WillEnterForeground is a good approach, as it will be called each time the app resumes from background to foreground even though there are no notifications.
For the scenario: UNUserNotificationCenterDelegate
You could only use this feature after iOS 10. Once you have implemented it on the device iOS 10+, DidReceiveRemoteNotification and ReceivedRemoteNotification will never be triggered. WillPresentNotification will be called when app is on the foreground. DidReceiveNotificationResponse will be fired when the app is on the background and user clicks notifications to open it.
As a conclusion, if you want to easily handle the notification AppDelegate.DidReceiveRemoteNotification is enough. If you want to consume the new features of UNUserNotificationCenter, AppDelegate.DidReceiveRemoteNotification and UNUserNotificationCenter should be both involved. The prior one for the iOS 7+ devices and the later one for iOS 10+ devices.
Update:
For iOS 10+, you could use UNUserNotificationCenter.Current.GetDeliveredNotifications to obtain the notifications that are still displayed in Notification Center. And if you only want to support iOS version 10 and later. I think UNUserNotificationCenter is enough, there's no need to implement AppDelegate.DidReceiveRemoteNotification(...) or AppDelegate.ReceivedRemoteNotification(...).
If the app is on background / killed state and the user clicks notification to
open the app, DidReceiveNotificationResponse will be called.
If the
user clicks icon to open your app and the app is killed you should
place your logic code in FinishedLaunching.
If the user clicks icon
to open your app and app is on background, you can handle
WillEnterForeground as you did before.
If the app is on foreground,
handle WillPresentNotification.

Is there a way to pause a repeating notification for one instance?

I want to be able to pause a recurring notification for one time and the time it needs to be trigged, I want it to trigger. I know about removeAllPendingNotifications. But not sure that will do the trick as it seems to remove it completely. What are your thoughts?
My solution is to remove the notification for the day using the code above or just know the identifier and remove the pending notification identifier. Then add the notification back but for the next day.
i.e An example is like a to do app that has recurring task but do not want to be notified of a task that is completed.
This stackoverflow as the code: Scheduling local notifications to repeat daily from tomorrow in Swift
You can use getPendingNotificationRequests and select the identifiers you need to cancel then pass them to removePendingNotificationRequests(withIdentifiers: [String])
UNUserNotificationCenter.current().getPendingNotificationRequests { (reqs) in
var ids = [String]()
reqs.forEach {
if $0.identifier == "someId" {
ids.append($0.identifier)
}
}
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers:ids)
}
`

iOS, CloudKit - do I need to do a fetch when my app starts?

I'm setting up registrations for notifications of iCloud changes.
Say a new device is added to the icloud account, I'm just wondering how that device will get the private database records.
Do I need to do a one off query?
I'm hoping that notifications will be used at all other times.
Let's start with some relevant characteristics of subscription notifications:
First: Subscription Notifications are specific to a user + device pair. If I install you app on my phone, I start getting notifications. I won't get the notifications on another device until I install the app there, too.
Second: Notifications are unreliable. Apple docs are quite clear that they do not guarantee delivery. When you receive a notification, there could have been several prior notifications. Thus, Apple offer's two mechanisms to track which notifications you've seen:
Read/unread status: you can mark notifs as read. Apple's docs contradict themselves about what this actually does. This page says
If you mark one or more notifications as read using a CKMarkNotificationsReadOperation object, those notifications are not returned, even if you specify nil for previousServerChangeToken.
However, this isn't true. The fetch operation clearly returns both read and unread notifications. WWDC 2014 Video 231 (Advanced Cloudkit) contradicts the documentation page, explaining that unread tokens are always returned as well as read tokens so multiple devices can sync up. The video gives a specific example that shows the benefits of this behavior. This behavior is also documented on SO: CKFetchNotificationChangesOperation returning old notifications
change token: each fetch operation will return a change token that you can cache. If you pass the token to a fetch, the fetch will only return tokens from that point, whether read or unread.
At first glance, it would seem that Apple is providing for the behavior you want: install the app on one device, start processing notifications, install the app on a second device, and fetch all those prior notifications in order to catch up.
Unfortunately, as I've documented in CKFetchNotificationChangesOperation: why are READ notifications all nil?, any time I fetch notifications, the ones previously marked as "read" all have nil contents. All the info in the read notifications is lost.
In my scenario, I chose to:
Always fetch the latest record(s) at startup
Fetch notifications using the previously saved change token (if it exists)
Process the new notifications
Mark the notifications as read
save the latest change token for use on the next fetch
For your scenario, you could try:
Fetch notifications using the previously saved change token (if it exists)
process the notifications (DO NOT MARK THEM AS READ)
save the latest change token for use on the next fetch
Your first device will ignore old notifications on each subsequent fetch because you're starting each fetch from the change token point. Your second device will start with a nil change token on the first execution, and thus pick up all of the old notifications.
One word of caution: even though aforementioned WWDC video clearly says Apple keeps all the old notifications, I have found no documentation that says how long they hold this info. It may be forever, it may not.
updated with notification fetch example
Here's how I'm fetching notifications, marking them read, and caching the change token:
#property CKServerChangeToken *notificationServerChangeToken;
Then...
-(void)checkForUnreadNotifications
{
//check for unread cloudkit messages
CKFetchNotificationChangesOperation *op = [[CKFetchNotificationChangesOperation alloc] initWithPreviousServerChangeToken:_notificationServerChangeToken];
op.notificationChangedBlock = ^(CKNotification *notification)
{
//this fires for each received notification. Take action as needed.
};
//maintain a pointer to the op. We will need to look at a property on the
//op from within the completion block. Use __weak to prevent retain problems
__weak CKFetchNotificationChangesOperation *operationLocal = op;
op.fetchNotificationChangesCompletionBlock = ^(CKServerChangeToken *newServerChangeToken, NSError *opError)
{
//this fires once, at the end, after all notifications have been returned.
//this is where I mark the notifications as read, for example. I've
//omitted that step because it probably doesn't fit your scenario.
//update the change token so we know where we left off
[self setNotificationServerChangeToken:newServerChangeToken];
if (operationLocal.moreComing)
{
//more notifications are waiting, recursively keep reading
[self checkForUnreadNotifications];
return;
}
};
[[CKContainer defaultContainer] addOperation:op];
}
To set and retrieve the cached change token from the user defaults, I use the following two functions:
-(void)setNotificationServerChangeToken:(CKServerChangeToken *)newServerChangeToken
{
//update the change token so we know where we left off
_notificationServerChangeToken = newServerChangeToken;
NSData *encodedServerChangeToken = [NSKeyedArchiver archivedDataWithRootObject:newServerChangeToken];
NSUserDefaults *userSettings = [NSUserDefaults standardUserDefaults];
[userSettings setObject:encodedServerChangeToken forKey:UD_KEY_NOTIFICATION_TOKEN_CKSERVERCHANGETOKEN_PROD];
//Note, the development and production cloudkit environments have separate change tokens. Depending on your needs, you may need to save both.
}
and...
-(void)getNotificationServerChangeToken
{
NSUserDefaults *userSettings = [NSUserDefaults standardUserDefaults];
NSData *encodedServerChangeToken = [userSettings objectForKey:UD_KEY_NOTIFICATION_TOKEN_CKSERVERCHANGETOKEN_PROD];
_notificationServerChangeToken = [NSKeyedUnarchiver unarchiveObjectWithData:encodedServerChangeToken];
}

Settings alarms while app is closed

How can I set local notifications with out forcing user to open app.
I need my app set a local notification for sunrise and sunset, but I don't want to ask people open app.
I know I can have up to 64 notifications via scheduleLocalNotification, but I need to set it for a year so I should be able to run app in background and set alarms for future sunrises and sunsets in background.
The simple answer is you can't. Your app can't run whenever it wants in the background; it can't schedule a timer to wake itself up to post more notifications when they are due.
The only way you could come close to something like this is by having a server which send a background push notification to your app as a wake-up call when a new batch of 64 notifications are coming close to needed to be posted.
However this would be relying on the fact the user doesn't terminate your app. If the user does then you'd have to send a non-background push notification to the user and hope they click on it to launch your app.
Android Awareness API has recently announced new features that provide a simple solution for your use-case (that avoids you having to explicitly manage location request or computing sunrise times). The way to achieve what you're trying to do is to create and register a TimeFence specified relative to sunrise/sunset.
For example:
// Create TimeFence
AwarenessFence sunriseFence =
TimeFence.aroundTimeInstant(TimeFence.TIME_INSTANT_SUNRISE,
0, 5 * ONE_MINUTE_MILLIS);
// Register fence with Awareness.
Awareness.FenceApi.updateFences(
mGoogleApiClient,
new FenceUpdateRequest.Builder()
.addFence("fenceKey", sunriseFence, myPendingIntent)
.build())
.setResultCallback(new ResultCallback<Status>() {
#Override
public void onResult(#NonNull Status status) {
if (status.isSuccess()) {
Log.i(TAG, "Fence was successfully registered.");
} else {
Log.e(TAG, "Fence could not be registered: " + status);
}
}
});
You will get callbacks when the fence evaluates to TRUE at sunrise, and when it evaluates back to FALSE at 5-min after sunrise.
Please check Fence API code snippets docs for how to add your custom app logic.

Resources