Files

6.5 KiB

SwimBuddy

Swimming workout tracker with Apple Watch and WearOS companion apps. Expo 57 + React Native 0.86 + NativeWind v5 + Drizzle ORM + Swift Watch target + Kotlin WearOS target.

Commands

npx expo start              # Dev server
npx expo run:ios            # Build & run on iOS
npx expo prebuild -p ios    # Regenerate ios/ dir (safe — targets/ persists)
npx expo prebuild -p ios && node scripts/add-sentry-watch.js  # Prebuild + add SentrySPM to watch target
npx drizzle-kit generate    # Regenerate SQL migrations after schema changes

WearOS:

./scripts/build-wearos.sh debug    # Build debug APK (reads version from app.json)
./scripts/build-wearos.sh release  # Build release APK (reads version from app.json)
./scripts/generate-wearos-icon.sh assets/icon.png  # Generate launcher icons

No npm test or lint scripts exist yet. No babel.config.js — NativeWind v5 uses CSS-first config.

Project Structure

src/
  features/<name>/screens/    # Screen components (registered in navigators)
  features/<name>/components/ # Feature-specific UI
  features/<name>/hooks/      # Feature-specific hooks
  components/                 # Shared UI primitives (flat)
  hooks/                      # Shared hooks
  navigation/                 # React Navigation (RootNav, TabNav, feature stacks)
  db/                         # Drizzle schema, client, relations, operations
  providers/                  # DatabaseProvider, SettingsProvider (MMKV)
  utils/                      # formatTime, calculateSwolf, uuid, etc.
  constants/                  # strokes, poolLengths, colors
  types/                      # workout.ts, common.ts
  modules/watch-connectivity/ # Expo native module bridging WCSession (Apple Watch)
  modules/wear-connectivity/  # Expo native module bridging Wear Data Layer (WearOS)
targets/watch/                # Apple Watch app (Swift/SwiftUI) — survives prebuild
targets/wearos/               # WearOS app (Kotlin/Compose) — survives prebuild

Key Conventions

  • Path aliases: @/, @features/, @components/, @db/, etc. (tsconfig.json)
  • Feature-based folder structure with type-based shared layer
  • NativeWind v5: className prop, no babel config, CSS-first. Use className not StyleSheet
  • className types: nativewind-env.d.ts at root — don't delete
  • lightningcss pinned to 1.30.1 in package.json overrides — required by NativeWind v5

Watch Target (Apple Watch)

  • Lives in targets/watch/, NOT in ios/ — survives expo prebuild --clean
  • Config: targets/watch/expo-target.config.js
  • Build: open ios/SwimBuddy.xcworkspace in Xcode, select Watch scheme
  • Uses App Groups (group.nl.guido-it.swimbuddy) for data sharing
  • WCSession for real-time messaging, transferUserInfo for reliable offline sync
  • HealthKit for swim workout tracking (indoor swimming)
  • Swift types StoredWorkout, StoredSet, CompletedLap in WorkoutManager.swift
  • SentrySPM: @bacons/apple-targets doesn't support swiftPackages yet. Run node scripts/add-sentry-watch.js after expo prebuild to add it automatically. For EAS builds, this runs automatically via the eas-build-post-install hook in package.json.

WearOS Target

  • Lives in targets/wearos/ — survives expo prebuild --clean
  • Kotlin + Jetpack Compose with Wear OS Compose libraries
  • Build: ./scripts/build-wearos.sh or open targets/wearos/ in Android Studio
  • Uses Wear Data Layer API for phone↔watch communication
  • Health Services Client for swim workout tracking
  • Namespace: nl.guidoit.swimbuddy

Database

  • expo-sqlite + Drizzle ORM (SQLite dialect, expo driver)
  • Schema: src/db/schema.ts — tables: templates, workouts, workoutSessions, sets, laps (laps references workoutSessions, not workouts)
  • Relations: src/db/relations.ts
  • CRUD: src/db/operations.ts
  • Client: src/db/client.ts — exports db and DATABASE_NAME
  • Migrations in drizzle/ — generated by npx drizzle-kit generate

WCSession Gotchas (Critical)

These are hard-won bugs that will waste hours if rediscovered:

  • NSNull crashes sendMessage: JS null becomes NSNull in ObjC, causing WCErrorCodePayloadUnsupportedTypes (Code 7010). Always strip null/undefined values before sending via cleanForWCSession().
  • Two delegate methods conflict: Having both session(_:didReceiveMessage:) AND session(_:didReceiveMessage:replyHandler:) on the same delegate causes iOS to deliver neither. Watch uses didReceiveMessage (no replyHandler); phone uses didReceiveMessage:replyHandler: for bidirectional sync.
  • isReachable is unreliable: Only true when both apps are foreground. Check session.activationState == .activated instead for connection readiness.
  • Phone sendMessage must use replyHandler: nil: Otherwise JS thread blocks waiting for a reply that watch never sends. Use fire-and-forget for workout plans; use transferUserInfo for data sync.
  • Watch suiteName vs UserDefaults.standard: Watch only needs local storage, so use UserDefaults.standard — the App Groups suite name is only needed for phone↔watch shared data.
  • Phone pendingEvents polling: Phone polls pollEvents() every 2s because Expo Modules' sendEvent API doesn't work on AppContext. Watch data arrives via didReceiveUserInfo delegate → stored in pendingEvents array.

Watch ↔ Phone Data Flow

  • Phone → Watch: sendMessage with workout plan (fire-and-forget, promise resolves immediately with ["status":"sent"])
  • Watch → Phone: transferUserInfo with completed workout data (reliable, works in background)
  • Phone receives: pollEvents() called from JS every 2s, saveWatchCompletedWorkout() writes to SQLite

UI Conventions

  • Swipe-to-delete on lists uses SwipeableRow component (react-native-gesture-handler)
  • BuilderScreen pool length is a 25m/50m toggle (not read-only from settings)
  • Templates and Stats features removed from navigation (DB schema preserved but UI deleted)
  • Ionicons used for bottom tab icons (@expo/vector-icons)

Gotchas

  • metro.config.js has .sql in sourceExts — required for Drizzle migration imports
  • expo prebuild clears and regenerates ios/ by default. Use --no-clean to preserve
  • Watch target compiles as separate Xcode target — not a RN module
  • react-native-css is a NativeWind v5 peer dependency — don't remove
  • @bacons/apple-targets plugin must be in app.json plugins array
  • WearOS uses same applicationId as iOS bundle identifier — don't change without updating both

Expo Docs

Expo SDK 57: https://docs.expo.dev/versions/v57.0.0/