As I refine my app in preparation for turning it in, I continue to struggle with the non-linear nature of object-oriented programming.
Don't get me wrong, it's fantastic. It allows me to do stuff I couldn't have dreamed of in a previous life! But it also opens the door to weird and challenging behaviors.
A Mysterious Mysterious Double Double
Yesterday I solved one of those mysteries.
My app displays events from Apple's Calendar App, so the user is presented a list of Apple calendars and asked to choose which ones will supply events to be displayed. That page was working great except on first launch, when some or all of the available calendars were showing up twice. This happened consistently, although which of the calendars got doubled seemed random.
Careful Sleuthing
I used some print statements and breakpoints to determine more specifically what was happening.
The function that collects my calendars into an array clears out the current array, then adds new calendars to it. I put some print statements in, believing it was silly because the function itself runs linearly from top to bottom as functions do, so my print statements should obviously output:
"I'm clearing the calendar array."
"I'm adding new calendars."
But when I ran the app, here's what I got instead:
"I'm clearing the calendar array."
"I'm clearing the calendar array."
"I'm adding new calendars."
"I'm adding new calendars."
That explains the doubling, sort of. But how is it possible? The function seems to be looping where there aren't any loops!
The only explanation I could think of would be that the function is getting called twice at the same time. Sure enough, adding a break point showed that this was the case; it was running concurrently on two different threads. But why?
More Sleuthing
So I searched the code for the function name, and no - I only ever call it explicitly one time, from the init of my EventManager class. The app creates only one instance of EventManager, so...
Well, there is one other place the function gets called, in the same init, buried within this statement:
NotificationCenter.default.addObserver(self, selector: #selector(self.updateCalendarsAndEvents), name: .EKEventStoreChanged, object: eventStore)
This notification will call the updateCalendarsAndEvents function whenever the data in the Apple Calendar App changes. But I'm not making any changes!
Although... the user IS asked for permission to access the Calendar app at the start of my update function. The way permissions work, the system only actually asks the user for permission once - on first launch. That could explain why the calendar list only misbehaves on first launch!
A Working Theory
So here's my theory: The app calls the update function when it initializes my EventManager. The function pauses for a response to the permissions request. Meanwhile, the Notification command is executed, so we are now subscribed. When the user responds with permission, the update function continues. Simultaneously, NotificationCenter interprets the change in permissions as reason enough to trigger the update function. So before the function call from init can be completed, the same function gets a second call from notifications, and now it's running on two different threads. The timing is close enough that both calls create new calendars after both calls erase existing calendars. It's also close enough that sometimes a new calendar created by the first call gets erased by the second call, which explains why the number of duplicated calendars seems random.
The Fix
A fix seemed impossible for a moment... All three parts are required: I need to updateCalendarsAndEvents when the EventManager is initialized, I need to ask the user for permission to access Calendar, and I need to subscribe to changes, so my app can fetch new calendars and events when they become available.
So I dug into the documentation, and soon discovered that the logic of my app was based on a misunderstanding.
I had thought I needed to check permissions every time I accessed the EventStore. But that is not the case. Requesting permission is a separate function from fetching data. So!
My mistaken code looked like this:
func updateCalendarsAndEvents() {
eventStore.requestAccess(to: EKEntityType.event) {
// closure containing the logic for erasing the current array and replacing it with new calendars
}
I fixed it by creating a Bool to check my current permission status, and moving the request for access into my EventManager init, like so:
init() {
NotificationCenter.default.addObserver(self, selector: #selector(self.updateCalendarsAndEvents), name: .EKEventStoreChanged, object: eventStore)
if StateBools.shared.noPermissionForCalendar {
eventStore.requestAccess(to: EKEntityType.event) {_,_ in}
// If permission is newly given, notifications will fetch new data
} else {
// If permission is already given, fetch new data.
updateCalendarsAndEvents()
}
}
With my new logic in place, the doubling has ceased! I just needed to make sure permissions were requested only when necessary, and allow the correct function call to handle the update so they weren't both trying to do it at once.
In Conclusion
Non-linear programming can lead to odd behaviors such as duplicated data caused by one function running twice concurrently, but careful sleuthing and clear logic will save the day!
Top comments (4)
Glad you got it working! A more defensive approach might be to ensure that only one thread or continuation can be in the loop at the same time, that way, even if something else decides to create overlapping executions you would never be able to get doubling as the second one would wait for the first to complete and then start afresh.
Hi Mike, sounds like a smart approach - thanks!
I'm not sure how to do that at this point... in another similar situation I set a bool to false at the start of the process, and true at the end; then I checked that bool before calling the function, but this relies on me remembering to check at each call site, and also depends on the second call being un-necessary, so it feels a bit kludgey. I'm guessing what I need is to learn more about thread safety. Swift has an
async
andawait
system which I tried to implement but ran into a bunch of errors, so that's on my to-do list to figure out.I think you could probably do this with a critical section - I'm no expert on Swift but perhaps something like:
You'd declare the lock globally or as a singleton.
Thanks, Mike - I will look into that...