Building a Reliable Multi-Device Fitness App in SwiftUI, Part 1
HealthKit Import, iCloud Sync, Widgets, and Apple Watch Coordination
Building a Reliable Multi-Device Fitness App in SwiftUI, Part 1
HealthKit Import, iCloud Sync, Widgets, and Apple Watch Coordination
Photo by Karl Pawlowicz on Unsplash
Overview
Modern fitness apps rarely live on a single device. A user might log a workout on their Apple Watch, check progress through a widget, and review insights on their iPhone later in the day. If these surfaces are not consistent, the entire experience begins to break down.
This release focused on evolving the app from a local-only tracker into a multi-device, multi-surface system that remains coherent across iPhone, widgets, and Apple Watch. The goal was not to add more features, but to make existing ones reliable regardless of where the interaction happens.
The key objectives were practical:
- import completed workouts from Apple Health
- maintain streak continuity across devices
- keep widgets and watch views aligned with the app state
- handle sync failures without redesigning the UI
- extract complex logic into testable units
The result is an architecture that separates domain data, integration services, decision logic, and presentation clearly.
Photo by Luiz Felipe on Unsplash
The Core Problem
A local-only fitness app fails in real-world usage.
Users do not operate within a single controlled flow. They complete workouts in multiple places:
- directly inside the app
- using Apple Watch workouts
- through Apple Health imports
- from third-party apps writing to HealthKit
If the app only trusts its own logging mechanism, inconsistencies begin to appear quickly. Streaks become inaccurate. Widgets drift. A new device starts without context.
The system therefore needed to answer four core questions:
- How should completed workouts be imported and interpreted?
- How should critical data be synchronized across devices?
- How do widgets and Apple Watch stay consistent without becoming sources of truth?
- How are failure states surfaced clearly to the user?
Architectural Shape
The system was restructured into four layers, each with a clear responsibility.
Domain Layer
SwiftData models act as the source of truth for app-owned data:
- Workout
- WorkoutLog
- WorkoutSchedule
- WeightLog
- WorkoutExercise
These models represent what the app owns and controls.
Integration Layer
All system frameworks and cross-device communication are isolated into services:
- HealthKitManager
- CloudSyncStatusCenter
- WidgetSharedStore
- PhoneWatchSessionManager
- WatchConnectivityManager
This separation prevents framework logic from leaking into UI or business logic.
Decision Logic Layer
The most complex part of the system is the interpretation of imported workouts. This was extracted into a pure helper:
- HealthWorkoutSyncLogic
This layer contains rules, not UI.
Presentation Layer
SwiftUI views render already-shaped state:
- HomeView
- SettingsView
- Widget views
- Watch ContentView
Views do not decide sync behavior. They display it.
HealthKit Workout Import
Photo by Karla Arróniz on Unsplash
Reading HealthKit data is simple. Interpreting it correctly is not.
A HealthKit workout might:
- match a scheduled workout
- match an existing template
- represent a valid unscheduled session
- be a duplicate from another source
- be a workout already written by the app
To manage this, the system separates data access from interpretation.
HealthKit Manager
The manager handles permissions and data retrieval:
private var readTypes: Set<HKObjectType> {
let types: [HKObjectType?] = [
HKObjectType.workoutType(),
HKObjectType.quantityType(forIdentifier: .stepCount),
HKObjectType.quantityType(forIdentifier: .heartRate),
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned),
HKObjectType.quantityType(forIdentifier: .bodyMass)
]
return Set(types.compactMap { $0 })
}
It then maps raw data into a transport model:
HealthWorkoutSample(
uuid: workout.uuid,
startDate: workout.startDate,
endDate: workout.endDate,
activityType: workout.workoutActivityType,
sourceBundleIdentifier: workout.sourceRevision.source.bundleIdentifier
)
This avoids passing HKWorkout objects throughout the app.
Extracting Sync Logic
The most failure-prone logic was isolated into a pure helper.
Matching scheduled workouts
nonisolated static func matchingSchedule(
for workout: HealthWorkoutSample,
schedules: [WorkoutSchedule],
existingLogs: [WorkoutLog],
reservedScheduleIDs: Set<UUID>
) -> WorkoutSchedule? {
schedules
.filter { schedule in
!reservedScheduleIDs.contains(schedule.id)
&& Calendar.current.isDate(schedule.scheduledFor, inSameDayAs: workout.endDate)
}
.min { lhs, rhs in
abs(lhs.scheduledFor.timeIntervalSince(workout.endDate))
< abs(rhs.scheduledFor.timeIntervalSince(workout.endDate))
}
}
This ensures matching remains conservative and avoids duplicate completion.
Matching existing workouts
nonisolated static func matchingExistingWorkout(
for healthWorkout: HealthWorkoutSample,
availableWorkouts: [Workout]
) -> Workout? {
availableWorkouts.first {
$0.title.lowercased().contains(healthWorkout.activityType.name)
}
}
If nothing matches, a fallback custom workout is created instead of dropping the data.
Dedupe Logic
Multiple apps may write near-identical workouts. Deduplication ensures accuracy.
Key checks:
- same day
- similar start and end times
- similar duration
- matching activity type
nonisolated static func isPotentialDuplicate(
_ lhs: HealthWorkoutSample,
_ rhs: HealthWorkoutSample
) -> Bool {
let startDelta = abs(lhs.startDate.timeIntervalSince(rhs.startDate))
let endDelta = abs(lhs.endDate.timeIntervalSince(rhs.endDate))
return startDelta < 600 && endDelta < 600
}
A source scoring system prioritizes higher quality data, such as Apple Watch records.
Cross-Device Sync
No single sync mechanism was sufficient.
CloudKit for structured data
Used for:
- WorkoutLog
- WorkoutSchedule
- WeightLog
let cloudConfiguration = ModelConfiguration(
"Cloud",
cloudKitDatabase: .automatic
)
iCloud Key-Value Store for preferences
Used for:
- theme selection
- goals
- onboarding state
let store = NSUbiquitousKeyValueStore.default
This allows lightweight sync without database overhead.
Widget Architecture

Widgets do not replicate business logic.
Instead, the app writes a projected snapshot:
- compact
- precomputed
- theme-aware
Widgets simply render this shared state.
This avoids:
- duplicated logic
- drift between app and widget
- heavy queries inside extensions
Apple Watch Coordination
The watch acts as a request layer, not a source of truth.
Flow:
- Watch sends action
- Phone processes real mutation
- Updated state syncs back
let payload = ["watchAction": "markWorkout"]
This avoids inconsistent local writes on the watch.
Failure-State Handling
Failures are treated as part of the product, not edge cases.
iCloud states:
- available
- no account
- restricted
- unavailable
let status = try await CKContainer.default().accountStatus()
Watch states:
- unreachable iPhone
- queued actions
- sync failures
These are surfaced through existing UI instead of new flows.
Testing Strategy
The most fragile logic was extracted and tested first.
Example test:
@Test
func deduplicatedWorkoutsDoesNotMergeDifferentTypes() {
let result = HealthWorkoutSyncLogic.deduplicatedWorkouts(...)
#expect(result.count == 2)
}
This creates a safety net around sync behavior.
Release Outcomes
From a product perspective:
- workouts from any source now count
- streaks persist across devices
- widgets stay visually and logically aligned
- watch behaves as a true companion
From an engineering perspective:
- clearer source of truth
- isolated integration services
- testable decision logic
Lessons Learned
- Reading system data is easy. Interpreting it is not.
- One sync tool is not enough.
- Widgets and watch should not own logic.
- Failure states must be visible.
- Extract logic before testing it.
Closing
This release was less about adding features and more about building a reliable system boundary around fitness data.
- HealthKit as an external source
- CloudKit as continuity infrastructure
- widgets and watch as synchronized surfaces
- decision logic as testable code
That foundation is what makes future iterations safer and more predictable.
If you’re curious to experience how these changes feel in practice, Shansan Fit is now available on the App Store.
https://apps.apple.com/in/app/shansan-fit/id6759307775
I would genuinely value your feedback, not just on the features, but on how the design feels, how the interactions flow, and whether the experience supports your consistency in a meaningful way
메타데이터
- post_id
- 1473c7c8e0ca
- slug
- building-a-reliable-multi-device-fitness-app-in-swiftui-part-1-1473c7c8e0ca
- url
- https://medium.com/@amoghjs7/building-a-reliable-multi-device-fitness-app-in-swiftui-part-1-1473c7c8e0ca
- canonical_url
- https://medium.com/@amoghjs7/building-a-reliable-multi-device-fitness-app-in-swiftui-part-1-1473c7c8e0ca
- author_url
- https://medium.com/@amoghjs7
- status
- ok
- fetched_at
- 2026-06-17 08:20:12