This week I set myself the goal of getting home screen widgets working in Habitat. I hadn’t touched the codebase in over a month since I’ve had to deal with the busiest semester of my MSc course. Before I went on a mini-hiatus, I added a Bubble Grid widget which simply displays all of your habits in a grid of Habitat-style bubbles.
Since my app uses SwiftData for storing habit data, the Bubble Grid had to query the database somehow. After a little research, it turns out this is actually very easy.
|
|
All it takes is a query in the widget entry view and; et voilà! Everything just works. Great.
Now, this is where I stopped for the Bubble Grid, since all I wanted to do was display a list of habits. My next widget idea, the Habit Heatmap, relies on a widget configuration intent to let the user pick whichever habit they want to display a heatmap for (per widget).
It’s here that I ran into problems. The issue with both SwiftData and WidgetKit being so new is that there isn’t much support out there for niche problems. And sometimes Apple’s guides aren’t fully updated, or only partially help.
I set up a new widget with a configuration app intent skeleton that looked something like this. (Note that the HabitEntity
struct doesn’t exist yet, but will be created in a later step):
|
|
Now I needed some way to change this selectedHabit
to something representing an actual Habit
. Apple’s documentation on custom intents was a useful starting point, and I ended up with this:
|
|
So now I have a way to select something that represents a habit, and a way to search for that habit via the HabitEntity.id
. The part that I was left scratching my head with was the HabitQuery
entities functions which are supposed to return a list of HabitEntity
. Since you can’t use SwiftData queries outside of SwiftUI views, I thought why not try a raw SwiftData fetch:
|
|
Computer says no. The solution? UserDefaults!
The eagle-eyed among you may have noticed HabitEntity
conforms to Codable
, allowing it to be encoded and decoded from JSON. With this in mind, it’s fairly simple to make a class that manages loading and updating a list of HabitEntity
stored in UserDefaults, you just have to be mindful of a couple of things:
- You must add the AppGroup capability to both your main app and widget. This is to allow sharing UserDefaults values.
- Sharing UserDefaults requires a custom suite, since
UserDefaults.standard
uses the suite corresponding to the target’s bundle identifier.
Lets deal with the custom UserDefaults suite. Make a file called UserDefaults-shared.swift
or something similar and paste the following:
|
|
Now any time you want to access the shared UserDefaults you can use UserDefaults.shared
in place of .standard
.
At this point, we need a way to save and load the list of HabitEntity
. I decided to go with a singleton struct called HabitEntityData
:
|
|
Now we can go back to the HabitQuery
from before and fill it in with our new HabitEntityData
methods:
|
|
I defined a function for updating the HabitEntity
list, but we need an appropriate place to call this so that the user always has an up-to-date list of their habits when selecting one in the widget configuration. To do that, I decided to observe changes in the screenPhase
environment to make the HabitEntity
list update every time the app goes to the background (i.e. is exited):
|
|
Finally, all that’s needed is a computed property in the widget entry view so we can find the Habit
object with the corresponding id
from the SwiftData query:
|
|
Conclusion
This is a pretty hacky solution. Considering that both SwiftData and WidgetKit are so new, I expected some integration that would make this slightly easier, but at least it doesn’t require any gnarly Objective-C code.
I hope this helped someone going through the same issue!