Add an Apple Watch face complication that opens the app

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.
This commit is contained in:
2026-06-20 22:15:26 -04:00
parent 8f69497b24
commit 192aa6f95a
5 changed files with 135 additions and 0 deletions
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Workouts</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>
@@ -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<LauncherEntry>) -> 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()
}
}