Croisciento Croisciento - 1 month ago 14
iOS Question

Wait for async code to finish Swift

I've been working on one of my project where I allow users to schedule multiple notifications at their desired time. I'm using the new

UserNotifications
in iOS 10.

In order for all notifications to be scheduled properly each notification needs to have its own unique identifier. I composed mine according to my data models :


  1. The id of my model

  2. A number that increments each time a new notification is created

  3. The above is separated by an underscore



So for example if I had to schedule 15 notifications for the object with the id 3 it would look like this :
3_1, 3_2, 3_3...3_15


Here is how I did it :

@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)")
}
})
}


The
checkDeliveredAndPendingNotifications
makes sure that later on I'll create identifiers that do not exist already.

When it has finished doing its job, I call the
createNotification
which uses the dictionary returned by the previous function to generate a proper identifier.

For example if there were 5 notifications delivered on disk and 10 others waiting for the model with the id 3 it would look like this :

["3" : 15]


The
createNotification
is simply going to take the value in the dictionary and increments it by 1 to create the identifier.

The real problem comes with :

UNUserNotificationCenter.current().add(takeMedecineRequest, withCompletionHandler: { (error) in
if let _ = error {
print("There was an error : \(error)")
}
})


It is an async task. Considering it does not wait, as soon as I get back to my loop in the
generateNotifications
the
checkDeliveredAndPendingNotifications
does not return a correct dictionary because the notification didn't finish creating.

Considering the example above if I had to schedule 3 notifications I'd like to get something like this:

print("identifierDictionary -> \(identifierDictionary)") // ["3":15], ["3":16], ["3":17]
print("unique identifier created -> \(takeMedecineIdentifier") // 3_16, 3_17, 3_18


But right now I'm getting :

print("identifierDictionary -> \(identifierDictionary)") // ["3":15], ["3":15], ["3":15]
print("unique identifier created -> \(takeMedecineIdentifier") // 3_16, 3_16, 3_16


So, how can I wait for this async call to finish before getting back to my loop?

Thanks in advance for your help.

Answer

If you do not need to be able to 'read' from the identifier which notification it is, you could used a randomized string as identifier instead.

Even if it is possible to properly generate a unique id like you do now you should not rely on the control flow for correct id generation. This is generally considered bad coding practice, especially when relying on (3th) party libraries or API's. One change could break it.

You could generate randomized strings as described here. Using a alphanumeric string of 24 characters gives (36+36+10)^24 combinations, making the chance of a collision negligable.

You can use the userinfo dictionary or some other means of persistence to associate the identifiers with specific notifications. If you are using CoreData you can associate notification objects, with unique identifiers, to medicineRequests.