What's the thread-safe way of fetching Reminders from various calendars? I'm just trying to count all reminders and print them. The printing works but the counting doesn't. Is there a race-condition because fetching reminders is asynchronous?
func loadFromCalendars(cals: [EKCalendar], completed: (NSError?)->()) {
// STEP 1 OF CREATING AN OVERALL COMPLETION BLOCK: Create a dispatch group.
let loadCalendarServiceGroup: dispatch_group_t = dispatch_group_create()
// Define errors to be processed when everything is complete.
// One error per service; in this example we'll have two
let configError: NSError? = nil
let preferenceError: NSError? = nil
var reminderCounter = 0
let eventStore : EKEventStore = EKEventStore()
eventStore.requestAccessToEntityType(EKEntityType.Event, completion: {
granted, error in
if (granted) && (error == nil) {
print("granted \(granted)")
print("error \(error)")
}
})
// Go through calendars.
for cal in cals {
let remindersPredicate = eventStore.predicateForRemindersInCalendars([cal])
// STEP 2 OF CREATING AN OVERALL COMPLETION BLOCK: Adding tasks to a dispatch group
dispatch_group_enter(loadCalendarServiceGroup)
eventStore.fetchRemindersMatchingPredicate(remindersPredicate) {
// MARK: Begininning of thread
reminders in
_ = (reminders!).map {
// TRYING TO COUNT HERE THE REMINDERS. ALWAYS PRINTS 0!
reminder -> EKReminder in
print(reminder.title)
reminderCounter += 1
return reminder
}
dispatch_async(dispatch_get_main_queue()) {
self.sendChangedNotification() // refreshes the UI
}
}
// STEP 3 OF CREATING AN OVERALL COMPLETION BLOCK: Leave dispatch group. This must be done at the end of the completion block.
dispatch_group_leave(loadCalendarServiceGroup)
// MARK: End of thread
}
// STEP 4 OF CREATING AN OVERALL COMPLETION BLOCK: Acting when the group is finished
dispatch_group_notify(loadCalendarServiceGroup, dispatch_get_main_queue(), {
print("************ reminder count: \(reminderCounter) ****************")
// Assess any errors
var overallError: NSError? = nil;
if configError != nil || preferenceError != nil {
// Either make a new error or assign one of them to the overall error. Use '??', which is the "nil Coalescing Operator". It's syntactic sugar for the longer expression:
// overallError = configError != nil ? configError : preferenceError
overallError = configError ?? preferenceError
} // Now call the final completion block
// Call the completed function passed to loadCalendarHelper. This will contain the stuff that I want executed in the end.
completed(overallError)
})
}
EDIT Thanks for the great tips, jtbandes! I simplified my code (a lot!) One question - I'm chaining some functions that change the resulting data structure. How can I make in the below code groupArrayBy() thread-safe?
public extension SequenceType {
/// Categorises elements of self into a dictionary, with the keys given by keyFunc
func groupArrayBy<U : Hashable>(@noescape keyFunc: Generator.Element -> U) -> [U:[Generator.Element]] {
var dict: [U:[Generator.Element]] = [:]
for el in self {
let key = keyFunc(el)
if case nil = dict[key]?.append(el) { dict[key] = [el] }
}
return dict
}
}
func loadFromCalendars(cals: [EKCalendar], completed: (NSError?)->()) {
let configError: NSError? = nil
let preferenceError: NSError? = nil
withEstore { // retrieves the EKEventStore
estore in
let predicate = estore.predicateForRemindersInCalendars(cals)
estore.fetchRemindersMatchingPredicate(predicate) { reminders in
print("Number of reminders: \(reminders?.count ?? 0)") // Prints correct result
let list = (reminders!).map {
// this map still works, it seems thread-safe
reminder -> ReminderWrapper in
return ReminderWrapper(reminder: reminder) // This still works. ReminderWrapper is just a wrapper class. Not very interesting...
}.groupArrayBy { $0.reminder.calendar } // ERROR: groupArrayBy doesn't seem to be thread-safe!
print("Number of reminders: \(Array(list.values).count)") // Prints a too low count. Proves that groupArrayBy isn't thread-safe.
dispatch_async(dispatch_get_main_queue()) {
self.sendChangedNotification() // refreshes the UI
completed(configError ?? preferenceError)
}
}
}
}
A few changes should be made to this code:
dispatch_group_leave(loadCalendarServiceGroup)
must be inside the fetchRemindersMatchingPredicate
block. Otherwise, the block you passed to dispatch_group_notify
will execute before the fetches are finished, which entirely defeats the purpose of using a group.
The requestAccessToEntityType
call is also asynchronous, but your code simply continues after starting the access request, without waiting for it to finish. You might want to chain your completion blocks together.
You're requesting access to the .Event
type, but you probably want .Reminder
.
reminderCounter += 1
is not thread-safe. You might want to dispatch_async
onto a serial queue before changing the counter (so there is no contention between threads), or you could use the OSAtomicAdd family of functions.
Instead of _ = (reminders!).map { reminder in ... }
, I would recommend you just use for reminder in reminders { ... }
.
I think what you're doing is overly complicated.
Notice that predicateForRemindersInCalendars
takes an array of calendars. You can simply pass all your calendars, cals
, to get a single predicate encompassing all of them, and run a single query:
let predicate = eventStore.predicateForRemindersInCalendars(cals)
eventStore.fetchRemindersMatchingPredicate(predicate) { reminders in
print("Number of reminders: \(reminders?.count ?? 0)")
dispatch_async(dispatch_get_main_queue()) {
self.sendChangedNotification() // refreshes the UI
completed(configError ?? preferenceError)
}
}