An API for dates and nothing else. No calendars, no timezones, no hours, minutes or seconds. Just dates!
Swift provides excellent date support with its Date, Calendar, TimeZone and related types. But there's a catch — they're all designed to work with a specific point in time. And that's not always how people think, and sometimes not even what we get from a server.
For example we never refer to a person's birthday as being a specific point in time. We don't say "Hey, it's Dave's birthday on the 29th of August at 2:14 am AEST". We simply say the 29th of August and everyone knows what we mean. But Apple's time APIs don't have that generalisation and that means extra work for developers to strip times, adjust time zones, and compare sanitised values. All of which is easy to get wrong.
This is where DayType steps in.
Basically DayType simplifies date handling through a Day type which represents of a 24-hour period independent of any timezone. There's no hours, minutes, seconds and milliseconds. Nor is there any time zones or even calendars to deal with. In other words, it does dates as people think about them.
- Installation
- Introducing Day
- Calendar generation
- Protocol conformance
- Property wrappers
- DayType and SwiftData
- References and thanks
- Future additions
DayType is a SPM package only. So install it as you would install any other package.
DayType's core type is Day which has all the necessary properties and functions to read, create and manipulate them. Most of which have been modelled off Apple's APIs for consistency and that sense of familiarity.
A Day has a number of convenience initialisers. Most of which are pretty self-explanatory and similar what you would be used to seeing with a Swift Date:
init() // Creates a `Day` based on the current time.
init(daysSince1970: DayInterval) // Creates a `Day` using the number of days since 1970.
init(timeIntervalSince1970: TimeInterval) // Creates a `Day` from a `TimeInterval`.
init(date: Date, usingCalendar calendar: Calendar = .current) // Creates a `Day` from a `Date` with an optional calendar.
init(_ dayComponents: DayComponents) throws // Creates a `Day` from a `DayComponents` value.
init(_ year: Int, _ month: Int, _ day: Int) throws // Creates a `Day` from individual year, month and day values. Short form.
init(year: Int, month: Int, day: Int) throws // Creates a `Day` from individual year, month and day values.Literally the number of days since Swift's base date of 00:00:00 UTC on 1 January 1970. Note this is the number of whole days, dropping any spare hours, minutes and seconds.
Note: This matches the number of days produced by this API code:
let fromDate = Calendar.current.startOfDay(for: Date(timeIntervalSince1970: 0)) let toDate = Calendar.current.startOfDay(for: Date()) let numberOfDays = Calendar.current.dateComponents([.day], from: fromDate, to: toDate).day!
Returns a DayComponents value containing the year, month, and dayOfMonth properties of the day. Computed on access using Hinnant's civil_from_days algorithm.
let components = try Day(2025, 8, 29).dayComponents
components.year // 2025
components.month // 8
components.dayOfMonth // 29Convenience property that returns a Day representing today's date. Equivalent to Day().
Returns the day of the week as a Weekday enum value.
try Day(2026, 3, 2).weekday // .monday
try Day(1970, 1, 1).weekday // .thursdayDay has a range of mathematical operators for adding and subtracting days from a Day:
// Adding days
var day = try Day(2000,1,1) + 5 // -> 2000-01-06
day += 5 // -> 2000-01-11
// Subtracting days
var day = try Day(2000,1,1) - 10 // -> 1999-12-21
day -= 5 // -> 1999-12-16
// Obtaining a duration in days
try Day(2000,1,10) - Day(2000,1,5) // -> 5 days duration.Using the passed Calendar and TimeZone this function converts a Day to a Swift Date in a specific timezone with the time components set to 00:00 (midnight).
Adds any number of years, months or days to a Day and returns a new Day. This is convenient for doing things like producing a sequence of dates for the same day on each month.
Uses Apple's Date.formatted(date:time:) function to format the day into a String using the formatting specified in Date.FormatStyle.DateStyle.
DayType can also generate a data structure specifically for building calendar UIs. It has a CalendarDays typealias which maps to a OrderedDictionary<Day, [DayComponents]> (Apple's swift-collections) where the key is the first Day of a single week in the calendar and the value is a an array of DayComponents values representing the days in that week. Starting from either Sunday or Monday.
The intent of this data structure is to allow easy mapping into a UI. Simply loop through the array values to create the Sunday to Saturday or Monday to Sunday cells.
Use calendarMonth(startingOn:) to generate the grid for the month containing a given day. It doesn't matter which day you give it, the function will work out the month to build out. Because it's focused on calendars the first and last week arrays may also contains some days from the prior or next month to ensure those week arrays have the full 7 days.
// Instance method
let month = try Day(2026, 3, 15).calendarMonth(startingOn: .monday)
// Static convenience
let month = try Day.calendarMonth(containing: Day(2026, 3, 15), startingOn: .sunday)
// Defaults to today and Sunday start
let month = Day.calendarMonth()The StartOfWeek enum controls which day begins each week row (.sunday or .monday).
The + and += operators merge calendar months together, automatically deduplicating overlapping boundary weeks:
let march = try Day(2026, 3, 15).calendarMonth(startingOn: .monday)
let april = try Day(2026, 4, 2).calendarMonth(startingOn: .monday)
// Merge two CalendarDays dictionaries
let twoMonths = march + april
// or …
var year = march
year += aprilDay is fully Codable.
When encoded or decoded it uses an Int representing the number of days since 1 January 1970. This value can also be accessed via the .daysSince1970 property.
Day is Equatable so days can be compared:
try Day(2001, 2, 3) == Day(2001, 2, 3) // trueDay is Comparable which enables the comparable operators: >, <, >= and <= for comparing days.
Day is also Hashable which allows it to be used as a dictionary key or in a set.
Day is Strideable which means you can use it in for loops as well as with the stride(from:to:by:) function. For example:
for day in try Day(2000, 1, 1) ... Day(2000, 1, 5) {
/// do something with the 1st, 2nd, 3rd, 4th and 5th.
}
for day in try Day(2000, 1, 1) ..< Day(2000, 1, 5) {
/// do something with the 1st, 2nd, 3rd and 4th.
}
for day in stride(from: try Day(2000, 1, 1), to: try Day(2000, 1, 5), by: 2) {
/// do something with the 1st and 3rd.
}DayType also provides a number of property wrappers which implement Codable, the intent being to allow easy conversions from all sorts of date formats that are often returned from servers.
All of the supplied property wrappers can read and write both Day and optional Day? properties and are grouped by the format of the data they expect to encode and decode.
These property wrappers are designed to encode and decode dates in the dd/MM/yyyy, MM/dd/yyyy and yyyy-MM-dd formats. For example:
struct MyData {
@DayString.DMY var dmyDay: Day // "30/04/2025"
@DayString.MDY var mdyDay: Day // "04/30/2025"
@DayString.YMD var ymdOptionalDay: Day? // "2025-04-30"
}Encodes and decodes days as epoch timestamps. For example:
struct MyData {
@Epoch.Seconds var optionalSeconds: Day? // 1746059246
@Epoch.Milliseconds var milliseconds: Day // 1746059246123
}Encodes and decodes standard ISO 8601 date strings. The only difference is that @ISO8601.SansTimezone is, as its name suggests, intended for reading strings written without a timezone value. For example:
struct MyData {
@ISO8601.Default var iso8601: Day // "2025-04-30T12:01:00Z"
@ISO8601.SansTimezone var optionalSansTimezone: Day? // "2025-04-30T12:01:00"
}By default all of DayType's property wrappers can handle decoding where the passed value is a null or if there is no value at all. For example:
struct MyData {
@DayString.DMY var dmy: Day?
}Will read both of these JSONs, setting dmy to nil:
// Null value.
{
"dmy": null
}
// Missing value.
{}When encoding, DayType will skip encoding nil values (producing {}), however some APIs require null values. To handle these APIs, DayType provides nested property wrappers which will write null values instead of skipping the keys altogether. For example:
struct MyData {
@DayString.DMY.Nullable var dmy: Day?
@Epoch.Seconds.Nullable var seconds: Day?
@ISO8601.Default.Nullable var iso8601: Day?
}Will write the following JSON when all the properties are nil:
{
"dmy": null,
"seconds": null,
"iso8601": null
}DayType works within SwiftData up to a point. That point being where you wish to use a Day in a SwiftData @Query. As an example you might do something like this:
@Model
class Holiday {
let startDatye: Day
let endEnd: Day
}
struct SomeView: View {
@Query(sort: \Holiday.startDate) private var holidays: [Holiday]
}This makes sense however when SwiftData writes the schema out to the database it will actually flatten the two Day fields into the Holiday Table as something like:
CREATE TABLE ZHOLIDAY (
Z_PK INTEGER PRIMARY KEY,
ZDAYSSINCE1970 INTEGER,
ZDAYSSINCE19701 INTEGER
)
The result of this is that when you run the @Query(…) it fails, claiming it's unable to resolve the startDate key path.
This is due to the way SwiftData works. When flattening it uses mirrors and raw types, reaching into each Day to get the name of the internal property holding the number of days since 1970.
There is no way (currently) around this. SwiftData has no facility to specifically name the database field and due to it's use or mirrors there's no swift trickery we can use to make it work.
Instead what we have to do is explicity use Day's daysSince1970 property in the query like this:
struct SomeView: View {
@Query(sort: \Holiday.startDate.daysSince1970) private var holidays: [Holiday]
}- Can't thank Howard Hinnant enough. Using his math instead of Apple's APIs produced a significant speed boost when converting to and from years, months and days.
Obviously there are a large number of useful functions that can be added to this API, many of which could come from various other calculations on the Hinnant date algorithms page. However I plan to add these as it becomes clear they will provide a useful addition rather than reimplementing a large number of functions that may not be needed.
Please feel free to drop a request for anything you'd like added.
