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:
- Enable App Groups capability in both targets
- 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 Type | Duration | Use Case |
|---|---|---|
| BGAppRefreshTask | ~30 seconds | Quick updates, refresh data |
| BGProcessingTask | Minutes (when plugged in) | Sync, cleanup, ML training |
| Background URLSession | Until complete | Downloads, uploads |
| Location updates | Continuous | Navigation, fitness tracking |
| Push notifications | Brief | Silent 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
| Integration | Key Concept | Gotcha |
|---|---|---|
| HealthKit | Per-type authorization | Not available on iPad |
| WidgetKit | TimelineProvider | Runs in separate process |
| Push Notifications | UNUserNotificationCenter | Check authorization status |
| Background Tasks | BGTaskScheduler | Always schedule next task |
| App Groups | Shared container | Must 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.