How can I know when UNUserNotificationCenter's removeAllPendingNotificationRequests() has completed? - ios

The iOS docs say that UNUserNotificationCenter's removeAllPendingNotificationRequests() is asynchronous.
What I want to do is this:
Call removeAllPendingNotificationRequests() to get rid of all my scheduled notifications
Schedule a bunch of new notifications, some of which may or may not have the same IDs as what was there previously
But since the documentation says that the method is asynchronously running on another thread (and there is no completion callback parameter) I'm worried that sometimes, depending on the vagaries of threads and timing and whatnot, that step #1 will still be going as I am creating things in step 2 and therefore it will also kill some of the new notifications I'm making.
This kind of stuff is a little tricky to test manually, since it depends on timing. So I'm curious is anyone knows if this is something I should be worried about or not...

In documentation for add notification I found this:
Calling -addNotificationRequest: will replace an existing notification
request with the same identifier.
Maybe the solution would be something like this:
Create new notification requests
Get all pending and filter out only the ones that will not be replaced
Delete not replaced
Add all new notifications
let center = UNUserNotificationCenter.current()
// Create new requests
let newRequests: [UNNotificationRequest] = [...]
let identifiersForNew: [String] = newRequests.map { $0.identifier }
center.getPendingNotificationRequests { pendingRequests in
// Get all pending notification requests and filter only the ones that will not be replaced
let toDelete = pendingRequests.filter { !identifiersForNew.contains($0.identifier) }
let identifiersToDelete = toDelete.map { $0.identifier }
// Delete notifications that will not be replaced
center.removePendingNotificationRequests(withIdentifiers: identifiersToDelete)
// Add all new requests
for request in newRequests {
center.add(request, withCompletionHandler: nil)
}
}

I have the same case as you and up to know I don't have a problem with this code:
center.getPendingNotificationRequests(completionHandler: { notifications in
var notificationIds:[String] = []
for notification in notifications {
if notification.identifier != "something_taht_I_dont_dismiss"{
notificationIds.append(notification.identifier)
}
}
self.center.removePendingNotificationRequests(withIdentifiers: notificationIds)
createAllNewNotifications()
})
If you want to double check all if the pending notifications are removed you can create simple recursion method for checking.
func removeAllNotificationsSafe() {
center.removeAllPendingNotificationRequests()
checkNotificationsAreRemoved()
}
func checkNotificationsAreRemoved() {
center.getPendingNotificationRequests(completionHandler: { notifications in
if notifications.count > 0 {
self.checkNotificationsAreRemoved()
} else {
self.doWhathverYouWant()
}
}
}
I don't believe this is needed, because all the actions of UNUserNotificationCenter will be synchronized between each other.

Related

local notification is not displayed

I'm working on a Xamarion iOS app. I want to display a local notification when an event occurs in my app, Like Outlook does when an email is received.
I'm using the following code, after having received the right to send notification from the user, of course:
var content = new UNMutableNotificationContent();
if (!string.IsNullOrWhiteSpace(value: mySoundFile))
{
content.Sound = UNNotificationSound.GetCriticalSound(mySoundFile);
}
if (!string.IsNullOrEmpty(myChannelDescription))
{
content.ThreadIdentifier = myChannelDescription;
}
content.Title = "MyTitle";
content.Body = "MyText";
var trigger = UNTimeIntervalNotificationTrigger.CreateTrigger(0.1, false);
var request = UNNotificationRequest.FromIdentifier(notification.Id.ToString(), content, trigger);
var center = UNUserNotificationCenter.Current;
center.AddNotificationRequest(request, null);
But the notification is not displayed.
Any help appreciated.
The reason for not displaying the notification could be several things. Try the following solutions:
Make sure that the application has the required rights to send notifications and that the user has enabled them.
Check if the sound file is valid and located in the application package. It may also be necessary to add it to the Info.plist file.
Check if the thread identifier is valid and not repeatable.
Check if the trigger time is valid. Setting 0.1 seconds is very short and may not be enough to display the notification.
If the above solutions do not work, it is worth using debugging tools to more closely examine why the notification is not displayed.
On iOS, you must request permission to use notifications before attempting to schedule them. Just like this, you can try to check if the following code is added to your project:
UNUserNotificationCenter.Current.RequestAuthorization(UNAuthorizationOptions.Alert, (approved, err) =>
{
...
});
You can change your AddNotificationRequest as follows to see if there is an error in the notification:
center.AddNotificationRequest(request, (err) =>
{
if (err != null)
{
throw new Exception($"Failed to schedule notification: {err}");
}
});
For more details, you can refer to the following documents to check some permission issues:
Enhanced User Notifications in Xamarin.iOS | Microsoft
Asking permission to use notifications | Apple Developer
UpDate: If your app is in the foreground. You could try implementing the delegate userNotificationCenter(_:willPresent:withCompletionHandler:) which will be called when a notification arrives while the app is in the foreground. Refer to the following code:
UNUserNotificationCenter.Current.Delegate = new TestDelegate();
public class TestDelegate: UNUserNotificationCenterDelegate
{
public override void WillPresentNotification(UNUserNotificationCenter center, UNNotification notification, Action<UNNotificationPresentationOptions> completionHandler)
{
completionHandler(UNNotificationPresentationOptions.Alert);
}
}

Is it possible to turn off drawing the UI during the next run loop cycle?

Is there any way I can temporarily disable the drawing of the UI (and the progression of animations and other related things)?
I have a notification system which coalesces notifications, so that multiple notifications which happen in a single run loop cycle are coalesced into one for the interested party, kind of like how setNeedsDisplay can be called any number of times in a single cycle, but the drawing only happens once.
The problem is, the way I'm coalescing notifications is to schedule the forward notification on the next run loop, and skip new updates in the meantime, kind of like this:
private var startedCoalescingUpdates = false
func receiveNotification() {
guard startedCoalescingUpdates == false else {
return
}
startedCoalescingUpdates = true
OperationQueue.main.addOperation {
self.setForwardNotification = false
self.delegate.updateOccurred()
}
}
Notifications can be chained together, so that one notification triggers another notification, which can trigger another, etc.
Because OperationQueue.main.addOperation happens on the next cycle (or at least it does if I call it from another block which was scheduled in addOperation), some UI drawing occurs whilst the notifications propagate. The UI can show several frames which look incorrect; I'd rather not draw the UI or respond to events, etc, until the notifications are all done, in order for all the data to be fully ready before its drawn for the first time. I'd probably do something like:
private var startedCoalescingUpdates = false
func receiveNotification() {
guard startedCoalescingUpdates == false else {
return
}
startedCoalescingUpdates = true
//////
stopDrawingUI()
//////
OperationQueue.main.addOperation {
self.setForwardNotification = false
//////
resumeDrawingUI()
//////
self.delegate.updateOccurred()
}
}
Is this possible?

Schedule and handle local notifications in iOS 10

I've been working on one of my project where I allow users to schedule multiple notifications at their desired time.
iOS 10 gave us the ability to use a DateComponents as the fire date for our notification but I'm kind of lost as to how I'm supposed to schedule and handle multiple notifications.
Each notification needs its own request which then in turn needs its own identifier else you cannot create multiple notifications.
I figured I had to work with the identifier to schedule and handle notifications, but now my code is such a mess that I've been seriously asking myself if there hasn't been an easier way of doing this.
According to my data model the notification unique identifier is composed of :
The id of my model
A number that increments each time a new notification is created
The above is separated by an underscore
So for example if I had to schedule 10 notifications for the object with the id 3 it would look like this : 3_1, 3_2, 3_3...
Every time I receive a notification I loop trough the received notifications to update my UI. And when the user desires to delete the received notifications for a specific model, I loop through the received notification's unique identifiers by checking the identifiers that starts with the same ID.
I don't really see how I could manage to do it otherwise because according to the documentation, the only way of deleting a delivered notification is by using the identifier : UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers:)
The thing is, it's creating all sort of problems and while I could easily correct them it looks very hacky. I'm not really proud of what I've done and I am looking for more clever ways of getting around it. I intentionally did not post any code because that's not the problem. The approach is the real problem.
I'm asking here because the UserNotifications framework is pretty new and I haven't been able to find ressources taking about this subject.
Any idea ? Thanks in advance.
Edit : Here's some code.
#available(iOS 10.0, *)
func checkDeliveredAndPendingNotifications(completionHandler: #escaping (_ identifierDictionary: Dictionary<String, Int>) -> ()) {
var identifierDictionary:[String: Int] = [:]
UNUserNotificationCenter.current().getDeliveredNotifications { (notifications) in
for notification in notifications {
let identifierArraySplit = notification.request.identifier.components(separatedBy: "_")
if identifierDictionary[identifierArraySplit[0]] == nil || identifierDictionary[identifierArraySplit[0]]! < Int(identifierArraySplit[1])! {
identifierDictionary[identifierArraySplit[0]] = Int(identifierArraySplit[1])
}
}
UNUserNotificationCenter.current().getPendingNotificationRequests(completionHandler: { (requests) in
for request in requests {
let identifierArraySplit = request.identifier.components(separatedBy: "_")
if identifierDictionary[identifierArraySplit[0]] == nil || Int(identifierArraySplit[1])! > identifierDictionary[identifierArraySplit[0]]! {
identifierDictionary[identifierArraySplit[0]] = Int(identifierArraySplit[1])
}
}
completionHandler(identifierDictionary)
})
}
}
#available(iOS 10.0, *)
func generateNotifications() {
for medecine in medecines {
self.checkDeliveredAndPendingNotifications(completionHandler: { (identifierDictionary) in
DispatchQueue.main.async {
self.createNotification(medecineName: medecine.name, medecineId: medecine.id, identifierDictionary: identifierDictionary)
}
})
}
}
#available(iOS 10.0, *)
func createNotification(medecineName: String, medecineId: Int identifierDictionary: Dictionary<String, Int>) {
let takeMedecineAction = UNNotificationAction(identifier: "TAKE", title: "Take your medecine", options: [.destructive])
let category = UNNotificationCategory(identifier: "message", actions: [takeMedecineAction], intentIdentifiers:[], options: [])
UNUserNotificationCenter.current().setNotificationCategories([category])
let takeMedecineContent = UNMutableNotificationContent()
takeMedecineContent.userInfo = ["id": medecineId]
takeMedecineContent.categoryIdentifier = "message"
takeMedecineContent.title = medecineName
takeMedecineContent.body = "It's time for your medecine"
takeMedecineContent.badge = 1
takeMedecineContent.sound = UNNotificationSound.default()
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 60, repeats: false)
var takeMedecineIdentifier = ""
for identifier in identifierDictionary {
if Int(identifier.key) == medecineId {
let nextIdentifierValue = identifier.value + 1
takeMedecineIdentifier = String(medecineId) + "_" + String(nextIdentifierValue)
}
}
let takeMedecineRequest = UNNotificationRequest(identifier: takeMedecineIdentifier, content: takeMedecineContent, trigger: trigger)
UNUserNotificationCenter.current().add(takeMedecineRequest, withCompletionHandler: { (error) in
if let _ = error {
print("There was an error : \(error)")
}
})
}
In the checkDeliveredAndPendingNotifications method, I loop through all the pending and delivered notifications, so later on I can create an identifier that does not exist already. I haven't found another way to generate unique identifiers for each notification.
As most of the work is done on another async queue, I've also created a completion handler when he has finished its job. I then call the createNotification method on the main thread (because I'm using Realm, and I'm obliged too do this) which should create a notification.
The problem here is the func add(UNNotificationRequest, withCompletionHandler: (Error?) -> Void)? = nil) method which is also doing work asynchronously. So when I go back to my loop in generateNotifications the checkDeliveredAndPendingNotifications return incorrect data. Well not incorrect it's just that the notification hasn't been created yet...
I'm a total noob with threading and I'm stuck with these kind of operations and I don't know where to go. I'm not sure I'm approaching the problem the right way.
You can get all notification and set/Delete as you need. Have a look on this accepted answer for both Objc c and swift.
How to schedule a local notification in iOS 10 (objective-c)

Contact Framework equivalent to ABAddressBook.ABAddressBookRegisterExternalChangeCallback

I am migrating an application from the deprecated Address Book Framework to the new Contacts Framework. The application utilizes ABAddressBookRegisterExternalChangeCallback to be notified when another application changes a contact.
I am unable to find equivalent functionality in the Contacts Framework. Apple documentation says to use the default notification center with the CNContactStoreDidChangeNotification notification:
The notification posted when changes occur in another CNContactStore.
Taking Apple's advice, my code looks like this:
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: "contactsChanged:",
name: CNContactStoreDidChangeNotification,
object: nil)
However, I have found two problems with this approach:
I am notified for all changes, including those made by my own application.
Notifications are spurious - I receive many notifications for a single change.
If I log the debug description of the notification when the change was made within my app, I get something like this:
NSConcreteNotification 0x7d3370e0 {name = CNContactStoreDidChangeNotification; userInfo = {
CNNotificationOriginationExternally = 1;
CNNotificationSourcesKey = (
);
}}
And if the changes are made externally:
NSConcreteNotification 0x7bf7a690 {name = CNContactStoreDidChangeNotification; userInfo = {
CNNotificationOriginationExternally = 1;
CNNotificationSourcesKey = (
);
}}
As you can see, nothing obvious with which to distinguish them.
Can anyone tell me how to get the same behavior from the Contacts Framework as one can get from ABAddressBookRegisterExternalChangeCallback?
First, I'd recommend filing a bug with Apple about the lack of a way to identify internal vs external changes in the API.
As a possible workaround, you could see if unregistering your observer before making a change and re-registering immediately afterward ensures that you miss all of your change notifications and still get all the external ones:
class ContactsThingy {
var observer: NSObjectProtocol?
let contacts = CNContactStore()
func contactStoreDidChange(notification: NSNotification) {
NSLog("%#", notification)
}
func registerObserver() {
let center = NSNotificationCenter.defaultCenter()
observer = center.addObserverForName(CNContactStoreDidChangeNotification, object: nil, queue: NSOperationQueue.currentQueue(), usingBlock: contactStoreDidChange)
}
func unregisterObserver() {
guard let myObserver = observer else { return }
let center = NSNotificationCenter.defaultCenter()
center.removeObserver(myObserver)
}
func changeContacts(request: CNSaveRequest) {
unregisterObserver() // stop watching for changes
defer { registerObserver() } // start watching again after this change even if error
try! contacts.executeSaveRequest(request)
}
}

NSMetadataQuery’s update notification interferes with (run loop?)

I emailed an Apple engineer last week about a problem with my NSMetadataQuery.
Here’s the email:
Hi,
I'm writing a document-based app or iOS and my method for renaming (moving the document to a new location) seems to conflict with the running NSMetadataQuery.
The query updates a couple of time after the move method is called, the first time it has the old URL of the item that just moved, and the next it has the new URL. However, because of my updating method (below) if a URL has been removed since the update, my model removes the deleted URL and vice versa for if it finds a URL which doesn't exist yet.
I think my problem is one of two issue, either the NSMetadataQuery's update method is insufficient and doesn't check an item's URL for the 'correct' attributes before deleting it (although looking over documentation I can't see anything that would suggest I'm missing something) or my renaming method isn't doing something it should.
I have tried disabling updates at the start of the renaming method and reenabling once all completion blocks are finished but it doesn't make any difference.
My NSMetadataQuery's update method:
func metadataQueryDidUpdate(notification: NSNotification) {
ubiquitousItemsQuery?.disableUpdates()
var ubiquitousItemURLs = [NSURL]()
if ubiquitousItemsQuery != nil && UbiquityManager.sharedInstance.ubiquityIsAvailable {
for var i = 0; i < ubiquitousItemsQuery?.resultCount; i++ {
if let result = ubiquitousItemsQuery?.resultAtIndex(i) as? NSMetadataItem {
if let itemURLValue = result.valueForAttribute(NSMetadataItemURLKey) as? NSURL {
ubiquitousItemURLs.append(itemURLValue)
}
}
}
// Remove deleted items
//
for (index, fileRepresentation) in enumerate(fileRepresentations) {
if fileRepresentation.fileURL != nil && !contains(ubiquitousItemURLs, fileRepresentation.fileURL!) {
removeFileRepresentations([fileRepresentation], fromDisk: false)
}
}
// Load documents
//
for (index, fileURL) in enumerate(ubiquitousItemURLs) {
loadDocumentAtFileURL(fileURL, completionHandler: nil)
}
ubiquitousItemsQuery?.enableUpdates()
}
}
And my renaming method:
func renameFileRepresentation(fileRepresentation: FileRepresentation, toNewNameWithoutExtension newName: String) {
if fileRepresentation.name == newName || fileRepresentation.fileURL == nil || newName.isEmpty {
return
}
let newNameWithExtension = newName.stringByAppendingPathExtension(NotableDocumentExtension)!
// Update file representation
//
fileRepresentation.nameWithExtension = newNameWithExtension
if let indexPath = self.indexPathForFileRepresentation(fileRepresentation) {
self.reloadFileRepresentationsAtIndexPaths([indexPath])
}
UbiquityManager.automaticDocumentsDirectoryURLWithCompletionHandler { (documentsDirectoryURL) -> Void in
let sourceURL = fileRepresentation.fileURL!
let destinationURL = documentsDirectoryURL.URLByAppendingPathComponent(newNameWithExtension)
// Update file representation
//
fileRepresentation.fileURL = destinationURL
if let indexPath = self.indexPathForFileRepresentation(fileRepresentation) {
self.reloadFileRepresentationsAtIndexPaths([indexPath])
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in
let coordinator = NSFileCoordinator(filePresenter: nil)
var coordinatorError: NSError?
coordinator.coordinateWritingItemAtURL(sourceURL, options: .ForMoving, writingItemAtURL: destinationURL, options: .ForReplacing, error: &coordinatorError, byAccessor: { (newSourceURL, newDestinationURL) -> Void in
var moveError: NSError?
let moveSuccess = NSFileManager().moveItemAtURL(newSourceURL, toURL: newDestinationURL, error: &moveError)
dispatch_async(dispatch_get_main_queue(), { () -> Void in
assert(moveError == nil || moveSuccess, "Error renaming (moving) document from \(newSourceURL) to \(newDestinationURL).\nSuccess? \(moveSuccess).\nError message: \(moveError).")
if let query = self.ubiquitousItemsQuery {
query.enableUpdates()
}
if moveError != nil || moveSuccess {
// TODO: Implement resetting file rep
}
})
})
})
}
}
I had a reply almost instantly but since then there’s been no reply.
Here’s the reply
One of the big things that jumps out at me is your usage of disableUpdates() and enableUpdates(). You’re executing them both on the same turn of the run loop, but NSMetadataQuery delivers results asynchronously. Since this code executes within your update notification, it is executing synchronously with respect to the query. So from the query’s point-of-view, it’s going to begin delivering updates by posting the notification. Posting a notification is a synchronous process, so while it’s posting the notification, updates will be disabled and the re-enabled. Thus, by the time the query is done posting the notification, it’s back in the exact same state it was in when it started delivering results. It sounds like that’s not the behavior you’re wanting.
Here’s where I need help
I took this to assume that NSMetadataQuery has some kind of cache which it adds results to while updates are disabled and when enabled, those (perhaps many) cache results are looped through and each are sent via the updates notification.
Anyway, I had a look at run loops on iOS and although I understand them as much as I can on my own, I don’t understand how the reply is helpful, i.e how to actually fix the problem - or what’s even causing the problem.
If anyone has any good idea I’d love your help!
Thanks.
Update
Here’s my log of when functions start and end:
start metadataQueryDidUpdate:
end metadataQueryDidUpdate:
start metadataQueryDidUpdate:
end metadataQueryDidUpdate:
start renameFileRepresentation:toNewNameWithoutExtension
start metadataQueryDidUpdate:
end metadataQueryDidUpdate:
end renameFileRepresentation:toNewNameWithoutExtension
start metadataQueryDidUpdate:
end metadataQueryDidUpdate:
start metadataQueryDidUpdate:
end metadataQueryDidUpdate:
start metadataQueryDidUpdate:
end metadataQueryDidUpdate:
I was having the same problem. NSMetaDataQuery updates tell you if there is a change, but does not tell you what that change was. If the change is a rename, there is no way to identify the previous name, so I can find the old entry in my tableView. Very frustrating.
But, you can get the information by using NSFileCoordinator and NSFilePresenter.
Use the NSFilePresenter method presentedSubitemAtURL(oldURL: NSURL, didMoveToURL newURL: NSURL)
As you noted, the query changed notification is called once with the old URL, and once with the new URL. The method above is called between those two notifications.

Resources