In the landscape of Apple’s ecosystem, every hardware platform has its unique application scenarios and development experiences. Although the Apple Watch leads the smartwatch field with a massive user base, developers who truly dedicate themselves to it and achieve commercial success are few and far between.
As a long-time Apple Watch user and Swift blogger, I have often felt that watchOS development is somewhat “mysterious”—there are numerous details outside the documentation, yet deep, practical articles online are scarce. For this reason, I specially invited top independent watchOS developer Haozes to share his development insights.
This article is not a pile of theories, but rather a collection of pitfalls he actu…
In the landscape of Apple’s ecosystem, every hardware platform has its unique application scenarios and development experiences. Although the Apple Watch leads the smartwatch field with a massive user base, developers who truly dedicate themselves to it and achieve commercial success are few and far between.
As a long-time Apple Watch user and Swift blogger, I have often felt that watchOS development is somewhat “mysterious”—there are numerous details outside the documentation, yet deep, practical articles online are scarce. For this reason, I specially invited top independent watchOS developer Haozes to share his development insights.
This article is not a pile of theories, but rather a collection of pitfalls he actually stepped into and “exclusive secrets” summarized while maintaining products with millions of users. These first-hand experiences are extremely valuable for watchOS developers. I am very specific grateful to Haozes for selflessly sharing this with the community.
As the developer of YaoYao - Jump Rope, Tooboo - Hiking Trail Guides, and DunDun - Squat Counter, the fitness products I develop are primarily centered around the Apple Watch App form factor, rather than the iPhone App. Below are some scattered watchOS development experiences I’ve gathered over the years. I hope they are useful to you.
Synergy Issues between Watch App and iOS App
If the Watch App and iOS App need to coordinate, inconsistent system versions between the phone and watch, or inconsistent app versions on either side, can lead to many problems.
watchOS and iOS Version Mismatch
For instance, if the user’s iOS version is 26.1 and the watchOS version is 26.0, this can lead to a series of issues:
Watch App fails to install: New users may find that after installing the phone App, the Watch App fails to install automatically in sync.
Data missing in the “Fitness” App on the phone after a Watch HKWorkoutSession ends, and Activity Rings fail to fill: If it is a fitness App, heavily reliant on HealthKit storage, this problem is quite common.
Watch Connectivity communication failure: The watch and phone Apps are unable to communicate using WCSession.
Watch App and iOS App Version Mismatch
For instance, if the user’s iOS App is version 1.1 and the Watch App is version 1.0:
- Watch Connectivity communication might fail:
WCSessionrelated methods may become ineffective.
Mutual Launching of Watch App and iOS App
Watch App Launching iOS App
Using WCSession’s sendMessage on the Watch side to send a message to the iOS App can directly launch the iOS App.
If the iOS App is not running, it will be launched directly; otherwise, it will run in the background. This is an extremely powerful feature. For example, since watchOS does not support SFSpeechRecognizer, you can even send the audio stream to the phone, complete the speech-to-text on the phone, and indirectly achieve real-time speech transcription on the watch.
YaoYao and Tooboo also use this method. They send the text required for speech synthesis to the phone App, which then uses speech synthesis to play the voice audio on the phone. This way, users can listen to music on their phone while working out and hear voice alerts simultaneously.
iOS App Launching Watch App
You can use HKHealthStore’s startWatchApp(with:) to launch the Watch App.
class func launchEmptyWatchApp(onComplete: ((Bool, Error?) -> Void)? = nil) {
let workoutConfiguration = HKWorkoutConfiguration()
workoutConfiguration.activityType = .other
workoutConfiguration.locationType = .outdoor
let healthStore = HKHealthStore()
healthStore.startWatchApp(with: workoutConfiguration) { (success, error) in
print(">>> startWatchApp, launchEmptyWatchApp: \(success), error: \(String(describing: error))")
onComplete?(success, error)
}
}
On the Watch side:
func handle(_ workoutConfiguration: HKWorkoutConfiguration) {
// It is not strictly necessary to start an HKWorkoutSession here
}
Note: This method is only available if the App Category is Healthcare & Fitness; otherwise, it will likely be rejected during review.
Data Synchronization between Watch App and iOS App
| Method | Support | Description |
|---|---|---|
| App Groups | ❌ | Not supported since watchOS 2.0. |
| CloudKit | ✅ | Supports features similar to SwiftData and legacy CloudKit. |
| iCloud Document | ❌ | watchOS does not support iCloud Ubiquitous Containers. |
| iCloud key-value | ✅ | Supports NSUbiquitousKeyValueStore, and is recommended. |
| Watch Connectivity | ✅ | Using WCSession to send messages or files is a relatively reliable, stable, and instant method to sync data between both sides. Highly recommended.More info: Apple Developer Documentation |
Note:
- The
WCSessionmethodtransferFileis not supported in the simulator and requires a real device for testing.
Abnormal Restarts and Recovery
Changing iPhone App Configuration Causes Watch App to Restart Immediately
Modifying any privacy permissions in the iPhone App (such as Notification, Location, or Health privacy permissions) will directly cause the Watch App to be immediately terminated with SIGKILL.
In general situations, this isn’t a huge issue. However, if your Watch App uses Watch Connectivity to communicate with the phone in real-time, and the user happens to encounter a need for authorization while operating the phone App, the Watch App will be killed, which severely impacts the user experience.
Abnormal Restart and Recovery of Workout Session Watch Apps
For Apps using HKWorkoutSession, if they crash abnormally during a workout, you can restart the workout session via the WKExtensionDelegate’s handleActiveWorkoutRecovery method, as per Apple’s documentation.
The handleActiveWorkoutRecovery method is not called when the watch is rebooted. For apps like Tooboo, a user might use it until the battery dies. After charging and turning the watch back on, there is no guarantee this method will be called.
1.
It is recommended to check HKHealthStore().recoverActiveWorkoutSession in applicationDidFinishLaunching.
1.
recoverActiveWorkoutSession can only recover general data defined by HKWorkoutSession (time, distance, calories, etc.). The App needs to backup and restore its own custom defined metrics.
Memory Leak Issues
Whether it is an iOS or watchOS App, when closed, it is often just Frozen rather than truly Restarted. This makes memory leak issues very insidious. It may accumulate over many uses before finally causing the App to be killed by the Watchdog due to excessive memory usage.
Nested TabView on watchOS Causes Memory Leaks!
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
TabView {
Text("Page V1")
Text("Page V1")
}.tabViewStyle(.verticalPage)
Text("Page H2")
}
}
}
You might need a similar layout architecture in your Watch App, but currently, this causes memory leaks. You need to avoid nesting TabView.
Battery Optimization
Apps using HKWorkoutSession gain a privilege that other Apps generally don’t have: running in the background. At the same time, the App bears a greater responsibility. If the UI has high-frequency animations or even ordinary data-driven UI refreshes running for a long time, it will consume a large amount of power.
Main Optimization Strategies
1. Reduce refresh when isLuminanceReduced is true
When users use the watch, their wrist is down most of the time; they can’t keep their arm raised forever. At this time, the system will dim the screen, meaning the isLuminanceReduced value becomes true. High-frequency refreshes should only occur when the wrist is raised; otherwise, refresh rates should be minimized as much as possible.
2. Reduce refresh when App is not in the foreground
Listen for NotificationCenter.default.publisher(for: WKApplication.willResignActiveNotification) and reduce refreshes when the App is not in the Active state.
Tooboo’s Performance Optimization
In Tooboo’s hiking scenario, users can navigate continuously for 12 hours (with GPS and heart rate sensors always working), and up to 16 hours in Low Power Mode (tested on Apple Watch Ultra 2).
The Tooboo Watch App primarily has 3 threads: Map rendering, the UI main thread, and the Navigation algorithm.
Every GPS refresh triggers the navigation algorithm to judge whether the user has deviated from the route or needs to turn, but the map doesn’t necessarily render at this time, nor are map tiles downloaded. Map downloading and rendering only happen when the user raises their wrist. When in the background or when luminance is reduced, expensive UI operations are very infrequent.
For UI rendering, Tooboo does not use SwiftUI’s default data-driven rendering method. During a workout, many metrics (heart rate, calories, distance, speed, location, etc.) change, and any change in a metric would cause a UI redraw, occurring several times per second.
Tooboo uses TimelineSchedule for timed refreshes. If the wrist is raised and the screen is bright, the Schedule frequency is increased to 1Hz; otherwise, it is lowered to refresh once every 10-20 seconds. This significantly reduces CPU consumption.
public struct MetricsTimelineSchedule: TimelineSchedule {
var startDate: Date
var isPaused: Bool
var lowInterval: Double
public init(from startDate: Date, isPaused: Bool, lowInterval: Double? = nil) {
self.startDate = startDate
self.isPaused = isPaused
self.lowInterval = lowInterval ?? Double(AppSetting.shared.lowRefreshInterval)
}
public func entries(from startDate: Date, mode: TimelineScheduleMode) -> AnyIterator<Date> {
var baseSchedule = PeriodicTimelineSchedule(from: self.startDate, by: (mode == .lowFrequency ? self.lowInterval : 1.0))
.entries(from: startDate, mode: mode)
return AnyIterator<Date> {
guard !isPaused else { return nil }
// print("MetricsTimelineSchedule next()")
return baseSchedule.next()
}
}
}
struct Metric: View {
@EnvironmentObject var viewModel: WorkoutViewModel
@Environment(\.isLuminanceReduced) var isLuminanceReduced
var body: some View {
TimelineView(MetricsTimelineSchedule(from: self.viewModel.workoutData.startDate ?? Date(), isPaused: (self.viewModel.workoutState == .paused || isLuminanceReduced == true))) { timeline in
ZStack(alignment: .topLeading) {
VStack(alignment: .leading, spacing: 0) {
Spacer()
Text(viewModel.elapsedSec.shortFormatted)
.foregroundStyle(Color.yellow)
// other metric data
Spacer()
}
.frame(maxWidth: .infinity, alignment: .leading)
.ignoresSafeArea(edges: .bottom)
.scenePadding()
}
}
}
}
For example, here the App refreshes at a low frequency when the workout is paused or isLuminanceReduced is true.
Summary
Apple has supported using SwiftUI to develop Watch App interfaces since watchOS 6. There is no longer any reason to use the previous UIKit-like WatchKit method for development. If you are building a new product, it is recommended to support from watchOS 9+.
Compared to debugging iOS Apps, debugging watchOS Apps is relatively difficult. Additionally, Crash reports collected via Xcode > Window > Organizer > Crash are about 1/10 to 1/20 of the logs received for iOS. For Watch Apps, it is best to use local logging to record issues, and then use WCSession to send the logs to the phone for collection.
Regarding product deployment, since a large number of users are not familiar with how to install Watch Apps, and inconsistent system versions can lead to installation failures, products of this category often face difficulties right at the first step. It is very necessary to make it easy for users to contact the developer and provide support.