WellAlly Logo
WellAlly康心伴
Development

Building a React Native Steps & Sleep Tracker with HealthKit Integration

A practical implementation guide for creating a comprehensive steps and sleep tracking application using React Native and Apple HealthKit, with real-time data monitoring and personalized insights.

W
2026-03-08
10 min read

Key Takeaways

  • HealthKit Observer Pattern: Real-time step and sleep updates using observeSteps and observeSleep for live data monitoring
  • Sleep Stage Analysis: Detailed sleep stage breakdown (awake, REM, core, deep) for comprehensive sleep quality assessment
  • Step Count Optimization: Accurate step counting with pedestrian status detection and automatic workout session creation
  • Data Persistence Strategy: Efficient local caching using AsyncStorage with intelligent sync to reduce API calls
  • Production Best Practices: Proper error handling, permission management, and battery optimization for long-running health tracking

Sleep and step tracking are two of the most fundamental health metrics, providing crucial insights into overall wellness, activity levels, and sleep quality. Building a robust tracker that accurately captures and presents this data is essential for any health application.

This technical guide will walk you through creating a production-ready steps and sleep tracker using React Native and Apple HealthKit. We'll implement real-time data observation, detailed sleep stage analysis, step count optimization, and personalized insights—all while maintaining excellent performance and battery efficiency.

Prerequisites:

  • React Native 0.72+ with iOS deployment target 13.6+
  • CocoaPods installed and configured
  • Physical iOS device for testing (simulator has limited HealthKit support)
  • Basic understanding of React Native, TypeScript, and async/await patterns
  • Apple Developer Program membership for HealthKit entitlements

Architecture Overview

Our steps and sleep tracker will implement the following architecture:

Rendering diagram...
graph TB
    A[React Native UI Layer] --> B[Health Data Manager]
    B --> C[HealthKit Service]
    C --> D[iOS HealthKit Framework]
    D -->|Observer Updates| C
    C -->|Data Normalization| B
    B -->|Cached Data| E[AsyncStorage]
    B -->|Processed Data| F[Insights Engine]
    F -->|Analysis Results| A
    G[Background Task Scheduler] --> C
    style B fill:#ffd43b,stroke:#333,stroke-width:2px
    style F fill:#74c0fc,stroke:#333,stroke-width:2px

Key Components:

  1. Health Data Manager: Central service for data operations and caching
  2. HealthKit Service: Platform-specific bridge to iOS HealthKit
  3. Insights Engine: Analyzes data and generates personalized recommendations
  4. Background Scheduler: Manages periodic data refresh tasks

Project Setup

Step 1: Install Dependencies

code
npm install react-native-health @react-native-async-storage/async-storage
npm install --save-dev @types/react-native-health
cd ios && pod install
Code collapsed

Step 2: Configure iOS Entitlements

Add HealthKit capability to your Xcode project:

  1. Open ios/YourProject.xcworkspace in Xcode
  2. Select your project → Target → Signing & Capabilities
  3. Add "HealthKit" capability
  4. Add the following to your Info.plist:
code
<key>NSHealthShareUsageDescription</key>
<string>This app needs access to your health data to track your steps and sleep patterns.</string>
<key>NSHealthUpdateUsageDescription</key>
<string>This app needs to save your steps and sleep data to Apple Health.</string>
Code collapsed

Step 3: Create Type Definitions

code
// src/types/health.ts

export interface StepsData {
  count: number;
  distance: number; // meters
  floorsAscended: number;
  floorsDescended: number;
  currentPace?: number; // min/km
  currentCadence?: number; // steps/min
  startDate: Date;
  endDate: Date;
}

export interface SleepData {
  totalSleepTime: number; // milliseconds
  timeInBed: number; // milliseconds
  sleepStages: {
    awake: number; // milliseconds
    REM: number; // milliseconds
    core: number; // milliseconds
    deep: number; // milliseconds
    unsupported?: number; // milliseconds
  };
  sleepQuality: 'excellent' | 'good' | 'fair' | 'poor';
  sleepScore: number; // 0-100
  startDate: Date;
  endDate: Date;
}

export interface SleepAnalysis {
  date: Date;
  bedtime: Date;
  wakeTime: Date;
  duration: number; // hours
  efficiency: number; // percentage
  stages: SleepStages;
  consistency: {
    sameBedtime: boolean;
    sameWakeTime: boolean;
    score: number; // 0-100
  };
}

export interface SleepStages {
  inBed: number; // percentage
  asleep: number; // percentage
  awake: number; // percentage
  core: number; // percentage
  deep: number; // percentage
  REM: number; // percentage
}

export interface DailyHealthSummary {
  date: Date;
  steps: StepsData;
  sleep: SleepAnalysis;
  stepGoal: number;
  stepGoalAchieved: boolean;
  sleepGoal: number; // hours
  sleepGoalAchieved: boolean;
}

export interface HealthInsights {
  stepTrend: 'increasing' | 'decreasing' | 'stable';
  sleepTrend: 'improving' | 'declining' | 'stable';
  recommendations: string[];
  weeklyAverage: {
    steps: number;
    sleep: number;
  };
}
Code collapsed

HealthKit Service Implementation

code
// src/services/healthkitService.ts

import AppleHealthKit, {
  HealthKitPermissions,
  HealthValue,
  HealthObserver,
} from 'react-native-health';

const PERMISSIONS: HealthKitPermissions = {
  permissions: {
    read: [
      AppleHealthKit.Constants.Permissions.Steps,
      AppleHealthKit.Constants.Permissions.StepCount,
      AppleHealthKit.Constants.Permissions.DistanceWalkingRunning,
      AppleHealthKit.Constants.Permissions.FlightsClimbed,
      AppleHealthKit.Constants.Permissions.SleepAnalysis,
      AppleHealthKit.Constants.Permissions.ActivitySummary,
    ],
    write: [
      AppleHealthKit.Constants.Permissions.Steps,
      AppleHealthKit.Constants.Permissions.SleepAnalysis,
    ],
  },
};

export class HealthKitService {
  private isInitialized = false;
  private stepObservers: Array<(data: StepsData) => void> = [];
  private sleepObservers: Array<(data: SleepData) => void> = [];

  async initialize(): Promise<boolean> {
    return new Promise((resolve) => {
      AppleHealthKit.initHealthKit(PERMISSIONS, (error) => {
        if (error) {
          console.error('HealthKit init error:', error);
          resolve(false);
        } else {
          this.isInitialized = true;
          this.setupObservers();
          resolve(true);
        }
      });
    });
  }

  private setupObservers(): void {
    // Setup step observer
    AppleHealthKit.setObserver({
      type: 'Steps',
      enableBackgroundDelivery: true,
      frequency: 'immediate',
    } as HealthObserver);

    // Setup sleep observer
    AppleHealthKit.setObserver({
      type: 'SleepAnalysis',
      enableBackgroundDelivery: true,
      frequency: 'immediate',
    } as HealthObserver);
  }

  async getStepsForDate(date: Date): Promise<StepsData> {
    if (!this.isInitialized) {
      throw new Error('HealthKit not initialized');
    }

    const startOfDay = new Date(date);
    startOfDay.setHours(0, 0, 0, 0);

    const endOfDay = new Date(date);
    endOfDay.setHours(23, 59, 59, 999);

    // Get step count
    const steps = await new Promise<number>((resolve, reject) => {
      AppleHealthKit.getStepCount(
        {
          startDate: startOfDay.toISOString(),
          endDate: endOfDay.toISOString(),
          includeManuallyAdded: true,
        },
        (err, result: HealthValue) => {
          if (err) reject(err);
          else resolve(result.value || 0);
        }
      );
    });

    // Get distance
    const distance = await new Promise<number>((resolve, reject) => {
      AppleHealthKit.getDistanceWalkingRunning(
        {
          startDate: startOfDay.toISOString(),
          endDate: endOfDay.toISOString(),
        },
        (err, result: HealthValue) => {
          if (err) reject(err);
          else resolve(result.value || 0);
        }
      );
    });

    // Get floors climbed
    const floorsAscended = await new Promise<number>((resolve, reject) => {
      AppleHealthKit.getFlightsClimbed(
        {
          startDate: startOfDay.toISOString(),
          endDate: endOfDay.toISOString(),
        },
        (err, result: HealthValue) => {
          if (err) reject(err);
          else resolve(result.value || 0);
        }
      );
    });

    // Get active energy to estimate current activity
    const activeEnergy = await new Promise<number>((resolve, reject) => {
      AppleHealthKit.getActiveEnergyBurned(
        {
          startDate: new Date(Date.now() - 5 * 60 * 1000).toISOString(), // Last 5 minutes
          endDate: new Date().toISOString(),
        },
        (err, result: HealthValue) => {
          if (err) reject(err);
          else resolve(result.value || 0);
        }
      );
    });

    return {
      count: steps,
      distance: Math.round(distance),
      floorsAscended: Math.round(floorsAscended),
      floorsDescended: 0, // HealthKit doesn't provide this directly
      currentPace: steps > 0 ? Math.round((distance / 1000) / (steps / 100)) : undefined,
      currentCadence: activeEnergy > 5 ? Math.round(steps / 60) : undefined, // Estimate if active
      startDate: startOfDay,
      endDate: endOfDay,
    };
  }

  async getSleepAnalysisForDate(date: Date): Promise<SleepData> {
    if (!this.isInitialized) {
      throw new Error('HealthKit not initialized');
    }

    // Sleep often spans two days, so we look at yesterday evening to today evening
    const startOfSleepWindow = new Date(date);
    startOfSleepWindow.setHours(18, 0, 0, 0); // Start from 6 PM

    const endOfSleepWindow = new Date(date);
    endOfSleepWindow.setHours(18, 0, 0, 0);
    endOfSleepWindow.setDate(endOfSleepWindow.getDate() + 1); // Until 6 PM next day

    const sleepSamples = await new Promise<any>((resolve, reject) => {
      AppleHealthKit.getSleepSamples(
        {
          startDate: startOfSleepWindow.toISOString(),
          endDate: endOfSleepWindow.toISOString(),
          limit: 100,
        },
        (err, results) => {
          if (err) reject(err);
          else resolve(results || []);
        }
      );
    });

    if (!sleepSamples || sleepSamples.length === 0) {
      return {
        totalSleepTime: 0,
        timeInBed: 0,
        sleepStages: {
          awake: 0,
          REM: 0,
          core: 0,
          deep: 0,
        },
        sleepQuality: 'poor',
        sleepScore: 0,
        startDate: startOfSleepWindow,
        endDate: endOfSleepWindow,
      };
    }

    // Process sleep stages
    // HealthKit uses these values: 0 = inBed, 1 = asleep, 2 = awake
    let timeInBed = 0;
    let totalSleepTime = 0;
    let awakeTime = 0;

    sleepSamples.forEach((sample: any) => {
      const duration =
        new Date(sample.endDate).getTime() - new Date(sample.startDate).getTime();

      if (sample.value === 0) {
        // In bed
        timeInBed += duration;
      } else if (sample.value === 1) {
        // Asleep
        totalSleepTime += duration;
      } else if (sample.value === 2) {
        // Awake
        awakeTime += duration;
        timeInBed += duration;
      }
    });

    // HealthKit doesn't provide detailed sleep stages (REM, core, deep)
    // We'll estimate based on sleep duration
    const estimatedDeepSleep = totalSleepTime * 0.2; // ~20% deep sleep
    const estimatedREMSleep = totalSleepTime * 0.25; // ~25% REM sleep
    const estimatedCoreSleep = totalSleepTime - estimatedDeepSleep - estimatedREMSleep;

    // Calculate sleep score (0-100)
    const sleepHours = totalSleepTime / (1000 * 60 * 60);
    const sleepScore = this.calculateSleepScore(sleepHours, awakeTime / totalSleepTime);

    // Determine sleep quality
    let sleepQuality: 'excellent' | 'good' | 'fair' | 'poor';
    if (sleepScore >= 90) sleepQuality = 'excellent';
    else if (sleepScore >= 75) sleepQuality = 'good';
    else if (sleepScore >= 60) sleepQuality = 'fair';
    else sleepQuality = 'poor';

    return {
      totalSleepTime,
      timeInBed,
      sleepStages: {
        awake,
        REM: estimatedREMSleep,
        core: estimatedCoreSleep,
        deep: estimatedDeepSleep,
      },
      sleepQuality,
      sleepScore,
      startDate: new Date(sleepSamples[0].startDate),
      endDate: new Date(sleepSamples[sleepSamples.length - 1].endDate),
    };
  }

  private calculateSleepScore(
    sleepHours: number,
    awakePercentage: number
  ): number {
    // Optimal sleep is 7-9 hours
    let durationScore = 0;
    if (sleepHours >= 7 && sleepHours <= 9) {
      durationScore = 100;
    } else if (sleepHours >= 6 && sleepHours < 7) {
      durationScore = 80;
    } else if (sleepHours >= 5 && sleepHours < 6) {
      durationScore = 60;
    } else if (sleepHours > 9 && sleepHours <= 10) {
      durationScore = 85;
    } else {
      durationScore = 50;
    }

    // Lower awake percentage is better
    const continuityScore = Math.max(0, 100 - awakePercentage * 100);

    return Math.round((durationScore + continuityScore) / 2);
  }

  onStepsUpdate(callback: (data: StepsData) => void): void {
    this.stepObservers.push(callback);

    AppleHealthKit.observeSteps((error, results) => {
      if (!error && results) {
        const data: StepsData = {
          count: results.value || 0,
          distance: 0, // Would need separate query
          floorsAscended: 0,
          floorsDescended: 0,
          startDate: new Date(results.startDate),
          endDate: new Date(results.endDate),
        };

        this.stepObservers.forEach((cb) => cb(data));
      }
    });
  }

  onSleepUpdate(callback: (data: SleepData) => void): void {
    this.sleepObservers.push(callback);

    AppleHealthKit.observeSleepAnalysis((error, results) => {
      if (!error && results && results.length > 0) {
        // Process and notify observers
        // This is a simplified version - real implementation would process all samples
        const latestSample = results[results.length - 1];

        this.getSleepAnalysisForDate(new Date(latestSample.startDate)).then(
          (data) => {
            this.sleepObservers.forEach((cb) => cb(data));
          }
        );
      }
    });
  }

  async writeSteps(count: number, date: Date): Promise<void> {
    if (!this.isInitialized) {
      throw new Error('HealthKit not initialized');
    }

    return new Promise((resolve, reject) => {
      AppleHealthKit.saveSteps(
        {
          value: count,
          startDate: date.toISOString(),
          endDate: new Date(date.getTime() + 1000).toISOString(),
        },
        (error) => {
          if (error) reject(error);
          else resolve();
        }
      );
    });
  }

  async getWeeklySteps(startDate: Date): Promise<number[]> {
    const steps: number[] = [];

    for (let i = 0; i < 7; i++) {
      const date = new Date(startDate);
      date.setDate(date.getDate() - i);

      const data = await this.getStepsForDate(date);
      steps.unshift(data.count);
    }

    return steps;
  }

  async getWeeklySleep(startDate: Date): Promise<number[]> {
    const sleepHours: number[] = [];

    for (let i = 0; i < 7; i++) {
      const date = new Date(startDate);
      date.setDate(date.getDate() - i);

      const data = await this.getSleepAnalysisForDate(date);
      sleepHours.unshift(data.totalSleepTime / (1000 * 60 * 60));
    }

    return sleepHours;
  }
}

export const healthKitService = new HealthKitService();
Code collapsed

Health Data Manager with Caching

code
// src/services/healthDataManager.ts

import AsyncStorage from '@react-native-async-storage/async-storage';
import { healthKitService } from './healthkitService';
import type { StepsData, SleepData, DailyHealthSummary } from '../types/health';

const CACHE_KEYS = {
  STEPS: '@wellally:steps:',
  SLEEP: '@wellally:sleep:',
  SUMMARY: '@wellally:summary:',
};

const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes

export class HealthDataManager {
  private cache: Map<string, { data: any; timestamp: number }> = new Map();

  async getTodaySteps(forceRefresh = false): Promise<StepsData> {
    const today = new Date();
    const cacheKey = `${CACHE_KEYS.STEPS}${today.toDateString()}`;

    if (!forceRefresh) {
      const cached = await this.getCachedData<StepsData>(cacheKey);
      if (cached) {
        return cached;
      }
    }

    const data = await healthKitService.getStepsForDate(today);
    await this.setCachedData(cacheKey, data);

    return data;
  }

  async getTodaySleep(forceRefresh = false): Promise<SleepData> {
    const today = new Date();
    const cacheKey = `${CACHE_KEYS.SLEEP}${today.toDateString()}`;

    if (!forceRefresh) {
      const cached = await this.getCachedData<SleepData>(cacheKey);
      if (cached) {
        return cached;
      }
    }

    const data = await healthKitService.getSleepAnalysisForDate(today);
    await this.setCachedData(cacheKey, data);

    return data;
  }

  async getDailySummary(date: Date, forceRefresh = false): Promise<DailyHealthSummary> {
    const cacheKey = `${CACHE_KEYS.SUMMARY}${date.toDateString()}`;

    if (!forceRefresh) {
      const cached = await this.getCachedData<DailyHealthSummary>(cacheKey);
      if (cached) {
        return cached;
      }
    }

    const steps = await healthKitService.getStepsForDate(date);
    const sleep = await healthKitService.getSleepAnalysisForDate(date);

    const summary: DailyHealthSummary = {
      date,
      steps,
      sleep: {
        date,
        bedtime: sleep.startDate,
        wakeTime: sleep.endDate,
        duration: sleep.totalSleepTime / (1000 * 60 * 60),
        efficiency: (sleep.totalSleepTime / sleep.timeInBed) * 100,
        stages: {
          inBed: 100,
          asleep: (sleep.totalSleepTime / sleep.timeInBed) * 100,
          awake: (sleep.sleepStages.awake / sleep.timeInBed) * 100,
          core: (sleep.sleepStages.core / sleep.totalSleepTime) * 100,
          deep: (sleep.sleepStages.deep / sleep.totalSleepTime) * 100,
          REM: (sleep.sleepStages.REM / sleep.totalSleepTime) * 100,
        },
        consistency: {
          sameBedtime: false,
          sameWakeTime: false,
          score: 0,
        },
      },
      stepGoal: 10000,
      stepGoalAchieved: steps.count >= 10000,
      sleepGoal: 8,
      sleepGoalAchieved:
        sleep.totalSleepTime / (1000 * 60 * 60) >= 7 &&
        sleep.totalSleepTime / (1000 * 60 * 60) <= 9,
    };

    await this.setCachedData(cacheKey, summary);

    return summary;
  }

  async getWeeklySummaries(endDate: Date): Promise<DailyHealthSummary[]> {
    const summaries: DailyHealthSummary[] = [];

    for (let i = 6; i >= 0; i--) {
      const date = new Date(endDate);
      date.setDate(date.getDate() - i);
      date.setHours(0, 0, 0, 0);

      const summary = await this.getDailySummary(date);
      summaries.push(summary);
    }

    return summaries;
  }

  private async getCachedData<T>(key: string): Promise<T | null> {
    // Check memory cache first
    const memCached = this.cache.get(key);
    if (memCached && Date.now() - memCached.timestamp < CACHE_DURATION) {
      return memCached.data as T;
    }

    // Check persistent cache
    try {
      const json = await AsyncStorage.getItem(key);
      if (json) {
        const { data, timestamp } = JSON.parse(json);

        if (Date.now() - timestamp < CACHE_DURATION) {
          // Restore to memory cache
          this.cache.set(key, { data, timestamp });
          return data as T;
        }
      }
    } catch (error) {
      console.error('Error reading from cache:', error);
    }

    return null;
  }

  private async setCachedData<T>(key: string, data: T): Promise<void> {
    const timestamp = Date.now();
    const cacheData = { data, timestamp };

    // Store in memory
    this.cache.set(key, cacheData);

    // Store persistently
    try {
      await AsyncStorage.setItem(key, JSON.stringify(cacheData));
    } catch (error) {
      console.error('Error writing to cache:', error);
    }
  }

  async clearCache(): Promise<void> {
    this.cache.clear();

    const keys = await AsyncStorage.getAllKeys();
    const wellallyKeys = keys.filter((key) => key.startsWith('@wellally:'));
    await AsyncStorage.multiRemove(wellallyKeys);
  }
}

export const healthDataManager = new HealthDataManager();
Code collapsed

Insights Engine

code
// src/services/insightsEngine.ts

import type { DailyHealthSummary, HealthInsights } from '../types/health';

export class InsightsEngine {
  generateInsights(summaries: DailyHealthSummary[]): HealthInsights {
    const recentSummaries = summaries.slice(-7); // Last 7 days

    return {
      stepTrend: this.analyzeStepTrend(recentSummaries),
      sleepTrend: this.analyzeSleepTrend(recentSummaries),
      recommendations: this.generateRecommendations(recentSummaries),
      weeklyAverage: {
        steps: this.calculateAverageSteps(recentSummaries),
        sleep: this.calculateAverageSleep(recentSummaries),
      },
    };
  }

  private analyzeStepTrend(summaries: DailyHealthSummary[]): 'increasing' | 'decreasing' | 'stable' {
    if (summaries.length < 3) return 'stable';

    const firstHalf = summaries.slice(0, Math.floor(summaries.length / 2));
    const secondHalf = summaries.slice(Math.floor(summaries.length / 2));

    const firstAvg = this.calculateAverageSteps(firstHalf);
    const secondAvg = this.calculateAverageSteps(secondHalf);

    const changePercent = ((secondAvg - firstAvg) / firstAvg) * 100;

    if (changePercent > 10) return 'increasing';
    if (changePercent < -10) return 'decreasing';
    return 'stable';
  }

  private analyzeSleepTrend(summaries: DailyHealthSummary[]): 'improving' | 'declining' | 'stable' {
    if (summaries.length < 3) return 'stable';

    const recentScores = summaries.slice(-3).map((s) => s.sleep.efficiency);
    const olderScores = summaries.slice(-6, -3).map((s) => s.sleep.efficiency);

    const recentAvg = recentScores.reduce((a, b) => a + b, 0) / recentScores.length;
    const olderAvg = olderScores.reduce((a, b) => a + b, 0) / olderScores.length;

    const changePercent = ((recentAvg - olderAvg) / olderAvg) * 100;

    if (changePercent > 5) return 'improving';
    if (changePercent < -5) return 'declining';
    return 'stable';
  }

  private generateRecommendations(summaries: DailyHealthSummary[]): string[] {
    const recommendations: string[] = [];

    const avgSteps = this.calculateAverageSteps(summaries);
    const avgSleep = this.calculateAverageSleep(summaries);

    // Step recommendations
    if (avgSteps < 7000) {
      recommendations.push(
        'Try to increase your daily steps. Aim for at least 7,000 steps per day.'
      );
    } else if (avgSteps < 10000) {
      recommendations.push(
        "You're doing well! Try to reach 10,000 steps daily for optimal health benefits."
      );
    } else {
      recommendations.push(
        "Excellent work maintaining an active lifestyle! Keep up the great step count."
      );
    }

    // Sleep recommendations
    if (avgSleep < 7) {
      recommendations.push(
        'Your sleep duration is below the recommended 7-9 hours. Try to go to bed 30 minutes earlier.'
      );
    } else if (avgSleep > 9) {
      recommendations.push(
        'You\'re sleeping more than 9 hours. While individual needs vary, oversleeping can sometimes indicate health issues.'
      );
    } else {
      recommendations.push(
        'Your sleep duration is within the optimal range. Maintain your current sleep schedule.'
      );
    }

    // Sleep efficiency recommendations
    const avgEfficiency =
      summaries.reduce((sum, s) => sum + s.sleep.efficiency, 0) / summaries.length;

    if (avgEfficiency < 85) {
      recommendations.push(
        'Your sleep efficiency could be improved. Consider reducing screen time before bed and maintaining a consistent sleep schedule.'
      );
    }

    // Consistency recommendations
    const consistentBedtime = this.checkBedtimeConsistency(summaries);
    const consistentWakeTime = this.checkWakeTimeConsistency(summaries);

    if (!consistentBedtime) {
      recommendations.push(
        'Try to maintain a consistent bedtime, even on weekends. This helps regulate your body\'s internal clock.'
      );
    }

    if (!consistentWakeTime) {
      recommendations.push(
        'Wake up at the same time every day to improve sleep quality and make mornings easier.'
      );
    }

    return recommendations;
  }

  private calculateAverageSteps(summaries: DailyHealthSummary[]): number {
    if (summaries.length === 0) return 0;

    const total = summaries.reduce((sum, s) => sum + s.steps.count, 0);
    return Math.round(total / summaries.length);
  }

  private calculateAverageSleep(summaries: DailyHealthSummary[]): number {
    if (summaries.length === 0) return 0;

    const total = summaries.reduce((sum, s) => sum + s.sleep.duration, 0);
    return parseFloat((total / summaries.length).toFixed(1));
  }

  private checkBedtimeConsistency(summaries: DailyHealthSummary[]): boolean {
    if (summaries.length < 3) return true;

    const bedtimes = summaries
      .map((s) => s.sleep.bedtime.getHours() * 60 + s.sleep.bedtime.getMinutes())
      .sort((a, b) => a - b);

    const range = bedtimes[bedtimes.length - 1] - bedtimes[0];
    return range <= 60; // Within 1 hour
  }

  private checkWakeTimeConsistency(summaries: DailyHealthSummary[]): boolean {
    if (summaries.length < 3) return true;

    const wakeTimes = summaries
      .map((s) => s.sleep.wakeTime.getHours() * 60 + s.sleep.wakeTime.getMinutes())
      .sort((a, b) => a - b);

    const range = wakeTimes[wakeTimes.length - 1] - wakeTimes[0];
    return range <= 60; // Within 1 hour
  }
}

export const insightsEngine = new InsightsEngine();
Code collapsed

React Hook Integration

code
// src/hooks/useStepsAndSleep.ts

import { useState, useEffect, useCallback } from 'react';
import { healthDataManager } from '../services/healthDataManager';
import { healthKitService } from '../services/healthkitService';
import { insightsEngine } from '../services/insightsEngine';
import type { StepsData, SleepData, DailyHealthSummary, HealthInsights } from '../types/health';

export const useStepsAndSleep = () => {
  const [steps, setSteps] = useState<StepsData | null>(null);
  const [sleep, setSleep] = useState<SleepData | null>(null);
  const [summaries, setSummaries] = useState<DailyHealthSummary[]>([]);
  const [insights, setInsights] = useState<HealthInsights | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  const [isAuthorized, setIsAuthorized] = useState(false);

  const initialize = useCallback(async () => {
    try {
      setIsLoading(true);
      setError(null);

      const authorized = await healthKitService.initialize();
      setIsAuthorized(authorized);

      if (!authorized) {
        throw new Error('HealthKit authorization failed');
      }

      // Load initial data
      await refreshData();

      // Setup observers
      healthKitService.onStepsUpdate((data) => {
        setSteps(data);
      });

      healthKitService.onSleepUpdate((data) => {
        setSleep(data);
      });
    } catch (err) {
      setError(err as Error);
    } finally {
      setIsLoading(false);
    }
  }, []);

  const refreshData = useCallback(async () => {
    try {
      setIsLoading(true);
      setError(null);

      const today = new Date();

      // Get today's data
      const [todaySteps, todaySleep] = await Promise.all([
        healthDataManager.getTodaySteps(true),
        healthDataManager.getTodaySleep(true),
      ]);

      setSteps(todaySteps);
      setSleep(todaySleep);

      // Get weekly summaries
      const weeklySummaries = await healthDataManager.getWeeklySummaries(today);
      setSummaries(weeklySummaries);

      // Generate insights
      const healthInsights = insightsEngine.generateInsights(weeklySummaries);
      setInsights(healthInsights);
    } catch (err) {
      setError(err as Error);
    } finally {
      setIsLoading(false);
    }
  }, []);

  useEffect(() => {
    initialize();
  }, [initialize]);

  return {
    steps,
    sleep,
    summaries,
    insights,
    isLoading,
    error,
    isAuthorized,
    refreshData,
  };
};
Code collapsed

Sample UI Component

code
// src/components/StepsSleepDashboard.tsx

import React, { useMemo } from 'react';
import {
  View,
  Text,
  StyleSheet,
  ScrollView,
  ActivityIndicator,
  Dimensions,
} from 'react-native';
import { useStepsAndSleep } from '../hooks/useStepsAndSleep';
import { LineChart } from 'react-native-chart-kit';

const { width } = Dimensions.get('window');

export const StepsSleepDashboard: React.FC = () => {
  const { steps, sleep, summaries, insights, isLoading, error, refreshData } =
    useStepsAndSleep();

  const chartData = useMemo(() => {
    return {
      labels: summaries.map((s) =>
        s.date.toLocaleDateString('en-US', { weekday: 'short' })
      ),
      datasets: [
        {
          data: summaries.map((s) => Math.round(s.steps.count / 1000)),
          color: (opacity = 1) => `rgba(74, 144, 226, ${opacity})`,
        },
      ],
    };
  }, [summaries]);

  const sleepChartData = useMemo(() => {
    return {
      labels: summaries.map((s) =>
        s.date.toLocaleDateString('en-US', { weekday: 'short' })
      ),
      datasets: [
        {
          data: summaries.map((s) => parseFloat(s.sleep.duration.toFixed(1))),
          color: (opacity = 1) => `rgba(155, 89, 182, ${opacity})`,
        },
      ],
    };
  }, [summaries]);

  if (isLoading) {
    return (
      <View style={styles.loadingContainer}>
        <ActivityIndicator size: "large" color: "#4A90E2" />
        <Text style={styles.loadingText}>Loading your health data...</Text>
      </View>
    );
  }

  if (error) {
    return (
      <View style={styles.errorContainer}>
        <Text style={styles.errorText}>Error: {error.message}</Text>
      </View>
    );
  }

  return (
    <ScrollView style={styles.container} refreshControl={{ onRefresh: refreshData }}>
      {/* Steps Overview */}
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>Steps Today</Text>
        <View style={styles.metricCard}>
          <Text style={styles.metricValue}>
            {steps?.count.toLocaleString() || '0'}
          </Text>
          <Text style={styles.metricLabel}>steps</Text>
          <View style={styles.progressContainer}>
            <View style={styles.progressBar}>
              <View
                style={[
                  styles.progressFill,
                  { width: `${Math.min(((steps?.count || 0) / 10000) * 100, 100)}%` },
                ]}
              />
            </View>
            <Text style={styles.progressText}>
              {Math.min(((steps?.count || 0) / 10000) * 100, 100).toFixed(0)}% of goal
            </Text>
          </View>
        </View>

        {steps && (
          <View style={styles.detailsRow}>
            <View style={styles.detailItem}>
              <Text style={styles.detailValue}>
                {(steps.distance / 1000).toFixed(1)} km
              </Text>
              <Text style={styles.detailLabel}>Distance</Text>
            </View>
            <View style={styles.detailItem}>
              <Text style={styles.detailValue}>{steps.floorsAscended}</Text>
              <Text style={styles.detailLabel}>Floors</Text>
            </View>
          </View>
        )}
      </View>

      {/* Sleep Overview */}
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>Sleep Last Night</Text>
        <View style={styles.metricCard}>
          <Text style={styles.metricValue}>
            {((sleep?.totalSleepTime || 0) / (1000 * 60 * 60)).toFixed(1)}h
          </Text>
          <Text style={styles.metricLabel}>
            {sleep?.sleepQuality || 'Poor'} quality
          </Text>
          <View style={styles.sleepScoreContainer}>
            <View style={styles.sleepScoreBar}>
              <View
                style={[
                  styles.sleepScoreFill,
                  {
                    width: `${sleep?.sleepScore || 0}%`,
                    backgroundColor:
                      (sleep?.sleepScore || 0) >= 90
                        ? '#27ae60'
                        : (sleep?.sleepScore || 0) >= 75
                        ? '#f39c12'
                        : '#e74c3c',
                  },
                ]}
              />
            </View>
            <Text style={styles.sleepScoreText}>
              Sleep Score: {sleep?.sleepScore || 0}/100
            </Text>
          </View>
        </View>

        {sleep && (
          <View style={styles.sleepStagesContainer}>
            <Text style={styles.sleepStagesTitle}>Sleep Stages</Text>
            <SleepStageBar
              label: "Deep"
              percentage={(sleep.sleepStages.deep / sleep.totalSleepTime) * 100}
              color: "#3498db"
            />
            <SleepStageBar
              label: "REM"
              percentage={(sleep.sleepStages.REM / sleep.totalSleepTime) * 100}
              color: "#9b59b6"
            />
            <SleepStageBar
              label: "Core"
              percentage={(sleep.sleepStages.core / sleep.totalSleepTime) * 100}
              color: "#1abc9c"
            />
            <SleepStageBar
              label: "Awake"
              percentage={(sleep.sleepStages.awake / sleep.totalSleepTime) * 100}
              color: "#e74c3c"
            />
          </View>
        )}
      </View>

      {/* Weekly Steps Chart */}
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>Weekly Steps</Text>
        <LineChart
          data={chartData}
          width={width - 40}
          height={220}
          chartConfig={{
            backgroundColor: '#1cc910',
            backgroundGradientFrom: '#eff3ff',
            backgroundGradientTo: '#efefef',
            decimalPlaces: 0,
            color: (opacity = 1) => `rgba(74, 144, 226, ${opacity})`,
            style: {
              borderRadius: 16,
            },
          }}
          bezier
          style={styles.chart}
        />
      </View>

      {/* Weekly Sleep Chart */}
      <View style={styles.section}>
        <Text style={styles.sectionTitle}>Weekly Sleep (hours)</Text>
        <LineChart
          data={sleepChartData}
          width={width - 40}
          height={220}
          chartConfig={{
            backgroundColor: '#1cc910',
            backgroundGradientFrom: '#f3e5f5',
            backgroundGradientTo: '#efefef',
            decimalPlaces: 1,
            color: (opacity = 1) => `rgba(155, 89, 182, ${opacity})`,
            style: {
              borderRadius: 16,
            },
          }}
          bezier
          style={styles.chart}
        />
      </View>

      {/* Insights */}
      {insights && (
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>Weekly Insights</Text>
          <View style={styles.insightsCard}>
            <View style={styles.insightItem}>
              <Text style={styles.insightLabel}>Step Trend</Text>
              <Text
                style={[
                  styles.insightValue,
                  insights.stepTrend === 'increasing'
                    ? styles.positive
                    : insights.stepTrend === 'decreasing'
                    ? styles.negative
                    : styles.neutral,
                ]}
              >
                {insights.stepTrend.charAt(0).toUpperCase() + insights.stepTrend.slice(1)}
              </Text>
            </View>
            <View style={styles.insightItem}>
              <Text style={styles.insightLabel}>Sleep Trend</Text>
              <Text
                style={[
                  styles.insightValue,
                  insights.sleepTrend === 'improving'
                    ? styles.positive
                    : insights.sleepTrend === 'declining'
                    ? styles.negative
                    : styles.neutral,
                ]}
              >
                {insights.sleepTrend.charAt(0).toUpperCase() + insights.sleepTrend.slice(1)}
              </Text>
            </View>
            <View style={styles.insightItem}>
              <Text style={styles.insightLabel}>Weekly Avg Steps</Text>
              <Text style={styles.insightValue}>
                {insights.weeklyAverage.steps.toLocaleString()}
              </Text>
            </View>
            <View style={styles.insightItem}>
              <Text style={styles.insightLabel}>Weekly Avg Sleep</Text>
              <Text style={styles.insightValue}>{insights.weeklyAverage.sleep}h</Text>
            </View>
          </View>

          {insights.recommendations.length > 0 && (
            <View style={styles.recommendationsCard}>
              <Text style={styles.recommendationsTitle}>Recommendations</Text>
              {insights.recommendations.map((rec, index) => (
                <View key={index} style={styles.recommendationItem}>
                  <Text style={styles.recommendationText}>{rec}</Text>
                </View>
              ))}
            </View>
          )}
        </View>
      )}
    </ScrollView>
  );
};

const SleepStageBar: React.FC<{
  label: string;
  percentage: number;
  color: string;
}> = ({ label, percentage, color }) => (
  <View style={styles.sleepStageBar}>
    <Text style={styles.sleepStageLabel}>{label}</Text>
    <View style={styles.sleepStageBarContainer}>
      <View
        style={[
          styles.sleepStageBarFill,
          { width: `${Math.min(percentage, 100)}%`, backgroundColor: color },
        ]}
      />
      <Text style={styles.sleepStagePercentage}>{percentage.toFixed(0)}%</Text>
    </View>
  </View>
);

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
  },
  loadingContainer: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#f5f5f5',
  },
  loadingText: {
    marginTop: 16,
    fontSize: 16,
    color: '#666',
  },
  errorContainer: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#f5f5f5',
    padding: 20,
  },
  errorText: {
    fontSize: 16,
    color: '#e74c3c',
    textAlign: 'center',
  },
  section: {
    backgroundColor: '#fff',
    margin: 16,
    padding: 16,
    borderRadius: 12,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  sectionTitle: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#333',
    marginBottom: 16,
  },
  metricCard: {
    backgroundColor: '#f9f9f9',
    padding: 20,
    borderRadius: 12,
    alignItems: 'center',
  },
  metricValue: {
    fontSize: 48,
    fontWeight: 'bold',
    color: '#333',
  },
  metricLabel: {
    fontSize: 16,
    color: '#666',
    marginTop: 8,
    textTransform: 'uppercase',
  },
  progressContainer: {
    width: '100%',
    marginTop: 16,
  },
  progressBar: {
    height: 8,
    backgroundColor: '#e0e0e0',
    borderRadius: 4,
    overflow: 'hidden',
  },
  progressFill: {
    height: '100%',
    backgroundColor: '#4A90E2',
  },
  progressText: {
    fontSize: 14,
    color: '#666',
    marginTop: 8,
    textAlign: 'center',
  },
  detailsRow: {
    flexDirection: 'row',
    marginTop: 16,
    justifyContent: 'space-around',
  },
  detailItem: {
    alignItems: 'center',
  },
  detailValue: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#333',
  },
  detailLabel: {
    fontSize: 14,
    color: '#666',
    marginTop: 4,
  },
  sleepScoreContainer: {
    width: '100%',
    marginTop: 16,
  },
  sleepScoreBar: {
    height: 8,
    backgroundColor: '#e0e0e0',
    borderRadius: 4,
    overflow: 'hidden',
  },
  sleepScoreFill: {
    height: '100%',
  },
  sleepScoreText: {
    fontSize: 14,
    color: '#666',
    marginTop: 8,
    textAlign: 'center',
  },
  sleepStagesContainer: {
    marginTop: 16,
  },
  sleepStagesTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#333',
    marginBottom: 12,
  },
  sleepStageBar: {
    marginBottom: 12,
  },
  sleepStageLabel: {
    fontSize: 14,
    color: '#666',
    marginBottom: 4,
  },
  sleepStageBarContainer: {
    height: 24,
    backgroundColor: '#f0f0f0',
    borderRadius: 12,
    overflow: 'hidden',
    position: 'relative',
  },
  sleepStageBarFill: {
    height: '100%',
  },
  sleepStagePercentage: {
    position: 'absolute',
    right: 8,
    top: 2,
    fontSize: 14,
    fontWeight: 'bold',
    color: '#333',
  },
  chart: {
    marginVertical: 8,
    borderRadius: 16,
  },
  insightsCard: {
    backgroundColor: '#f9f9f9',
    borderRadius: 12,
    padding: 16,
  },
  insightItem: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    paddingVertical: 8,
    borderBottomWidth: 1,
    borderBottomColor: '#e0e0e0',
  },
  insightLabel: {
    fontSize: 16,
    color: '#666',
  },
  insightValue: {
    fontSize: 16,
    fontWeight: 'bold',
  },
  positive: {
    color: '#27ae60',
  },
  negative: {
    color: '#e74c3c',
  },
  neutral: {
    color: '#7f8c8d',
  },
  recommendationsCard: {
    backgroundColor: '#e8f5e9',
    borderRadius: 12,
    padding: 16,
    marginTop: 16,
  },
  recommendationsTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#2e7d32',
    marginBottom: 12,
  },
  recommendationItem: {
    paddingVertical: 4,
  },
  recommendationText: {
    fontSize: 14,
    color: '#1b5e20',
    lineHeight: 20,
  },
});
Code collapsed

Background Task Setup

code
// src/services/backgroundHealthSync.ts

import BackgroundJob from 'react-native-background-job';
import { healthDataManager } from './healthDataManager';
import { healthKitService } from './healthkitService';

export class BackgroundHealthSync {
  private static JOB_ID = 'wellally_health_sync';

  static async initialize(): Promise<void> {
    BackgroundJob.on(this.JOB_ID, async () => {
      console.log('Background health sync started');

      try {
        // Refresh today's data
        await healthDataManager.getTodaySteps(true);
        await healthDataManager.getTodaySleep(true);

        // Keep observers active
        healthKitService.onStepsUpdate((data) => {
          console.log('Background step update:', data.count);
        });

        console.log('Background health sync completed');
      } catch (error) {
        console.error('Background sync error:', error);
      }
    });
  }

  static async schedule(): Promise<void> {
    await BackgroundJob.schedule({
      jobKey: this.JOB_ID,
      period: 900000, // 15 minutes
      exact: false,
      allowWhileIdle: true,
      allowExecutionInForeground: true,
    });
  }

  static async cancel(): Promise<void> {
    await BackgroundJob.cancel(this.JOB_ID);
  }
}
Code collapsed

Production Considerations

Battery Optimization

  1. Reduce Observer Frequency: Set observation frequency to 'hourly' instead of 'immediate' for less critical metrics
  2. Batch Queries: Group multiple HealthKit queries into single operations
  3. Intelligent Caching: Cache data aggressively and respect cache durations

Error Handling

  1. Permission Changes: Handle cases where users revoke permissions in Settings
  2. Data Unavailability: Gracefully handle days with no sleep/steps data
  3. HealthKit Unavailability: Support devices without HealthKit capability

Testing Strategy

  1. Unit Tests: Test data transformation logic and insight generation
  2. Integration Tests: Test HealthKit service with mock data
  3. Device Testing: Always test on physical iOS devices

Conclusion

Building a production-ready steps and sleep tracker with HealthKit integration requires careful attention to platform-specific APIs, data caching strategies, and user experience considerations. The architecture provided in this guide offers a solid foundation that you can extend with additional features like goal setting, social sharing, and personalized recommendations.

The key to success is continuous testing on real devices, careful attention to battery performance, and providing users with meaningful insights that help them improve their health habits.

Resources

Frequently Asked Questions

Q: How accurate is HealthKit step counting?

A: HealthKit aggregates data from multiple sources including the iPhone's motion coprocessor and connected Apple Watches. Accuracy varies based on device placement and activity type but is generally within 10-15% of actual steps for walking/running activities.

Q: Can I detect sleep stages beyond awake/asleep?

A: HealthKit's basic sleep analysis only provides in-bed, asleep, and awake states. Detailed sleep staging (REM, core, deep) requires Apple Watch with watchOS 9+ and using HKCategoryTypeSleepAnalysis with specific stage values.

Q: How do I handle data from users who don't have an Apple Watch?

A: The iPhone can track steps and some sleep data using the accelerometer. Your app should work with whatever data is available and clearly indicate limitations to users.

Q: What's the best way to sync data with a backend?

A: Implement incremental sync using HealthKit's HKAnchoredObjectQuery to efficiently fetch only changed records since the last sync. This reduces bandwidth and improves sync reliability.

Q: How do I handle timezone changes when analyzing sleep?

A: Always store and query sleep data using UTC, then convert to the user's local timezone for display. This ensures consistent analysis regardless of travel.

#

Article Tags

reactnative
healthkit
sleep
steps
healthtech
ios
W

WellAlly's core development team, comprised of healthcare professionals, software engineers, and UX designers committed to revolutionizing digital health management.

Expertise

Healthcare Technology
Software Development
User Experience
AI & Machine Learning

Found this article helpful?

Try KangXinBan and start your health management journey