Building Local Calendar Sync Day 01: Creating a new project and exploring EventKit

Building Local Calendar Sync Day 01: Creating a new project and exploring EventKit

This is the beginning of a blog series about building a small software tool called Local Calendar Sync, which runs locally on my computer and will hopefully solve the Calendar Problem in my life. I initially wanted to outsource this work, but Fiverr doesn't seem to work for software projects. So I will build it myself!

I want to build a macOS app with the native tool chain for Apple, which means using Swift and Xcode. I chose these technologies for the following two reasons: First, I enjoy learning new programming languages and frameworks. For the majority of my career, I was building apps usually with cross-platform frameworks like Xamarin (now known as .NET MAUI) or React Native, to ensure maximum platform coverage. For this project though, I need to access native APIs and want to have an as minimal footprint as possible. I don’t want another Electron app running 24/7 on my computer.

Creating a new project in Xcode

So I opened XCode on my Mac and created a new project. After clicking through the various options, which came with absolutely no descriptions, I chose to create a Multiplatform App, from which I heard I might be able to re-use parts of it for a potential iOS version in the future.

I am considering an iOS version in the future, as the Local Calendar Sync app could also be running on an iPhone or iPad in the Background, which are more likely to be online 24/7 than a desktop computer or laptop. Also, a potential Windows clone could be an option for the future. But for now, I will focus on macOS.

Accessing the calendar and exploring EventKit

The first thing I needed to do is checking, if my idea is actually possible to achieve with the local calendar access in macOS. A short research lead me to the conclusion, that the local calendar is accessed via the EventKit SDK in the Apple world.

All I wanted to know was if I could access the events and their details from the local macOS calendar and if I could create new events (the Blockers) for these events in the other calendars - all without needing to care about authentication against the respective accounts.

Asking for permission

To access the local calendar on macOS, you have to ask the user for permission. This happens in the code and by declaring the need for calendar access in the app’s metadata. The code part is done with two simple lines:

// Create an event store
let store = EKEventStore()
                    
// Request full access
guard try await store.requestFullAccessToEvents() else { return }

But before this code can even be executed, we have to declare the fact, that we want to access the calendar and why in the Info.plist file, by setting the Privacy - Calendars Full Access Usage Description property and adding a description.

Reading calendar entries

Now it was time to read the first entries. In the ContentView.swift file, which declares the default window that gets shown when starting the newly created application, I added a Button, that when clicked executes some code. This code was supposed to fetch events from all calendars between now and one year from now and write their details onto the console. To also test, if new events can be created, I also wanted to create a single test event from the app.

After some fiddling with weird predicates and filters, the final test-code looks like this:

import SwiftUI
import EventKit

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            
            Text("Hello CalSync!")
            
            Button(action: {
                Task {
                    // Create an event store
                    let store = EKEventStore()
                    
                    // Request full access
                    guard try await store.requestFullAccessToEvents() else { return }
                    
                    // Create a predicate
                    let predicate = store.predicateForEvents(
                        withStart: Date(),
                        end: Date().addingTimeInterval(365 * 24 * 60 * 60), // 1 year
                        calendars: nil)
                    
                    // Fetch the events
                    let events = store.events(matching: predicate)
                    let relevantEvents = events.filter { $0.availability == EKEventAvailability.busy }
                    let sortedEvents = relevantEvents.sorted { $0.compareStartDate(with: $1) == .orderedAscending }
                                        
                    print("\\(sortedEvents.count) events found.")
                    for event in sortedEvents {
                        print("\\(event.title) is from \\(event.calendar.title) at \\(Calendar.current.dateComponents([.day, .year, .month], from: event.startDate)) full day: \\(event.isAllDay)")
                    }

										// Create a test event
                    let event = EKEvent.init(eventStore: store)
                    event.title = "Local Calendar Sync Test"
                    event.calendar = store.defaultCalendarForNewEvents
                    event.startDate = Date()
                    event.endDate = Date.init(timeInterval: 3600, since: Date())
                    
                    do {
                      try store.save(event, span: .thisEvent)
                    } catch {
                      print("saving event error: \\(error)")
                    }
                }
            }, label: {
                Text("Fetch events")
            })
        }
        .padding()
    }
}

Success! When running the app and clicking the button, all upcoming events from all calendars are printed to the console and a new event is created in my default calendar!

Yes - Super nice! This removes one of the biggest potential blockers for this project, as this verifies, that I read from and write to all calendars across accounts, as long as they are connected to the macOS system and available in the native calendar app. No need to access the calendars directly through the APIs of Office, Outlook, Google etc.

What’s next?

This is, where the story should have ended. I solved the technical challenging bit and found the right technology for my project. Here I wanted to give it to a Freelancer via Fiverr, to build the rest for me and transform my little code experiments into a proper tool.

That I am writing these lines means: It all came different. Although I tried to describe my requirements and the technical restrictions as clear as even possible to me, I did not find a single developer on Fiverr, which was available and understood my requirements or even read the document, I’ve created carefully. The details of that miserable experience are for a different blog post, but it was so frustrating, that I decided to develop the tool on my own and document the process here.


☝️ Advertisement Block: I will buy myself a pizza every time I make enough money with these ads to do so. So please feed a hungry developer and consider disabling your Ad Blocker.