A multi-platform ISO 8601 week number app for iPhone, Apple Watch, Mac menu bar, widgets, and Live Activities.
Built entirely with Apple frameworks. Zero external dependencies.
![]() Week View |
![]() Days View |
![]() Week View (Dark) |
![]() Days View (Dark) |
| Feature | Description | |
|---|---|---|
| š | ISO Week Display | Current ISO 8601 week number with Monday--Sunday date range |
| š | Days View | Date range calculator with weeks and days breakdown |
| š± | Three Orientations | Portrait, landscape, and upside-down ā each a distinct SwiftUI layout |
| ā | Apple Watch | Digital Crown navigation with haptic feedback |
| š» | macOS Menu Bar | Always-available MenuBarExtra popover |
| š§© | Widgets | Home Screen and Lock Screen widgets (4 families) |
| ā” | Live Activities | Real-time week number on the Lock Screen and Dynamic Island |
| ā±ļø | Complications | Circular, rectangular, corner, and inline watch faces |
| š | Auto Rollover | Week updates automatically at Monday midnight via Task.sleep |
| šØ | Custom Themes | Indigo-to-cyan gradients with light and dark mode support |
| āæ | Accessibility | VoiceOver, Dynamic Type, adjustable actions, reduce motion |
| š | Privacy | No data collection, no network calls, fully offline |
graph TB
App["WhichWeek<br/>Shared Codebase"]
App --> iOS["iOS App"]
App --> Watch["watchOS App"]
App --> Mac["macOS App"]
App --> Widgets["WidgetKit"]
App --> Live["Live Activities"]
iOS --> Portrait["Portrait Layout"]
iOS --> Landscape["Landscape Layout"]
iOS --> UpsideDown["Upside-Down Layout"]
Watch --> Crown["Digital Crown<br/>Navigation"]
Watch --> Complications["Complications"]
Complications --> Circular["Circular"]
Complications --> Rectangular["Rectangular"]
Complications --> Corner["Corner"]
Complications --> Inline["Inline"]
Mac --> MenuBar["MenuBarExtra<br/>Popover"]
Widgets --> HomeScreen["Home Screen<br/>Widget"]
Widgets --> LockScreen["Lock Screen<br/>Widget"]
Widgets --> Control["Control<br/>Widget"]
Live --> DynamicIsland["Dynamic Island"]
Live --> LockScreenLive["Lock Screen<br/>Activity"]
style App fill:#4F46E5,color:#fff,stroke:#4338CA
style iOS fill:#06B6D4,color:#fff,stroke:#0891B2
style Watch fill:#06B6D4,color:#fff,stroke:#0891B2
style Mac fill:#06B6D4,color:#fff,stroke:#0891B2
style Widgets fill:#06B6D4,color:#fff,stroke:#0891B2
style Live fill:#06B6D4,color:#fff,stroke:#0891B2
WhichWeek follows an MVVM-Lite pattern with Environment-based state propagation. A single DateManager instance is injected at the app root and flows through the entire view hierarchy via SwiftUI's @Environment.
flowchart LR
DM["DateManager<br/>@Observable @MainActor<br/><i>Single Source of Truth</i>"]
DM -->|"@Environment"| WeekView["Week View<br/><small>3 orientation layouts</small>"]
DM -->|"@Environment"| DaysView["Days View<br/><small>Date range calculator</small>"]
DM -->|"@Environment"| WatchView["Watch View<br/><small>Digital Crown + haptics</small>"]
DM -->|"Timeline Provider"| WidgetView["Widget Views<br/><small>4 families</small>"]
DM -->|"Pure Foundation"| CompDS["WatchComplication<br/>DataSource<br/><small>Zero WidgetKit imports</small>"]
WeekView -->|"@Bindable"| DM
DaysView -->|"@Bindable"| DM
WatchView -->|".onChange sync"| DM
CompDS --> Complications["Complication<br/>Entry Views"]
subgraph Concurrency
Timer["Week Rollover<br/>Task.sleep ā Monday midnight"]
Orientation["Orientation Observer<br/>async notification sequence"]
end
Timer -->|"updates"| DM
Orientation -->|"updates"| WeekView
style DM fill:#4F46E5,color:#fff,stroke:#4338CA
style Concurrency fill:#F3F4F6,stroke:#D1D5DB,color:#374151
All view-facing properties are computed from a single stored property: selectedDate. There is no duplicated state. User interactions funnel back through DateManager methods, which update selectedDate, triggering SwiftUI to re-render.
flowchart TD
SD["selectedDate: Date<br/><i>Single stored property</i>"]
SD --> SW["selectedWeek<br/><small>computed get/set</small>"]
SD --> SY["selectedYear<br/><small>computed get/set</small>"]
SD --> WIY["weeksInYear<br/><small>52 or 53</small>"]
SD --> ILY["isLeapYear<br/><small>Bool</small>"]
SD --> MON["mondayOfSelectedWeek<br/><small>Date</small>"]
SD --> WDR["weekDateRange<br/><small>Mon ā Sun string</small>"]
SW --> Views["SwiftUI Views"]
SY --> Views
WIY --> Views
ILY --> Views
MON --> Views
WDR --> Views
Views -->|"user taps stepper"| SelectWeek["selectWeek(_ :)"]
Views -->|"user taps stepper"| SelectYear["selectYear(_ :)"]
Views -->|"user taps Today"| Reset["resetToCurrentWeek()"]
SelectWeek -->|"sets"| SD
SelectYear -->|"sets"| SD
Reset -->|"sets"| SD
style SD fill:#4F46E5,color:#fff,stroke:#4338CA
style Views fill:#06B6D4,color:#fff,stroke:#0891B2
classDiagram
class DateManager {
<<@Observable @MainActor>>
+selectedDate: Date
+selectedWeek: Int «computed»
+selectedYear: Int «computed»
+weeksInYear: Int «computed»
+isLeapYear: Bool «computed»
+mondayOfSelectedWeek: Date «computed»
+weekDateRange: String «computed»
+weekNumberText: String «computed»
+daysInRange: Int «computed»
+selectWeek(Int)
+selectYear(Int)
+resetToCurrentWeek()
+updateFromDate(Date)
-startWeekRolloverTimer()
-nextMondayMidnight() Date
}
class AppTheme {
<<struct>>
+backgroundGradient: LinearGradient
+accentColor: Color
+textColor: Color
+secondaryTextColor: Color
+init(colorScheme: ColorScheme)
}
class WatchComplicationDataSource {
<<Pure Foundation>>
+weekNumber(for: Date) Int
+yearForWeek(for: Date) Int
+weekDateRange(for: Date) String
+isLeapYear(for: Date) Bool
}
class WatchComplicationEntry {
<<TimelineEntry>>
+date: Date
+weekNumber: Int
+yearForWeek: Int
+weekDateRange: String
}
DateManager --> AppTheme : themed by
WatchComplicationDataSource --> WatchComplicationEntry : produces
| Layer | Framework | Purpose |
|---|---|---|
| UI | SwiftUI | Declarative views across all platforms |
| Data | Foundation | Calendar, Date, DateComponents, DateFormatter |
| Widgets | WidgetKit | Home Screen, Lock Screen, and Control widgets |
| Intents | AppIntents | Widget configuration and Shortcuts |
| Live Activities | ActivityKit | Lock Screen and Dynamic Island updates |
| Testing | Swift Testing | @Suite / @Test unit tests with @MainActor |
| UI Testing | XCTest | Snapshot tests, launch performance |
| Concurrency | Swift Concurrency | async/await, Task.sleep, async sequences |
- 5 deployment targets from one Swift codebase ā iOS, watchOS, macOS, widgets, Live Activities
- Orientation-adaptive layouts ā three complete SwiftUI hierarchies selected by
LayoutModeenum - Automatic week rollover ā
Task.sleepfires precisely at the next Monday midnight - Digital Crown UX ā smooth week navigation with
.digitalCrownRotationand haptic feedback - Dual binding strategy ā Watch Crown value and
DateManagerstay in sync via two.onChangemodifiers - Pure Foundation data source ā
WatchComplicationDataSourcehas zero WidgetKit imports, fully unit-testable - Static
DateFormatterproperties ā created once, reused across the app (performance) - Accessibility-first ā VoiceOver labels, hints, adjustable actions,
@ScaledMetric, reduce motion support - 40+ unit tests ā covering
DateManager,WatchComplicationDataSource, edge cases, and UI snapshots - Zero external dependencies ā 100% Apple frameworks
40+ tests across unit and UI test suites.
| Suite | Coverage |
|---|---|
| DateManager | Initialization, weeksInYear, year range, weekNumberText, isLeapYear, mondayOfSelectedWeek, weekDateRange, updateFromDate, resetToCurrentWeek, week clamping, daysInRange, nextMondayMidnight |
| WatchComplicationDataSource | 15+ tests for week number, year-for-week, date range, and leap year calculations across edge-case dates |
| UI Tests | App Store screenshot snapshots (Fastlane), launch performance measurement |
All unit tests run with @MainActor isolation to match DateManager's actor context.
| Minimum | |
|---|---|
| iOS | 18.0+ |
| watchOS | 11.0+ |
| macOS | 15.0+ |
| Xcode | 16.0+ |
| Swift | 6.0 |
git clone https://github.com/arthurkahwa/whichweek.git
cd whichweek
open WhichWeek.xcodeprojSelect a target (iPhone, Apple Watch, or Mac) and run.
This project is licensed under the MIT License.



