WidgetData

The data the main app shares with its home-screen and lock-screen widgets. The app writes a JSON snapshot into an App Group container; the widget extension reads it.

  • Model: Shared/Sources/MinutaShared/Models/WidgetData.swift
  • Writer: Minuta/Sources/Services/WidgetDataService.swift
  • Widget extension: Minuta/Widgets/ (MinutaWidgets.swift, WidgetIntents.swift)

Build status

The widget extension is disabled in the build pending Apple Developer portal setup (App Group registration and the tools.minuta.app.widgets bundle ID). The target lines are commented out in Minuta/project.yml. The model and views compile and have SwiftUI previews, but widgets do not ship until the App Group capability is provisioned.

The model

WidgetData is a Codable, Sendable snapshot — a denormalized, widget-friendly view rather than the live records:

public struct WidgetData: Codable, Sendable {
    public var runningTimers: [WidgetTimer]
    public var todayRecordsCount: Int
    public var todayTotalDuration: TimeInterval
    public var recentTags: [WidgetTag]     // up to 4, most-recently-used
    public var lastUpdated: Date
}

WidgetTimer and WidgetTag are trimmed projections of TimeRecord and Tag carrying only what the widget renders (id, start time, tag name/color, comment). WidgetTimer.currentDuration computes elapsed time from startTime so the widget can tick without re-reading storage.

Shared constants live in WidgetDataConstants:

public static let filename = "widget-data.json"
public static let appGroupIdentifier = "group.tools.minuta.app"
public static let maxRecentTags = 4

App Group sharing

The main app and the widget extension cannot read each other’s sandboxed containers directly. They share through an App Group container resolved by identifier:

FileManager.default
    .containerURL(forSecurityApplicationGroupIdentifier: WidgetDataConstants.appGroupIdentifier)?
    .appendingPathComponent(WidgetDataConstants.filename)

This is independent of the user-chosen Automerge storage folder — widgets never read the .automerge files; they only read the snapshot the app publishes.

Writing the snapshot

WidgetDataService is an actor. The app calls updateWidgetData(runningTimers:todayRecords:tags:recentTagIds:) when relevant state changes. It builds WidgetTimers from the running records, sums today’s completed durations, takes up to maxRecentTags recent tags, encodes to ISO-8601 JSON, writes atomically to the App Group file, and asks WidgetKit to refresh:

let jsonData = try encoder.encode(data)
try jsonData.write(to: url, options: .atomic)
WidgetCenter.shared.reloadAllTimelines()

Reading in the widget

WidgetDataReader (in MinutaWidgets.swift) decodes the same file, returning an empty WidgetData() if the container or file is missing. MinutaTimelineProvider feeds it into the timeline, scheduling the next refresh every 60 seconds when a timer is running and every 15 minutes otherwise.

Widget families

The bundle ships two widgets:

  • MinutaHomeWidget (home screen): systemSmall, systemMedium, systemLarge — running timer with stop button, today summary, and quick-start tag buttons.
  • MinutaLockScreenWidget (lock screen, iOS): accessoryCircular, accessoryRectangular, accessoryInline — compact running-timer or today’s-total displays.

Interactive intents

Widget buttons are wired to App Intents in WidgetIntents.swift (StartTimerFromWidgetIntent, StopTimerFromWidgetIntent, StopAllTimersFromWidgetIntent). Because the widget process cannot mutate the app’s storage, each intent writes a PendingTimerRequest to pending-timer.json in the App Group container. The main app processes that request the next time it becomes active.

Related

  • TimeRecord — the source records that WidgetTimer projects from.