Search code examples
swifteventkitswift-concurrency

Add multiple events to the Calendar with Concurrency


I created a function to add my course events to the calendar app using EventKit.

After learning the swift concurrency, I want to update my code to make the progress much faster, namely using the detached task or TaskGroup to add these events.

Synchronize code without detached task or task group:

func export_test() {
    Task.detached {
        for i in 0...15 {
            print("Task \(i): Start")
            let courseEvent = EKEvent(eventStore: eventStore)
            courseEvent.title = "TEST"
            courseEvent.location = "TEST LOC"
            courseEvent.startDate = .now
            courseEvent.endDate = .now.addingTimeInterval(3600)
            courseEvent.calendar = eventStore.defaultCalendarForNewEvents
            courseEvent.addRecurrenceRule(EKRecurrenceRule(recurrenceWith: .daily, interval: 1, end: nil))
            do {
                try eventStore.save(courseEvent, span: .futureEvents)
            } catch { print(error.localizedDescription) }
            
            print("Task \(i): Finished")
        }
    }
}

Doing the same thing using the TaskGroup :

func export_test() {
    Task.detached {
        await withTaskGroup(of: Void.self) { group in
            for i in 0...15 {
                group.addTask {
                    print("Task \(i): Start")
                    let courseEvent = EKEvent(eventStore: eventStore)
                    courseEvent.title = "TEST"
                    courseEvent.location = "TEST LOC"
                    courseEvent.startDate = .now
                    courseEvent.endDate = .now.addingTimeInterval(3600)
                    courseEvent.calendar = eventStore.defaultCalendarForNewEvents
                    courseEvent.addRecurrenceRule(EKRecurrenceRule(recurrenceWith: .daily, interval: 1, end: nil))
                    do {
                        try eventStore.save(courseEvent, span: .futureEvents)
                    } catch { print(error.localizedDescription) }
                    
                    print("Task \(i): Finished")
                }
            }
        }
    }
}

The output of the TaskGroup version:

Task 0: Start
Task 1: Start
Task 2: Start
Task 4: Start
Task 3: Start
Task 5: Start
Task 6: Start
Task 7: Start
Task 0: Finished
Task 8: Start
Task 1: Finished
Task 9: Start

Sometimes, only a few tasks will been done, and others will not, or even never been started (I created 16 tasks but only printed 9 in this example). Sometimes, all of these events can be added.

In my point of view, I have created 16 child tasks in the TaskGroup.

Each child task will add one event to the Calendar. I think in this way, I can take the full advantage of the multi-core performance (maybe it's actually not. 🙃)

If I put the for-loop inside the group.addTask closure, it will always have the expected result, but in this way, we only have a single loop so the TaskGroup may no longer needed.

I'm really exhausted🙃🙃.

snapshot: Snapshot


Solution

  • After a long time researching every aspect of the code. I found that the problem is try eventStore.save(courseEvent, span: .futureEvents).

    Alternatively, I use try eventStore.save(..., span: ..., commit: false) and eventStore.commit() to solve the problem.

    I think this is caused by the data races because I'm using swift concurrency. While one event is saving, another one calls save method to save again, leading to data conflicts (just my guess.)

    To solve this, luckily, we can commit a batch of events later using eventStore.commit() to avoid data conflicts. The result is what I expected !!🥳

    And after that optimization, the performance of this function is up to 25% faster (exactly 136ms faster). (haha. Perfect.)

    Final Code (in Swift 5.7):

    func export_test() {
        Task.detached {
            await withTaskGroup(of: Void.self) { group in
                for i in 0...15 {
                    group.addTask {
                        print("Task \(i): Start")
                        let courseEvent = EKEvent(eventStore: eventStore)
                        courseEvent.title = "TEST"
                        courseEvent.location = "TEST LOC"
                        courseEvent.startDate = .now
                        courseEvent.endDate = .now.addingTimeInterval(3600)
                        courseEvent.calendar = eventStore.defaultCalendarForNewEvents
                        courseEvent.addRecurrenceRule(EKRecurrenceRule(recurrenceWith: .daily, interval: 1, end: nil))
                        try eventStore.save(courseEvent, span: .futureEvents, commit: false)
                    }
                }
            }
            
            eventStore.commit()
        }
    }