I’m currently in the initial stages of discovery for my third digital product, a Todo List app called Atomic Todo that looks to create an easier to manage implementation of the 7 days, 6 weeks and 6 months planning system I use.
A two key features of that 7 days, 6 weeks, 6 month planning system is the ability to assign todos to different granularities of time (i.e. assign a task to a month or assign a task to a day) and to be able to move todos from one granularity to the other, so if a todo assigned to a day can be reassigned to a month which, I wasn’t 100% sure on how I was going to do using SwiftData (something I have no prior experience using).
In order to explore the problem and to better understand how SwiftData worked I decided to conduct a short spike during which I would define a set of models, create a set of test data and build a basic SwiftData and SwiftUI based app that would show me a set of todo lists with the items I’d expect within them.
The test data
There are four granularities of time that I’m aiming to include in the MVP of Atomic Todo:
- Year (e.g. 2023)
- Month (e.g. November 2023)
- Week (e.g. Week 46 of 2023)
- Day (e.g. 18th November 2023)
The higher granularities (i.e. Year) will contain todos assigned to the lower granularities within it (i.e. like an aggregation — Month level granularity shows todos assigned to the month, the weeks in the month and the days in the month) but sometimes you might not want to see these so I want a mechanism to control which granularities the todos are visible in.
Due to the way the higher granularities aggregate those lower granularity todos there’s a couple of edge cases that need to be tested around how weeks are handled as a week can span across month and year boundaries.
With these needs in mind I created a set of test data:
- Todo assigned to 2023
- Todo assigned to November 2023
- Todo assigned to 46th week of 2023
- Todo assigned to 13th November 2023
- Todo assigned to 48th week of 2023
- Expect to see Todo in w/c 27/11
- Expect to see Todo in November 2023 and December 2023
- Todo assigned to 52nd week of 2023
- Expect to see Todo in December 2023
- Don’t expect to see Todo in Jan 2024
- Todo assigned to 52nd week of 2024
- Expect to see Todo in December 2024
- Expect to see Todo in Jan 2024
- Expect to see Todo in 2024
- Expect to see Todo in 2023
- Todo assigned to 13th November 2023 set to only be shown in day list
- Todo assigned to 13th November 2023 set to be shown in day and week lists
- Todo assigned to 13th November 2023 set to be shown in day, week and month lists
- Todo assigned to 13th November 2023 set to be shown in all lists (re-use existing day example)
To conduct my spike I created a new SwiftData based app in Xcode, created a new Group for each approach to store the SwiftData model, the ViewModel and the ContentView. When I wanted to test an approach I then swapped the ContentView and modelContainer used within the App.
The UI for the ContentViews were pretty basic. Containing a NavigationStack , with a navigationTitle set to the current approach being tested and a Button that would call a method on the ViewModel to add the test data.
The ViewModel would then be responsible for creating the todo lists which would then query the database for todos that fell within it. The ViewModel would also have the method add the test data, which would create instances of the SwiftData model and add it to the database.
The SwiftData model would have a few shared properties across the different approaches such as a title and flags for if the todo is shown in different granularities of time:
I had to use separate flags for the visibility instead of an array of them as I could never get the SwiftData Predicate to be happy with checking if the todo’s visibility array contained the todo list’s granularity.
Approach 1 — Todos define period of time they span
The first approach I wanted to test was having a start and end date on the todo that would be set to the start and end of the period of time they were assigned to (i.e. if a todo was assigned to the month of November 2023 then the start date would be 2023–11–01T00:00:00.000Z and the end date would be 2023–12–01T00:00:00.00Z).
Querying todos in different granularities
My thinking with the approach was by having the dates set I could query the list of todos from the todo list ViewModel to pull in those todos that fall within that period of time’s boundaries, which would solve the issue with weeks spanning the boundaries of months and years.
For days and weeks granularity lists this would be as simple as checking the start date was on or after the start date of the period of time and the end date was on or before the end date of the period of time.
For years and months I’d be checking to see if the start date or end date was within the start and end dates of the period of time.
Using two dates for this approach also meant I could use the end date to sort the lists of todos.
Moving todos between granularities
When moving todos between granularities the todo would set the start and end dates to that of the granularity it would be moving to and updating the flags for the todo’s visibility in the different granularities.
Approach 2 — Use Date Components
The second approach I wanted to test was breaking the date into the separate year, month, week of the year and day components so that should a todo be assigned to a month only the year and month values are set.
Querying todos in different granularities
My thinking with this approach was that by setting these individual date components it would be easier to represent the granularity and when querying the todos as part of the ViewModel I could query that the appropriate properties matched the period the todo list represented.
For the day list I would match the year, month and day values and for week lists I would match the year and week.
For the month and year lists things got a little more complicated as those lists would need to see which weeks overlapped the start of the period to pull in todos assigned to those weeks.
There’s a downside to the date components approach in that sorting becomes a lot more involved, having to define the order of components to sort on which I could never get working well because where at the year and month granularity some values are nil this led to the order being messed up.
Moving todos between granularities
When moving todos between granularities the todo would update the date components that those of the granularity of the todo list it was moving to (i.e. moving from a year list to a day list would set the year, month, week and day components based on the day list’s date). Then the flags for visibility in different granularities would be updated.
Approach 3 — A hybrid of the two
The third approach I looked into was a bit of a hybrid of the two. The todo would have a date associated with it but I’d use a dynamic value on the model to get the date components so that I didn’t need to set these manually and when creating todos I could assign them to a date and define which granularity they were to be at in order to make things easier for myself.
My thinking was that this approach could use the same properties on the todo list ViewModel from approach 2 but this didn’t work because Predicate s in SwiftData can’t use dynamic values.
I thought about adding the date components as properties so they could be queried and use logic in the constructor of the model to set these but this would have just been me creating a more convoluted implementation of approach 2 so I stopped working on this.
What I’ve learned
During the spike I learned a lot about SwiftData and how it works and some of the traps I might fall into going forth which I’ll need to model my data to mitigate. I also learned a few useful APIs such as DateComponent , SortDescriptor.
In terms of the approaches to follow I think I’ll go with the start and end date approach as this makes it easier handle the week crossing the month/year boundary issue and it gives me a means to sort the todos nicely in the higher granularity todo lists.
When modelling data in the app I’ll need to make sure I don’t make use of enum s or dynamic model properties if I’m going to need to query them as this really seems to trip SwiftData up. This will likely lead to a lot of categorical information being stored as separate properties so I’m hoping that there’s not a limit on properties that I come across later.