Building Local Calendar Sync Day 04: Settings

Day 4 of building Calendar Sync, which runs locally on my computer and will hopefully solve the Calendar Problem in my life. Today, we built the Settings screen.

Building Local Calendar Sync Day 04: Settings

Now, that the core logic works, it was time to build a Settings View, where users can select, which calendars they want to sync. I call the calendars to watch “Source calendars” and the ones to create blockers in “Target calendars”. For these, I needed checkboxes.

In my original briefing document, I had a rough layout in mind for the Settings View, which I used as a guidance. As you can see, this was before I realized, that I needed to separate between source and target calendars.

Creating the UI

With the help of Google and Stack Overflow, building the UI with helpful controls like VStack  and HStack  for horizontal and vertical layouting or ForEach  for repeating elements to list all calendars, the UI was fun to craft.

But there were other things that gave me headaches. SwiftUI has a force-into-success model, (which I usually value a lot)., that forces you to define a Binding for each Toggle control. Defining a binding of a static UI element is straightforward, but as I did not know, which calendars will show up in the UI and as I was dynamically iterating over them, I had to create a dynamic dictionary for Calendar IDs to their selected state.

@State private var selectedSourceCalendars: [String: Bool] = [:]

As Swift does not have a built-in binding for that, I had to create a Custom Binding for it.

Saving the settings

Displaying the UI was not enough - of course, I also need to store the data. For this, SwiftUI offers the handy UserDefaults Interface, which allows you to read and write simple data to a local storage. In addition, you can mark variables with the @AppStorage annotation, so that every time this variable gets updated, its value get also written to the UserDefaults.

Super handy - works great for the configurable Blocker Title in the settings - unfortunately does not support dictionaries like the ones I defined for the calendar checkboxes 😕. But as I already had Custom Bindings for those, which get notified, when the variable gets updated, I wanted to use them to save the updated dictionaries to the UserDefaults storage.

Unfortunately, it turned out, that the UserDefaults storage does not support dictionaries but only basic arrays, so I had to write a transformation myself, which basically only takes those keys from the dictionary where the value is true  (= checkbox is checked) and craft an array out of those. A bunch of back and force transformation and, I am sure, that there is a smarter way but done is better than perfect.

private func selectedTargetCalendarBinding(for key: String) -> Binding<Bool> {
    return Binding(get: {
        return self.selectedTargetCalendars[key] ?? false
    }, set: {
        self.selectedTargetCalendars[key] = $0
        saveSelectedTargetCalendars()
    })
}

private func saveSelectedSourceCalendars() {
    var array: [String] = []
    for calendar in selectedSourceCalendars {
        if (selectedSourceCalendars[calendar.key] == true) {
            array.append(calendar.key)
        }
    }
    UserDefaults.standard.set(array, forKey: "sourceCalendars")
}

Loading settings

The last part and the one the took me the longest to implement was loading the saved values into the Settings View when it gets opened. Not only did I need to transform the stored String array back to the bindable dictionary, that the checkboxes require, but also I had to find the right place and time to do so.

Again, this is something the @AppStorage annotation would have done for me, but for some weird reason IN THE YEAR 2024, WE CAN NOT STORE DICTIONARIES TO THE LOCAL STORAGE in SwiftUI?! Whatever…

In Swift, the constructor is the init() function of a class, so I thought this might be a good place to read the current values from the local storage and set them as the initial values for my Bindings. Nope! I got overloaded with error messages, yelling cryptic stuff at me about not being allowed to give Bindings a dynamic initial value and not calling self before the whole class is loaded (whatever that means).

Again, done is better than perfect, so I just used the onAppear callback on my view to read the stored settings after the view is rendered and now immediately update the Bindings afterward. I am sure, that this is not the most elegant way, so if you read this and are shocked by my ignorance, pls teach me better. But did I mention that done is better than perfect?

var body: some View {
    VStack(alignment: .leading, content: {
			// ...
		}).onAppear {
        self.selectedSourceCalendars = loadSelectedSourceCalendars()
    }
}

private func loadSelectedSourceCalendars() -> [String: Bool] {
    var dict: [String: Bool] = [:]
    let selected = UserDefaults.standard.stringArray(forKey: "sourceCalendars")
    for id in selected ?? [] {
        dict[id] = true
    }
    
    return dict
}

And this worked. Whenever I need to access the calendars, that the user selected for syncing (also in other places like the CalendarService.swift), I can just consult the UserDefaults store. 💪


☝️ 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.