When people build iOS and watchOS apps together, they usually start fast and copy code between targets. It works for a week, then every bug fix has to be done twice. Two weeks later, it feels like you are carrying two apps that look similar but behave differently.
Shared logic should stay in one place while each platform keeps a thin presentation layer.
Step 1: Move business language into shared types
Start by defining a shared model that both platforms can understand. Keep it boring and explicit.
struct SessionSummary: Equatable {
let minutesToday: Int
let currentStreak: Int
let lastUpdatedAt: Date
}
This is your shared language. No UI code yet. No platform code yet.
Step 2: Hide data access behind a protocol
If both apps talk directly to storage or network code, you will duplicate error handling immediately.
protocol SessionStore {
func loadSummary() async throws -> SessionSummary
func logSession(minutes: Int) async throws
}
Now both iOS and watchOS can depend on the same interface.
Step 3: Build one view model for both platforms
Centralize behavior in one view model so both platforms stay consistent.
@MainActor
final class SessionViewModel: ObservableObject {
@Published private(set) var title = "Loading..."
@Published private(set) var isBusy = false
@Published private(set) var errorMessage: String?
private let store: SessionStore
init(store: SessionStore) {
self.store = store
}
func refresh() async {
isBusy = true
defer { isBusy = false }
do {
let summary = try await store.loadSummary()
title = "\(summary.minutesToday) min today | streak \(summary.currentStreak)"
errorMessage = nil
} catch {
errorMessage = "Could not load session summary."
}
}
}
At this point, your behavior is centralized. That is the hardest and most valuable part.