This document is the authoritative reference for AI assistants integrating DayType. It covers every public type, property, method, and key behaviour. Read this before writing any code that imports
DayType,DayTypeMacros, orDayTypeUI.
Day represents a calendar date as a 24-hour period, independent of any timezone or time of day. It is not a point in time — it is the generalisation that people mean when they say "the 29th of August". No hours, minutes, seconds, or timezone complexity.
Internally stored as daysSince1970: Int (whole days since 1 Jan 1970 UTC). All date math uses Hinnant algorithms for performance — Foundation's Calendar is used only when converting to/from Date.
| Product | Import | What it contains |
|---|---|---|
DayType |
import DayType |
Core Day type, DayComponents, Weekday, CalendarDay, CalendarDays, DayError |
DayTypeMacros |
import DayTypeMacros |
Property wrapper macros (usually used indirectly via the wrappers below) |
DayTypeUI |
import DayTypeUI |
SwiftUI components: CalendarGrid, CalendarPicker |
import DayType
// Create days
let today = Day() // today
let today = Day.today // same thing
let birthday = try Day(1990, 8, 29) // 29 Aug 1990
let xmas = try Day(year: 2026, month: 12, day: 25)
// Arithmetic
let nextWeek = today + 7
let yesterday = today - 1
let nights = checkOut - checkIn // Int: number of days between
// Components
let comps = birthday.dayComponents // DayComponents(year: 1990, month: 8, dayOfMonth: 29)
let weekday = birthday.weekday // Weekday.wednesday
// Convert back to Foundation
let date = birthday.date() // Date at midnight in .current calendar
let formatted = birthday.formatted() // e.g. "Aug 29, 1990"public struct Day: Codable, Equatable, Comparable, Hashable, Strideablepublic let daysSince1970: IntThe sole stored value. Whole days since 1 Jan 1970 UTC. Use this when sorting Day values in SwiftData @Query — see the SwiftData note.
public static var today: Day // Day representing today
public static func isLeapYear(_ year: Int) -> Bool
public static func daysInMonth(_ month: Int, year: Int) -> Int
public static func daysInYear(_ year: Int) -> Int| Init | Throws | Notes |
|---|---|---|
init() |
no | Today's date |
init(daysSince1970: DayInterval) |
no | Raw days-since-epoch value |
init(timeIntervalSince1970: TimeInterval) |
no | Truncates to whole days |
init(date: Date, usingCalendar calendar: Calendar = .current) |
no | Extracts year/month/day from the Date using the given calendar |
init(_ dayComponents: DayComponents) throws |
yes | Validates ranges |
init(_ year: Int, _ month: Int, _ day: Int) throws |
yes | Short-form; validates ranges |
init(year: Int, month: Int, day: Int) throws |
yes | Named params; validates ranges |
Validation: month must be 1–12; day must be 1–daysInMonth. Throws DayError otherwise.
let d = try Day(2026, 13, 1) // throws DayError.monthOutOfRange(month: 13)
let d = try Day(2026, 2, 30) // throws DayError.dayOutOfRange(day: 30, month: 2, year: 2026)public var dayComponents: DayComponents // year, month, dayOfMonth — computed via Hinnant algorithm
public var weekday: Weekday // .sunday … .saturdayDay + Int -> Day // add days
Day - Int -> Day // subtract days
Day - Day -> Int // difference in days (can be negative)
Day += Int // mutating add
Day -= Int // mutating subtract// Add years, months, or days with month-end clamping
func day(byAdding component: Day.Component, value: Int) -> DayDay.Component cases: .year, .month, .day.
Month-end clamping: try Day(2026, 1, 31).day(byAdding: .month, value: 1) → 2026-02-28 (not an error).
// Convert to Foundation Date
func date(inCalendar calendar: Calendar = .current, timeZone: TimeZone? = nil) -> Date
// Format as a localised date string using a fixed preset
func formatted(_ day: Date.FormatStyle.DateStyle = .abbreviated) -> String
// Format using a custom, component-based Date.FormatStyle — for shapes the presets
// above can't express, e.g. day + month with no year: `.dateTime.day().month(.abbreviated)`
func formatted(_ style: Date.FormatStyle) -> String// Instance method — month containing this day
func calendarMonth(startingOn startOfWeek: StartOfWeek = .sunday) -> CalendarDays
// Static convenience
static func calendarMonth(containing day: Day = .today, startingOn startOfWeek: StartOfWeek = .sunday) -> CalendarDaysReturns a CalendarDays (see below) covering the full month. The first and last week rows may include padding days from adjacent months to complete 7-day rows.
Day is Strideable, so you can use it in ranges and stride:
for day in try Day(2026, 1, 1) ... Day(2026, 1, 5) { … } // closed range, 5 days
for day in try Day(2026, 1, 1) ..< Day(2026, 1, 5) { … } // half-open, 4 days
for day in stride(from: try Day(2026, 1, 1), to: try Day(2026, 1, 10), by: 2) { … }Encodes and decodes as a single Int (daysSince1970). Not an ISO string — use the property wrappers below when a specific wire format is needed.
public struct DayComponents: Equatable, HashableA simple snapshot of year, month, and day-of-month.
public let year: Int
public let month: Int
public let dayOfMonth: Int
public init(year: Int, month: Int, dayOfMonth: Int)
public func day() throws -> Day // converts back to Day; throws DayError if invalidObtain from Day.dayComponents. Do not compute Day from components in a tight loop — prefer Day arithmetic instead.
public enum DayError: Error, Equatable {
case monthOutOfRange(month: Int)
case dayOutOfRange(day: Int, month: Int, year: Int)
}Thrown by any Day initialiser that accepts year/month/day components. Conforms to Equatable for use in test assertions.
public enum Weekday: Int, CaseIterable, Sendable {
case sunday = 0
case monday = 1
case tuesday = 2
case wednesday = 3
case thursday = 4
case friday = 5
case saturday = 6
}Raw values match Hinnant's weekday_from_days output. Obtained via day.weekday.
public enum StartOfWeek: Sendable {
case sunday
case monday
}Controls which column is first in calendar grid rows. Passed to calendarMonth(startingOn:).
public struct CalendarDay: Hashable, Equatable, IdentifiableA single cell in a calendar grid, pairing a Day with its pre-computed DayComponents.
public let day: Day
public let dayComponents: DayComponents
public var id: Day { day }
public init(day: Day)public typealias CalendarDays = OrderedDictionary<Day, [CalendarDay]>The data structure for calendar UI. Keys are the week-start Day (Sunday or Monday, depending on startOfWeek). Values are 7-element [CalendarDay] arrays for that week, in order.
Key invariant: OrderedDictionary maintains insertion order, not sorted order. The merge operators below explicitly sort by key to guarantee chronological order after every merge.
CalendarDays + CalendarDays -> CalendarDays // merge, deduplicate boundary weeks, sort
CalendarDays += CalendarDays // mutating mergeWhen two dictionaries share a key (boundary weeks overlap between months), the left-hand side entry is kept.
let march = try Day(2026, 3, 15).calendarMonth(startingOn: .monday)
let april = try Day(2026, 4, 2).calendarMonth(startingOn: .monday)
let twoMonths = march + aprilpublic typealias DayInterval = IntAlias used in Day.init(daysSince1970:) for readability.
All wrappers support both Day and Day?. On decode, missing JSON keys and null values both map to nil for optional properties. On encode, nil values skip the key by default — use the .Nullable variant to write explicit null.
Decodes/encodes slash-separated date strings.
| Wrapper | Wire format |
|---|---|
@DayString.DMY |
"30/04/2025" |
@DayString.MDY |
"04/30/2025" |
@DayString.YMD |
"2025-04-30" |
struct MyData: Codable {
@DayString.DMY var arrival: Day
@DayString.YMD var departure: Day?
}Decodes/encodes Unix timestamps.
| Wrapper | Wire format |
|---|---|
@Epoch.Seconds |
1746059246 |
@Epoch.Milliseconds |
1746059246123 |
struct MyData: Codable {
@Epoch.Seconds var eventDate: Day
@Epoch.Milliseconds var updatedAt: Day?
}Decodes/encodes ISO 8601 datetime strings. Time component is stripped; only the date part is kept.
| Wrapper | Wire format |
|---|---|
@ISO8601.Default |
"2025-04-30T12:01:00Z" |
@ISO8601.SansTimezone |
"2025-04-30T12:01:00" |
struct MyData: Codable {
@ISO8601.Default var createdAt: Day
@ISO8601.SansTimezone var updatedAt: Day?
}Append .Nullable to any wrapper to encode nil as null instead of omitting the key:
struct MyData: Codable {
@DayString.DMY.Nullable var dmy: Day? // encodes as { "dmy": null }
@Epoch.Seconds.Nullable var seconds: Day? // encodes as { "seconds": null }
@ISO8601.Default.Nullable var iso: Day? // encodes as { "iso": null }
}Day works as a SwiftData @Model property. However, @Query sort descriptors using a Day key path fail because SwiftData flattens the struct and loses the property name. Use daysSince1970 explicitly:
// WRONG — SwiftData can't resolve the key path
@Query(sort: \Holiday.startDate) var holidays: [Holiday]
// CORRECT
@Query(sort: \Holiday.startDate.daysSince1970) var holidays: [Holiday]import DayTypeUI // also re-exports DayTypeNo internal padding on any component — the consuming view decides all padding and spacing.
public struct CalendarGrid: ViewAn infinitely-scrolling calendar grid. Renders weeks as 7-cell rows. Supports single-day and date-range selection. Pre-generates 10 years of history and 1 year forward; extends lazily as the user scrolls.
Width is always 7 * cellSize. Height is either visibleRows * cellSize (when visibleRows is set) or measured from available space.
Selection model (range mode):
- Tap any day outside the selection → starts a new selection from that day.
- Tap the start or end date → enters adjust mode for that endpoint.
- Tap a second time → commits the endpoint.
- On Mac Catalyst, pointer hover previews the moving endpoint live.
public enum Mode {
case single
case range
}Single-day init:
public init(
selection: Binding<Day>,
cellSize: CGFloat = 56,
visibleRows: Int? = nil,
scrollTo: Binding<Day?> = .constant(nil)
)Range init:
public init(
start: Binding<Day>,
end: Binding<Day>,
cellSize: CGFloat = 56,
visibleRows: Int? = nil,
scrollTo: Binding<Day?> = .constant(nil)
)| Parameter | Notes |
|---|---|
selection / start / end |
Two-way bindings. The grid writes the selected day(s) back immediately on each tap. |
cellSize |
Square size of each day cell in points. Default 56. |
visibleRows |
Fixed row count. When nil, the grid measures its container height and fills it. |
scrollTo |
Set to a Day to programmatically animate-scroll to that day. The grid resets the binding to nil after scrolling. |
Example:
@State private var selected = Day.today
@State private var scrollTarget: Day? = nil
CalendarGrid(selection: $selected, visibleRows: 6, scrollTo: $scrollTarget)
.padding()
// Programmatic scroll
Button("Go to today") { scrollTarget = .today }public struct CalendarPicker: ViewA higher-level wrapper around CalendarGrid that adds:
- An optional title label.
- A tappable date header (single mode: the selected date; range mode: "From [date] to [date]"). Tapping a date scrolls the grid back to it.
- A days/nights summary label below the header (range mode only).
Always renders CalendarGrid with visibleRows: 6.
Single-day init:
public init(title: String? = nil, selection: Binding<Day>)Range init:
public init(title: String? = nil, start: Binding<Day>, end: Binding<Day>)Example:
@State private var checkIn = Day.today
@State private var checkOut = Day.today + 7
CalendarPicker(title: "Select dates", start: $checkIn, end: $checkOut)
.padding()
.presentationDetents([.medium, .large])Dayis timezone-free. There are no timezone semantics stored on aDay. Conversion toDatevia.date(inCalendar:timeZone:)is the only place timezones enter — and that's the caller's responsibility.- Throwing inits validate eagerly. Pass invalid month/day values and you get a
DayErrorimmediately, not a corrupted value. day(byAdding:)clamps, never throws. Adding months or years to a day at month-end silently clamps (Jan 31 + 1 month = Feb 28). For.dayarithmetic, use the+/-operators instead — they're equivalent and clearer.CalendarDaysmust be sorted after merging. The+/+=operators do this automatically. If you build aCalendarDaysdictionary by hand (e.g. with direct dictionary subscript), call.sort { $0.key < $1.key }before use.CalendarGridwidth is fixed. The view always frames itself at7 * cellSize. If you need a specific width, setcellSizeaccordingly — don't constrain the grid externally.- Codable encodes as
Int, not a string. ADayround-tripped throughJSONEncoder/JSONDecoderuses itsdaysSince1970integer value. If your server sends a string or timestamp, use the property wrappers.