Syncing Data with Widget Extension

Does anyone know the best way to sync data between your app and widget extension?


Context

I’m learning how to make widgets for the first time. I have data in my app that I want to show in my widget.

I thought I would store the handful of id’s or values in UserDefaults and pull them out for the widget.

Realized that widgets don’t have access to the same UserDefaults.standard as the main app. So I created an AppGroup and switched to using UserDefaults(suiteName:)

But now that I have added this functionality to my app, my app takes much longer to build and then prints this warning:

Couldn't read values in CFPrefsPlistSource<0x100e60050> (Domain: group.TeamID.BundleId, User: kCFPreferencesAnyUser, ByHost: Yes, Container: (null), Contents Need Refresh: Yes): Using kCFPreferencesAnyUser with a container is only allowed for System Containers, detaching from cfprefsd

I’ve searched all over the apple developer forums and StackOverflow. And I’ve found tons of people hitting the same issue and a lot of answers that don’t seem to help at all.


Things I’ve Tried

  1. Setting the AppGroup name to be group.TeamId.BundleId
  2. Adding the PrivacyInfo.xcprivacy file with Privacy Accessed API Type as UserDefaults and Privacy Accessed API Reasons as C617.1: Inside app or group container, per documentation
  3. Wiping the device

I found a lot of documentation saying it’s an erroneous error. But I’m skeptical because ever since adding the UserDefaults, my app has taken longer to build.


Code Snippet

From inside app code


import Foundation
import HealthKit
import SwiftUI
import WidgetKit

class HealthDataManager: ObservableObject {
    var healthStore: HKHealthStore?
    
    @Published private(set) var healthDataAvailable: Bool
    
    @AppStorage(GoalStrings.stepProgress, store: UserDefaults(suiteName: AppStrings.appGroupName)) private(set) var steps: Int = 0
    
    @AppStorage(GoalStrings.distanceProgress, store: UserDefaults(suiteName: AppStrings.appGroupName)) private(set) var distanceTraveled: Double = 0
    
    @Published private(set) var distanceUnit: DistanceUnit {
        didSet {
            distanceUnitId = distanceUnit.id
            fetchDistance()
        }
    }
    @AppStorage(UnitStrings.unitId, store: UserDefaults(suiteName: AppStrings.appGroupName)) private var distanceUnitId = 0
    
    init() {
        let userDefaultUnit = UserDefaults(suiteName: AppStrings.appGroupName)?.string(forKey: UnitStrings.unitName)
        distanceUnit = distanceUnits.first{$0.displayName == userDefaultUnit} ?? distanceUnits[0]
        
        healthDataAvailable = HKHealthStore.isHealthDataAvailable()
        if (!healthDataAvailable) { return }
        
        healthStore = HKHealthStore()
        setupHealthQueries()
    }
    
    func setupHealthQueries() {
        let steps = HKQuantityType(.stepCount)
        let distance = HKQuantityType(.distanceWalkingRunning)
        let quantityTypeSet: Set = [steps, distance]
            
        Task {
            do {
                try await healthStore!.requestAuthorization(toShare: [], read: quantityTypeSet)
                
                fetchSteps()
                fetchDistance()
                // Setup Observer for Steps
                if let stepCountType = HKObjectType.quantityType(forIdentifier: .stepCount) {
                    let stepObserverQuery = HKObserverQuery(sampleType: stepCountType, predicate: nil) { (query, completionHandler, errorOrNil) in
                        if let error = errorOrNil {
                            print("Error in stepObserverQuery: \(error)")
                            return
                        }
                        self.fetchSteps()
                    }
                    healthStore!.execute(stepObserverQuery)
                }
                // Setup Observer for Distance
                if let distanceType = HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning) {
                    let distanceObserverQuery = HKObserverQuery(sampleType: distanceType, predicate: nil) { (query, completionHandler, errorOrNil) in
                        if let error = errorOrNil {
                            print("Error in distanceObserverQuery: \(error)")
                            return
                        }
                        self.fetchDistance()
                    }
                    healthStore!.execute(distanceObserverQuery)
                }
            } catch {
                print("Error getting data from healthStore")
            }
        }
    }
    
    func fetchSteps() {
        let predicate = HKQuery.predicateForSamples(withStart: .startOfDay, end: Date())
        let query = HKStatisticsQuery(quantityType: HKQuantityType(.stepCount), quantitySamplePredicate: predicate) { _, stats, error in
            guard let quantity = stats?.sumQuantity(), error == nil else {
                if (error!.localizedDescription == "No data available for the specified predicate.") {
                    DispatchQueue.main.async {
                        self.steps = 0
                    }
                    return
                }
                print("Error in fetchSteps:")
                print(error ?? "stats.sumQuantity nil")
                return
            }
            DispatchQueue.main.async {
                self.steps = Int(quantity.doubleValue(for: .count()))
            }
        }
        healthStore!.execute(query)
    }
    
    func fetchDistance() {
        let predicate = HKQuery.predicateForSamples(withStart: .startOfDay, end: Date())
        let query = HKStatisticsQuery(quantityType: HKQuantityType(.distanceWalkingRunning), quantitySamplePredicate: predicate) { _, stats, error in
            guard let quantity = stats?.sumQuantity(), error == nil else {
                if (error!.localizedDescription == "No data available for the specified predicate.") {
                    DispatchQueue.main.async {
                        self.distanceTraveled = 0
                    }
                    return
                }
                print("Error in fetchDistance:")
                print(error ?? "stats.sumQuantity nil")
                return
            }
            DispatchQueue.main.async {
                let fetchedValue = quantity.doubleValue(for: self.distanceUnit.healthUnit)
                let formattedDistance = self.distanceUnit.displayName == UnitStrings.kilometers ?  Double(round(100 * fetchedValue) / 100) : Double(round(1 * fetchedValue) / 1)
                self.distanceTraveled = formattedDistance
            }
        }
        healthStore!.execute(query)
        WidgetCenter.shared.reloadAllTimelines()
    }
    
    func changeUnit(id: Int) {
        self.distanceUnit = distanceUnits.first{$0.id == id} ?? self.distanceUnit
        UserDefaults(suiteName: AppStrings.appGroupName)?.setValue(distanceUnit.displayName, forKey: UnitStrings.unitName)
        WidgetCenter.shared.reloadAllTimelines()
    }
}

From widget code


    func createTimeline(date: Date, completion: @escaping (Timeline<SimpleEntry>) -> ()) {
        let currentSteps: Int = UserDefaults(suiteName: AppStrings.appGroupName)?.integer(forKey: GoalStrings.stepProgress) ?? 0
        
        let stepsGoal: Double = UserDefaults(suiteName: AppStrings.appGroupName)?.double(forKey: GoalStrings.stepGoal) ?? 90.9

        let udStepsColorId: Int? = UserDefaults(suiteName: AppStrings.appGroupName)?.integer(forKey: ColorStrings.stepsColorId)
        
        let stepsColor: GoalColor = GoalColors.colorList.first(where: {$0.id == udStepsColorId}) ?? GoalColors.colorList[0]
        
        let currentDistance: Double = UserDefaults(suiteName: AppStrings.appGroupName)?.double(forKey: GoalStrings.distanceProgress) ?? 0
        
        let distanceGoal: Double = UserDefaults(suiteName: AppStrings.appGroupName)?.double(forKey: GoalStrings.distanceGoal) ?? 456.5
        
        let udUnitId: Int? = UserDefaults(suiteName: AppStrings.appGroupName)?.integer(forKey: UnitStrings.unitId)
        let distanceUnit: DistanceUnit = distanceUnits.first{$0.id == udUnitId} ?? distanceUnits[0]
        
        let udDistanceColorId: Int? = UserDefaults(suiteName: AppStrings.appGroupName)?.integer(forKey: ColorStrings.distanceColorId)
        let distanceColor: GoalColor = GoalColors.colorList.first(where: {$0.id == udDistanceColorId}) ?? GoalColors.colorList[0]

        let entry = SimpleEntry(date: date, steps: currentSteps, stepsGoal: Double(stepsGoal), stepsColor: stepsColor, distance: currentDistance, distanceGoal: distanceGoal, unit: distanceUnit, distanceColor: distanceColor)
        
        let timeline = Timeline(entries: [entry], policy: .never)
        completion(timeline)
    }