Skip to content

arthurkahwa/whichweek

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Ā 

History

5 Commits
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 
Ā 

Repository files navigation

WhichWeek — ISO Week Number

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.

Download on the App Store

Swift iOS watchOS macOS WidgetKit License Dependencies


Available on the App Store

Category: Utilities Ā |Ā  Price: Free Ā |Ā  Min OS: iOS 18.0

Download on the App Store


Screenshots

Week View
Week View
Days View
Days View
Week View (Dark)
Week View (Dark)
Days View (Dark)
Days View (Dark)

Features

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

Multi-Platform Targets

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
Loading

Architecture

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
Loading

State Management

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
Loading

Class Diagram

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
Loading

Tech Stack

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

Highlights

  • 5 deployment targets from one Swift codebase — iOS, watchOS, macOS, widgets, Live Activities
  • Orientation-adaptive layouts — three complete SwiftUI hierarchies selected by LayoutMode enum
  • Automatic week rollover — Task.sleep fires precisely at the next Monday midnight
  • Digital Crown UX — smooth week navigation with .digitalCrownRotation and haptic feedback
  • Dual binding strategy — Watch Crown value and DateManager stay in sync via two .onChange modifiers
  • Pure Foundation data source — WatchComplicationDataSource has zero WidgetKit imports, fully unit-testable
  • Static DateFormatter properties — 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

Testing

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.


Requirements

Minimum
iOS 18.0+
watchOS 11.0+
macOS 15.0+
Xcode 16.0+
Swift 6.0

Getting Started

git clone https://github.com/arthurkahwa/whichweek.git
cd whichweek
open WhichWeek.xcodeproj

Select a target (iPhone, Apple Watch, or Mac) and run.


License

This project is licensed under the MIT License.


Built with SwiftUI Ā· 100% Apple Frameworks Ā· Zero Dependencies

Download on the App Store

About

WhichWeek - A multi-platform ISO week number app for iOS, watchOS, and macOS with widgets, complications, and menu bar integration

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors