Bobby Encoded
PostsAbout
PostsAbout

© 2026 Bobby Jose

← Back to Blog

iOS System Integrations: HealthKit, Widgets, and More

May 19, 2025 · 9 min read

iOS, HealthKit, WidgetKit, Push Notifications, Interview Prep

Introduction

iOS apps become powerful when they integrate with the system. HealthKit lets you read and write health data, WidgetKit puts your app on the home screen, and background tasks keep data fresh. Understanding these APIs shows interviewers you can build apps that feel native and useful beyond their main screen.

Building a nutrition app required deep integration with Apple Health to sync meal data and read glucose readings - here's what I learned.


HealthKit Fundamentals

Setup and Authorization

import HealthKit

class HealthKitManager {
    static let shared = HealthKitManager()

    let healthStore = HKHealthStore()

    // Types we want to read
    let readTypes: Set<HKObjectType> = [
        HKObjectType.quantityType(forIdentifier: .bloodGlucose)!,
        HKObjectType.quantityType(forIdentifier: .bodyMass)!,
        HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
        HKObjectType.quantityType(forIdentifier: .stepCount)!,
        HKObjectType.categoryType(forIdentifier: .sleepAnalysis)!
    ]

    // Types we want to write
    let writeTypes: Set<HKSampleType> = [
        HKObjectType.quantityType(forIdentifier: .dietaryEnergyConsumed)!,
        HKObjectType.quantityType(forIdentifier: .dietaryProtein)!,
        HKObjectType.quantityType(forIdentifier: .dietaryCarbohydrates)!,
        HKObjectType.quantityType(forIdentifier: .dietaryFatTotal)!
    ]

    func requestAuthorization() async throws {
        guard HKHealthStore.isHealthDataAvailable() else {
            throw HealthKitError.notAvailable
        }

        try await healthStore.requestAuthorization(toShare: writeTypes, read: readTypes)
    }

    func checkAuthorizationStatus(for type: HKObjectType) -> HKAuthorizationStatus {
        healthStore.authorizationStatus(for: type)
    }
}

enum HealthKitError: LocalizedError {
    case notAvailable
    case unauthorized
    case queryFailed(Error)

    var errorDescription: String? {
        switch self {
        case .notAvailable: return "HealthKit is not available on this device"
        case .unauthorized: return "HealthKit access not authorized"
        case .queryFailed(let error): return "Query failed: \(error.localizedDescription)"
        }
    }
}

HealthKit Availability

HealthKit isn't available on all devices (notably iPad). Always check HKHealthStore.isHealthDataAvailable() before attempting any HealthKit operations. Apps crash if you skip this check.


Reading Health Data

Sample Queries

extension HealthKitManager {
    // Read glucose readings
    func fetchGlucoseReadings(
        from startDate: Date,
        to endDate: Date
    ) async throws -> [GlucoseReading] {
        let glucoseType = HKQuantityType.quantityType(forIdentifier: .bloodGlucose)!

        let predicate = HKQuery.predicateForSamples(
            withStart: startDate,
            end: endDate,
            options: .strictStartDate
        )

        let sortDescriptor = NSSortDescriptor(
            key: HKSampleSortIdentifierStartDate,
            ascending: false
        )

        return try await withCheckedThrowingContinuation { continuation in
            let query = HKSampleQuery(
                sampleType: glucoseType,
                predicate: predicate,
                limit: HKObjectQueryNoLimit,
                sortDescriptors: [sortDescriptor]
            ) { _, samples, error in
                if let error = error {
                    continuation.resume(throwing: HealthKitError.queryFailed(error))
                    return
                }

                let readings = (samples as? [HKQuantitySample])?.map { sample in
                    GlucoseReading(
                        value: sample.quantity.doubleValue(for: .milligramsPerDeciliter),
                        timestamp: sample.startDate,
                        source: sample.sourceRevision.source.name
                    )
                } ?? []

                continuation.resume(returning: readings)
            }

            healthStore.execute(query)
        }
    }
}

Statistics Queries for Aggregated Data

extension HealthKitManager {
    // Read step count (aggregated)
    func fetchSteps(for date: Date) async throws -> Int {
        let stepsType = HKQuantityType.quantityType(forIdentifier: .stepCount)!

        let calendar = Calendar.current
        let startOfDay = calendar.startOfDay(for: date)
        let endOfDay = calendar.date(byAdding: .day, value: 1, to: startOfDay)!

        let predicate = HKQuery.predicateForSamples(
            withStart: startOfDay,
            end: endOfDay,
            options: .strictStartDate
        )

        return try await withCheckedThrowingContinuation { continuation in
            let query = HKStatisticsQuery(
                quantityType: stepsType,
                quantitySamplePredicate: predicate,
                options: .cumulativeSum
            ) { _, statistics, error in
                if let error = error {
                    continuation.resume(throwing: HealthKitError.queryFailed(error))
                    return
                }

                let steps = statistics?.sumQuantity()?.doubleValue(for: .count()) ?? 0
                continuation.resume(returning: Int(steps))
            }

            healthStore.execute(query)
        }
    }
}

Writing Health Data

extension HealthKitManager {
    func saveMealNutrition(_ meal: Meal) async throws {
        let calories = HKQuantitySample(
            type: HKQuantityType.quantityType(forIdentifier: .dietaryEnergyConsumed)!,
            quantity: HKQuantity(unit: .kilocalorie(), doubleValue: Double(meal.calories)),
            start: meal.loggedAt,
            end: meal.loggedAt,
            metadata: ["MealName": meal.name, "MealType": meal.mealType.rawValue]
        )

        let protein = HKQuantitySample(
            type: HKQuantityType.quantityType(forIdentifier: .dietaryProtein)!,
            quantity: HKQuantity(unit: .gram(), doubleValue: meal.protein),
            start: meal.loggedAt,
            end: meal.loggedAt
        )

        let carbs = HKQuantitySample(
            type: HKQuantityType.quantityType(forIdentifier: .dietaryCarbohydrates)!,
            quantity: HKQuantity(unit: .gram(), doubleValue: meal.carbs),
            start: meal.loggedAt,
            end: meal.loggedAt
        )

        try await healthStore.save([calories, protein, carbs])
    }
}

HealthKit Privacy

HealthKit uses granular, per-type authorization. Users can grant access to steps but deny glucose readings. Always check authorization status before reading and handle partial permissions gracefully. You can't determine if a user denied or never responded to a read request - design your UI accordingly.


Push Notifications

Authorization and Scheduling

import UserNotifications

class NotificationManager {
    static let shared = NotificationManager()

    func requestAuthorization() async throws -> Bool {
        let center = UNUserNotificationCenter.current()
        let options: UNAuthorizationOptions = [.alert, .sound, .badge]

        return try await center.requestAuthorization(options: options)
    }

    func scheduleReminder(
        title: String,
        body: String,
        at time: DateComponents,
        identifier: String
    ) async throws {
        let content = UNMutableNotificationContent()
        content.title = title
        content.body = body
        content.sound = .default

        let trigger = UNCalendarNotificationTrigger(
            dateMatching: time,
            repeats: true
        )

        let request = UNNotificationRequest(
            identifier: identifier,
            content: content,
            trigger: trigger
        )

        try await UNUserNotificationCenter.current().add(request)
    }

    func scheduleMealReminders() async {
        let reminders = [
            ("breakfast", "Time for Breakfast!", 8, 0),
            ("lunch", "Time for Lunch!", 12, 0),
            ("dinner", "Time for Dinner!", 18, 30)
        ]

        for (id, title, hour, minute) in reminders {
            var components = DateComponents()
            components.hour = hour
            components.minute = minute

            try? await scheduleReminder(
                title: title,
                body: "Don't forget to log your meal",
                at: components,
                identifier: "meal-reminder-\(id)"
            )
        }
    }

    func cancelReminder(identifier: String) {
        UNUserNotificationCenter.current()
            .removePendingNotificationRequests(withIdentifiers: [identifier])
    }
}

WidgetKit

Timeline Provider

import WidgetKit
import SwiftUI

// Widget timeline entry
struct NutritionEntry: TimelineEntry {
    let date: Date
    let calories: Int
    let goal: Int
}

// Widget provider
struct NutritionProvider: TimelineProvider {
    func placeholder(in context: Context) -> NutritionEntry {
        NutritionEntry(date: Date(), calories: 1200, goal: 2000)
    }

    func getSnapshot(in context: Context, completion: @escaping (NutritionEntry) -> Void) {
        let entry = NutritionEntry(date: Date(), calories: 1200, goal: 2000)
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<NutritionEntry>) -> Void) {
        // Fetch real data from shared container
        let userDefaults = UserDefaults(suiteName: "group.com.yourapp")
        let calories = userDefaults?.integer(forKey: "todayCalories") ?? 0
        let goal = userDefaults?.integer(forKey: "calorieGoal") ?? 2000

        let entry = NutritionEntry(date: Date(), calories: calories, goal: goal)

        // Refresh every 30 minutes
        let nextUpdate = Calendar.current.date(byAdding: .minute, value: 30, to: Date())!
        let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))

        completion(timeline)
    }
}

Widget View

struct NutritionWidgetView: View {
    let entry: NutritionEntry

    var progress: Double {
        Double(entry.calories) / Double(entry.goal)
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text("Today's Nutrition")
                .font(.headline)

            HStack {
                CircularProgressView(progress: progress)
                    .frame(width: 50, height: 50)

                VStack(alignment: .leading) {
                    Text("\(entry.calories)")
                        .font(.title2.bold())
                    Text("of \(entry.goal) kcal")
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
            }
        }
        .padding()
    }
}

// Widget configuration
@main
struct NutritionWidget: Widget {
    let kind: String = "NutritionWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: NutritionProvider()) { entry in
            NutritionWidgetView(entry: entry)
        }
        .configurationDisplayName("Nutrition Progress")
        .description("Track your daily nutrition at a glance")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}

Widget Data Sharing

Widgets run in a separate process and can't access your main app's data directly. Use App Groups to share data via UserDefaults(suiteName:) or a shared Core Data container. Keep widget data updates lightweight - write to the shared container in your main app, and the widget reads it.


Background Tasks

import BackgroundTasks

class BackgroundTaskManager {
    static let shared = BackgroundTaskManager()

    let syncTaskIdentifier = "com.yourapp.sync"
    let refreshTaskIdentifier = "com.yourapp.refresh"

    func registerTasks() {
        BGTaskScheduler.shared.register(
            forTaskWithIdentifier: syncTaskIdentifier,
            using: nil
        ) { task in
            self.handleSyncTask(task as! BGProcessingTask)
        }

        BGTaskScheduler.shared.register(
            forTaskWithIdentifier: refreshTaskIdentifier,
            using: nil
        ) { task in
            self.handleRefreshTask(task as! BGAppRefreshTask)
        }
    }

    func scheduleSync() {
        let request = BGProcessingTaskRequest(identifier: syncTaskIdentifier)
        request.requiresNetworkConnectivity = true
        request.requiresExternalPower = false

        do {
            try BGTaskScheduler.shared.submit(request)
        } catch {
            print("Could not schedule sync: \(error)")
        }
    }

    private func handleRefreshTask(_ task: BGAppRefreshTask) {
        task.expirationHandler = { }

        Task {
            await WidgetCenter.shared.reloadAllTimelines()
            task.setTaskCompleted(success: true)
            scheduleRefresh()  // Always schedule next!
        }
    }
}

Interview Questions

Q1: How do you share data between your app and a widget?

Answer: Use App Groups for shared containers:

  1. Enable App Groups capability in both targets
  2. Use shared UserDefaults or Core Data:
// In main app - save data
let defaults = UserDefaults(suiteName: "group.com.yourapp")
defaults?.set(1500, forKey: "todayCalories")

// In widget - read data
let defaults = UserDefaults(suiteName: "group.com.yourapp")
let calories = defaults?.integer(forKey: "todayCalories") ?? 0

For Core Data, configure the persistent container to use the shared App Group container URL.

Q2: What are the different types of background tasks in iOS?

Answer:

Task TypeDurationUse Case
BGAppRefreshTask~30 secondsQuick updates, refresh data
BGProcessingTaskMinutes (when plugged in)Sync, cleanup, ML training
Background URLSessionUntil completeDownloads, uploads
Location updatesContinuousNavigation, fitness tracking
Push notificationsBriefSilent push triggers
// BGAppRefreshTask - quick updates
let request = BGAppRefreshTaskRequest(identifier: "refresh")
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)

// BGProcessingTask - longer operations
let request = BGProcessingTaskRequest(identifier: "sync")
request.requiresNetworkConnectivity = true

Q3: How does HealthKit authorization work?

Answer: HealthKit uses granular, per-type authorization:

  • Authorization is per data type (steps, glucose, etc.)
  • Read/write permissions are separate
  • User can grant partial permissions
  • For read types, you can't determine if user denied vs never asked
  • Always check before reading; handle denial gracefully
// Check authorization
let status = healthStore.authorizationStatus(for: stepsType)
switch status {
case .sharingAuthorized: // Can write
case .sharingDenied: // User denied
case .notDetermined: // Never asked
}

Common Mistakes

1. Not Checking HealthKit Availability

// ❌ Crashes on iPad
let store = HKHealthStore()
try await store.requestAuthorization(...)

// ✅ Check first
guard HKHealthStore.isHealthDataAvailable() else {
    throw HealthKitError.notAvailable
}

2. Network Calls in Widgets

// ❌ May timeout, unreliable
func getTimeline(...) {
    Task {
        let data = try await api.fetchData()  // Risky
    }
}

// ✅ Use cached data from main app
func getTimeline(...) {
    let defaults = UserDefaults(suiteName: "group.com.app")
    let cachedData = defaults?.data(forKey: "widgetData")
}

3. Forgetting to Schedule Next Background Task

// ❌ Task runs once, never again
private func handleRefreshTask(_ task: BGAppRefreshTask) {
    // Do work...
    task.setTaskCompleted(success: true)
    // Forgot to schedule next!
}

// ✅ Always schedule next
private func handleRefreshTask(_ task: BGAppRefreshTask) {
    // Do work...
    task.setTaskCompleted(success: true)
    scheduleRefresh()  // Schedule next execution
}

Summary

IntegrationKey ConceptGotcha
HealthKitPer-type authorizationNot available on iPad
WidgetKitTimelineProviderRuns in separate process
Push NotificationsUNUserNotificationCenterCheck authorization status
Background TasksBGTaskSchedulerAlways schedule next task
App GroupsShared containerMust enable in both targets

System integrations make your app feel native and extend its usefulness beyond the main screen. Master these APIs to build apps that users love keeping on their home screen.


Part 8 of the iOS Interview Prep series.

← Previous

iOS Testing and Debugging Essentials

Next →

Building Custom SwiftUI Views and Animations