From 192aa6f95aa3237342cec0e62cb5157c5d2eac56 Mon Sep 17 00:00:00 2001 From: rzen Date: Sat, 20 Jun 2026 22:15:26 -0400 Subject: [PATCH] Add an Apple Watch face complication that opens the app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A static WidgetKit accessory widget (launcher only — no data sharing, App Group, or entitlements). Tapping any accessory widget opens its containing app, so this is enough to place a Workouts button on the watch face. Supports the circular, corner, inline, and rectangular accessory families. New 'Workouts Watch Widget' app-extension target embedded in the watch app via project.yml. --- CHANGELOG.md | 2 + README.md | 3 + .../Resources/Info-WatchWidget.plist | 31 ++++++++ .../WorkoutsWatchWidget.swift | 79 +++++++++++++++++++ project.yml | 20 +++++ 5 files changed, 135 insertions(+) create mode 100644 Workouts Watch Widget/Resources/Info-WatchWidget.plist create mode 100644 Workouts Watch Widget/WorkoutsWatchWidget.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f169a1..7f36529 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ **June 2026** +Add a Workouts complication to your Apple Watch face — tap it to open the app straight from any watch face. + Prop your iPhone up during an Apple Watch workout and it now runs the same live flow side by side — Ready → work/rest → Finish with running timers — and you can drive from either device: swipe ahead, finish a set, or add one on whichever is closer, and the other follows along. Automatic moves, like a rest timer running out, advance both devices on their own. Editing an exercise or split on iPhone now steps the Apple Watch out of that workout, showing it as "Editing on iPhone" until you're done — so the watch never keeps running an exercise whose plan you're changing. diff --git a/README.md b/README.md index c6aa4df..46f5212 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,9 @@ your own iCloud Drive. bidirectional: drive from either device — swipe ahead, finish a set, add one — and the other follows. Only *human* transitions are sent; automatic ones (a rest timer ending) advance both devices independently off shared start times, so they never fight. +- **Watch face complication** — a launcher complication you can place on any Apple + Watch face; tap it to open the app. Available in the circular, corner, inline, and + rectangular accessory slots. - **iCloud Drive sync** — your data lives as human-readable JSON in your iCloud Drive, synced across devices and visible in the Files app. iCloud is required. diff --git a/Workouts Watch Widget/Resources/Info-WatchWidget.plist b/Workouts Watch Widget/Resources/Info-WatchWidget.plist new file mode 100644 index 0000000..d50bcac --- /dev/null +++ b/Workouts Watch Widget/Resources/Info-WatchWidget.plist @@ -0,0 +1,31 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Workouts + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + ITSAppUsesNonExemptEncryption + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/Workouts Watch Widget/WorkoutsWatchWidget.swift b/Workouts Watch Widget/WorkoutsWatchWidget.swift new file mode 100644 index 0000000..001e987 --- /dev/null +++ b/Workouts Watch Widget/WorkoutsWatchWidget.swift @@ -0,0 +1,79 @@ +import SwiftUI +import WidgetKit + +// A launcher complication: a static button on the watch face that opens the +// Workouts app. It carries no data, so the timeline is a single entry that +// never refreshes. Tapping any accessory widget launches its containing app, +// so no deep link or App Group is needed. + +private struct LauncherEntry: TimelineEntry { + let date: Date +} + +private struct LauncherProvider: TimelineProvider { + func placeholder(in context: Context) -> LauncherEntry { + LauncherEntry(date: .now) + } + + func getSnapshot(in context: Context, completion: @escaping (LauncherEntry) -> Void) { + completion(LauncherEntry(date: .now)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + // Nothing ever changes — one entry, never reload. + completion(Timeline(entries: [LauncherEntry(date: .now)], policy: .never)) + } +} + +private struct LauncherView: View { + @Environment(\.widgetFamily) private var family + + private let glyph = "dumbbell.fill" + + var body: some View { + switch family { + case .accessoryInline: + Label("Workouts", systemImage: glyph) + case .accessoryRectangular: + Label("Workouts", systemImage: glyph) + .font(.headline) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + case .accessoryCorner: + Image(systemName: glyph) + .font(.title2) + .widgetLabel("Workouts") + default: // .accessoryCircular and any future families + ZStack { + AccessoryWidgetBackground() + Image(systemName: glyph) + .font(.title3) + } + } + } +} + +struct WorkoutsLauncherComplication: Widget { + private let kind = "WorkoutsLauncher" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: LauncherProvider()) { _ in + LauncherView() + .containerBackground(.clear, for: .widget) + } + .configurationDisplayName("Open Workouts") + .description("Tap to open the Workouts app.") + .supportedFamilies([ + .accessoryCircular, + .accessoryCorner, + .accessoryInline, + .accessoryRectangular, + ]) + } +} + +@main +struct WorkoutsWatchWidgetBundle: WidgetBundle { + var body: some Widget { + WorkoutsLauncherComplication() + } +} diff --git a/project.yml b/project.yml index 04ef7ff..199bbd7 100644 --- a/project.yml +++ b/project.yml @@ -78,6 +78,9 @@ targets: excludes: - "Resources/Info-*.plist" - "Resources/*.entitlements" + dependencies: + - target: Workouts Watch Widget + embed: true postBuildScripts: - script: '"${SRCROOT}/Scripts/update_build_number.sh"' name: Update Build Number @@ -98,3 +101,20 @@ targets: ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor TARGETED_DEVICE_FAMILY: "4" DEVELOPMENT_ASSET_PATHS: "\"Workouts Watch App/Preview Content\"" + + # ---- watchOS widget extension (a launcher complication for the watch face) -- + Workouts Watch Widget: + type: app-extension + platform: watchOS + sources: + - path: Workouts Watch Widget + excludes: + - "Resources/Info-*.plist" + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: dev.rzen.indie.Workouts.watchkitapp.widget + INFOPLIST_FILE: "Workouts Watch Widget/Resources/Info-WatchWidget.plist" + GENERATE_INFOPLIST_FILE: false + SWIFT_STRICT_CONCURRENCY: complete + WATCHOS_DEPLOYMENT_TARGET: "26.0" + TARGETED_DEVICE_FAMILY: "4"