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:
@@ -1,5 +1,7 @@
|
|||||||
**June 2026**
|
**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.
|
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.
|
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.
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ your own iCloud Drive.
|
|||||||
bidirectional: drive from either device — swipe ahead, finish a set, add one — and the
|
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)
|
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.
|
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
|
- **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.
|
Drive, synced across devices and visible in the Files app. iCloud is required.
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -78,6 +78,9 @@ targets:
|
|||||||
excludes:
|
excludes:
|
||||||
- "Resources/Info-*.plist"
|
- "Resources/Info-*.plist"
|
||||||
- "Resources/*.entitlements"
|
- "Resources/*.entitlements"
|
||||||
|
dependencies:
|
||||||
|
- target: Workouts Watch Widget
|
||||||
|
embed: true
|
||||||
postBuildScripts:
|
postBuildScripts:
|
||||||
- script: '"${SRCROOT}/Scripts/update_build_number.sh"'
|
- script: '"${SRCROOT}/Scripts/update_build_number.sh"'
|
||||||
name: Update Build Number
|
name: Update Build Number
|
||||||
@@ -98,3 +101,20 @@ targets:
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
|
||||||
TARGETED_DEVICE_FAMILY: "4"
|
TARGETED_DEVICE_FAMILY: "4"
|
||||||
DEVELOPMENT_ASSET_PATHS: "\"Workouts Watch App/Preview Content\""
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user