--- name: axiom-energy-ref description: Complete energy optimization API reference - Power Profiler workflows, timer/network/location/background APIs, iOS 26 BGContinuedProcessingTask, MetricKit monitoring, with all WWDC code examples license: MIT metadata: version: "1.0.0" --- # Energy Optimization Reference Complete API reference for iOS energy optimization, with code examples from WWDC sessions and Apple documentation. **Related skills**: `axiom-energy` (decision trees, patterns), `axiom-energy-diag` (troubleshooting) --- ## Part 1: Power Profiler Workflow ### Recording a Trace with Instruments #### Tethered Recording (Connected to Mac) ``` 1. Connect iPhone wirelessly to Xcode - Xcode → Window → Devices and Simulators - Enable "Connect via network" for your device 2. Profile your app - Xcode → Product → Profile (Cmd+I) - Select Blank template - Click "+" → Add "Power Profiler" - Optionally add "CPU Profiler" for correlation 3. Record - Select your app from target dropdown - Click Record (red button) - Use app normally for 2-3 minutes - Click Stop 4. Analyze - Expand Power Profiler track - Examine per-app lanes: CPU, GPU, Display, Network ``` **Important**: Use wireless debugging. When device is charging via cable, system power usage shows 0. #### On-Device Recording (Without Mac) From WWDC25-226: Capture traces in real-world conditions. ``` 1. Enable Developer Mode Settings → Privacy & Security → Developer Mode → Enable 2. Enable Performance Trace Settings → Developer → Performance Trace → Enable Set tracing mode to "Power Profiler" Toggle ON your app in the app list 3. Add Control Center shortcut Control Center → Tap "+" → Add a Control → Performance Trace 4. Record Swipe down → Tap Performance Trace icon → Start Use app (can record up to 10 hours) Tap Performance Trace icon → Stop 5. Share trace Settings → Developer → Performance Trace Tap Share button next to trace file AirDrop to Mac or email to developer ``` ### Interpreting Power Profiler Metrics | Lane | Meaning | What High Values Indicate | |------|---------|--------------------------| | System Power | Overall battery drain rate | General energy consumption | | CPU Power Impact | Processor activity score | Computation, timers, parsing | | GPU Power Impact | Graphics rendering score | Animations, blur, Metal | | Display Power Impact | Screen power usage | Brightness, content type | | Network Power Impact | Radio activity score | Requests, downloads, polling | **Key insight**: Values are scores for comparison, not absolute measurements. Compare before/after traces on the same device. ### Comparing Before/After (Example from WWDC25-226) ```swift // Before optimization: CPU Power Impact = 21 VStack { ForEach(videos) { video in VideoCardView(video: video) } } // After optimization: CPU Power Impact = 4.3 LazyVStack { ForEach(videos) { video in VideoCardView(video: video) } } ``` --- ## Part 2: Timer Efficiency APIs ### NSTimer with Tolerance ```swift // Basic timer with tolerance let timer = Timer.scheduledTimer( withTimeInterval: 1.0, repeats: true ) { [weak self] _ in self?.updateUI() } timer.tolerance = 0.1 // 10% minimum recommended // Add to run loop (if not using scheduledTimer) RunLoop.current.add(timer, forMode: .common) // Always invalidate when done deinit { timer.invalidate() } ``` ### Combine Timer Publisher ```swift import Combine class ViewModel: ObservableObject { private var cancellables = Set() func startPolling() { Timer.publish(every: 1.0, tolerance: 0.1, on: .main, in: .default) .autoconnect() .sink { [weak self] _ in self?.refresh() } .store(in: &cancellables) } func stopPolling() { cancellables.removeAll() } } ``` ### Dispatch Timer Source (Low-Level) From Energy Efficiency Guide: ```swift let queue = DispatchQueue(label: "com.app.timer") let timer = DispatchSource.makeTimerSource(queue: queue) // Set interval with leeway (tolerance) timer.schedule( deadline: .now(), repeating: .seconds(1), leeway: .milliseconds(100) // 10% tolerance ) timer.setEventHandler { [weak self] in self?.performWork() } timer.resume() // Cancel when done timer.cancel() ``` > For DispatchSourceTimer lifecycle safety and crash prevention, see `axiom-timer-patterns`. ### Event-Driven Alternative to Timers From Energy Efficiency Guide: Prefer dispatch sources over polling. ```swift // Monitor file changes instead of polling let fileDescriptor = open(filePath.path, O_EVTONLY) let source = DispatchSource.makeFileSystemObjectSource( fileDescriptor: fileDescriptor, eventMask: [.write, .delete], queue: .main ) source.setEventHandler { [weak self] in self?.handleFileChange() } source.setCancelHandler { close(fileDescriptor) } source.resume() ``` --- ## Part 3: Network Efficiency APIs ### URLSession Configuration ```swift // Standard configuration with energy-conscious settings let config = URLSessionConfiguration.default config.waitsForConnectivity = true // Don't fail immediately config.allowsExpensiveNetworkAccess = false // Prefer WiFi config.allowsConstrainedNetworkAccess = false // Respect Low Data Mode let session = URLSession(configuration: config) ``` ### Discretionary Background Downloads From WWDC22-10083: ```swift // Background session for non-urgent downloads let config = URLSessionConfiguration.background( withIdentifier: "com.app.downloads" ) config.isDiscretionary = true // System chooses optimal time config.sessionSendsLaunchEvents = true // Set timeouts config.timeoutIntervalForResource = 24 * 60 * 60 // 24 hours config.timeoutIntervalForRequest = 60 let session = URLSession(configuration: config, delegate: self, delegateQueue: nil) // Create download task with scheduling hints let task = session.downloadTask(with: url) task.earliestBeginDate = Date(timeIntervalSinceNow: 2 * 60 * 60) // 2 hours from now task.countOfBytesClientExpectsToSend = 200 // Small request task.countOfBytesClientExpectsToReceive = 500_000 // 500KB response task.resume() ``` ### Background Session Delegate ```swift class DownloadDelegate: NSObject, URLSessionDownloadDelegate { func urlSession( _ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL ) { // Move file from temp location let destination = FileManager.default.urls( for: .documentDirectory, in: .userDomainMask )[0].appendingPathComponent("downloaded.data") try? FileManager.default.moveItem(at: location, to: destination) } func urlSessionDidFinishEvents( forBackgroundURLSession session: URLSession ) { // Notify app delegate to call completion handler DispatchQueue.main.async { if let handler = AppDelegate.shared.backgroundCompletionHandler { handler() AppDelegate.shared.backgroundCompletionHandler = nil } } } } ``` --- ## Part 4: Location Efficiency APIs ### CLLocationManager Configuration ```swift import CoreLocation class LocationService: NSObject, CLLocationManagerDelegate { private let manager = CLLocationManager() func configure() { manager.delegate = self // Use appropriate accuracy manager.desiredAccuracy = kCLLocationAccuracyHundredMeters // Reduce update frequency manager.distanceFilter = 100 // Update every 100 meters // Allow indicator pause when stationary manager.pausesLocationUpdatesAutomatically = true // For background updates (if needed) manager.allowsBackgroundLocationUpdates = true manager.showsBackgroundLocationIndicator = true } func startTracking() { manager.requestWhenInUseAuthorization() manager.startUpdatingLocation() } func startSignificantChangeTracking() { // Much more energy efficient for background manager.startMonitoringSignificantLocationChanges() } func stopTracking() { manager.stopUpdatingLocation() manager.stopMonitoringSignificantLocationChanges() } } ``` ### iOS 26+ CLLocationUpdate (Modern Async API) ```swift import CoreLocation func trackLocation() async throws { for try await update in CLLocationUpdate.liveUpdates() { // Check if device became stationary if update.stationary { // System pauses updates automatically // Consider switching to region monitoring break } if let location = update.location { handleLocation(location) } } } ``` ### CLMonitor for Significant Changes ```swift import CoreLocation func setupRegionMonitoring() async { let monitor = CLMonitor("significant-changes") // Add condition to monitor let condition = CLMonitor.CircularGeographicCondition( center: currentLocation.coordinate, radius: 500 // 500 meter radius ) await monitor.add(condition, identifier: "home-region") // React to events for try await event in monitor.events { switch event.state { case .satisfied: // Entered region handleRegionEntry() case .unsatisfied: // Exited region handleRegionExit() default: break } } } ``` ### Location Accuracy Options | Constant | Accuracy | Battery Impact | Use Case | |----------|----------|----------------|----------| | `kCLLocationAccuracyBestForNavigation` | ~1m | Extreme | Turn-by-turn only | | `kCLLocationAccuracyBest` | ~10m | Very High | Fitness tracking | | `kCLLocationAccuracyNearestTenMeters` | ~10m | High | Precise positioning | | `kCLLocationAccuracyHundredMeters` | ~100m | Medium | Store locators | | `kCLLocationAccuracyKilometer` | ~1km | Low | Weather, general | | `kCLLocationAccuracyThreeKilometers` | ~3km | Very Low | Regional content | --- ## Part 5: Background Execution APIs ### beginBackgroundTask (Short Tasks) ```swift class AppDelegate: UIResponder, UIApplicationDelegate { var backgroundTask: UIBackgroundTaskIdentifier = .invalid func applicationDidEnterBackground(_ application: UIApplication) { backgroundTask = application.beginBackgroundTask(withName: "Save State") { // Expiration handler - clean up self.endBackgroundTask() } // Perform quick work saveState() // End immediately when done endBackgroundTask() } private func endBackgroundTask() { guard backgroundTask != .invalid else { return } UIApplication.shared.endBackgroundTask(backgroundTask) backgroundTask = .invalid } } ``` ### BGAppRefreshTask ```swift import BackgroundTasks // Register at app launch func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { BGTaskScheduler.shared.register( forTaskWithIdentifier: "com.app.refresh", using: nil ) { task in self.handleAppRefresh(task: task as! BGAppRefreshTask) } return true } // Schedule refresh func scheduleAppRefresh() { let request = BGAppRefreshTaskRequest(identifier: "com.app.refresh") request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15 min try? BGTaskScheduler.shared.submit(request) } // Handle refresh func handleAppRefresh(task: BGAppRefreshTask) { scheduleAppRefresh() // Schedule next refresh let fetchTask = Task { do { let hasNewData = try await fetchLatestData() task.setTaskCompleted(success: hasNewData) } catch { task.setTaskCompleted(success: false) } } task.expirationHandler = { fetchTask.cancel() } } ``` ### BGProcessingTask ```swift import BackgroundTasks // Register BGTaskScheduler.shared.register( forTaskWithIdentifier: "com.app.maintenance", using: nil ) { task in self.handleMaintenance(task: task as! BGProcessingTask) } // Schedule with requirements func scheduleMaintenance() { let request = BGProcessingTaskRequest(identifier: "com.app.maintenance") request.requiresNetworkConnectivity = true request.requiresExternalPower = true // Only when charging try? BGTaskScheduler.shared.submit(request) } // Handle func handleMaintenance(task: BGProcessingTask) { let operation = MaintenanceOperation() task.expirationHandler = { operation.cancel() } operation.completionBlock = { task.setTaskCompleted(success: !operation.isCancelled) } OperationQueue.main.addOperation(operation) } ``` ### iOS 26+ BGContinuedProcessingTask From WWDC25-227: Continue user-initiated tasks with system UI. ```swift import BackgroundTasks // Info.plist: Add identifier to BGTaskSchedulerPermittedIdentifiers // "com.app.export" or "com.app.exports.*" for wildcards // Register handler (can be dynamic, not just at launch) func setupExportHandler() { BGTaskScheduler.shared.register("com.app.export") { task in let continuedTask = task as! BGContinuedProcessingTask var shouldContinue = true continuedTask.expirationHandler = { shouldContinue = false } // Report progress continuedTask.progress.totalUnitCount = 100 continuedTask.progress.completedUnitCount = 0 // Perform work for i in 0..<100 { guard shouldContinue else { break } performExportStep(i) continuedTask.progress.completedUnitCount = Int64(i + 1) } continuedTask.setTaskCompleted(success: shouldContinue) } } // Submit request func startExport() { let request = BGContinuedProcessingTaskRequest( identifier: "com.app.export", title: "Exporting Photos", subtitle: "0 of 100 photos" ) // Submission strategy request.strategy = .fail // Fail if can't start immediately // or default: queue if can't start do { try BGTaskScheduler.shared.submit(request) } catch { // Handle submission failure showExportNotAvailable() } } ``` ### EMRCA Principles (from WWDC25-227) Background tasks must be: | Principle | Meaning | Implementation | |-----------|---------|----------------| | **E**fficient | Lightweight, purpose-driven | Do one thing well | | **M**inimal | Keep work to minimum | Don't expand scope | | **R**esilient | Save progress, handle expiration | Checkpoint frequently | | **C**ourteous | Honor preferences | Check Low Power Mode | | **A**daptive | Work with system | Don't fight constraints | --- ## Part 6: Display & GPU Efficiency APIs ### Dark Mode Support ```swift // Check current appearance let isDarkMode = traitCollection.userInterfaceStyle == .dark // React to appearance changes override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { updateColorsForAppearance() } } // Use dynamic colors let dynamicColor = UIColor { traitCollection in switch traitCollection.userInterfaceStyle { case .dark: return UIColor.black // OLED: True black = pixels off = 0 power default: return UIColor.white } } ``` ### Frame Rate Control with CADisplayLink From WWDC22-10083: ```swift class AnimationController { private var displayLink: CADisplayLink? func startAnimation() { displayLink = CADisplayLink(target: self, selector: #selector(update)) // Control frame rate displayLink?.preferredFrameRateRange = CAFrameRateRange( minimum: 10, // Minimum acceptable maximum: 30, // Maximum needed preferred: 30 // Ideal rate ) displayLink?.add(to: .current, forMode: .default) } @objc private func update(_ displayLink: CADisplayLink) { // Update animation updateAnimationFrame() } func stopAnimation() { displayLink?.invalidate() displayLink = nil } } ``` ### Stop Animations When Not Visible ```swift class AnimatedViewController: UIViewController { private var animator: UIViewPropertyAnimator? override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) startAnimations() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) stopAnimations() // Critical for energy } private func stopAnimations() { animator?.stopAnimation(true) animator = nil } } ``` --- ## Part 7: Disk I/O Efficiency APIs ### Batch Writes ```swift // BAD: Multiple small writes for item in items { let data = try JSONEncoder().encode(item) try data.write(to: fileURL) // Writes each item separately } // GOOD: Single batched write let allData = try JSONEncoder().encode(items) try allData.write(to: fileURL) // One write operation ``` ### SQLite WAL Mode ```swift import SQLite3 // Enable Write-Ahead Logging var db: OpaquePointer? sqlite3_open(dbPath, &db) var statement: OpaquePointer? sqlite3_prepare_v2(db, "PRAGMA journal_mode=WAL", -1, &statement, nil) sqlite3_step(statement) sqlite3_finalize(statement) ``` ### XCTStorageMetric for Testing ```swift import XCTest class DiskWriteTests: XCTestCase { func testDiskWritePerformance() { measure(metrics: [XCTStorageMetric()]) { // Code that writes to disk saveUserData() } } } ``` --- ## Part 8: Low Power Mode & Thermal Response APIs ### Low Power Mode Detection ```swift import Foundation class PowerStateManager { private var cancellables = Set() init() { // Check initial state updateForPowerState() // Observe changes NotificationCenter.default.publisher( for: .NSProcessInfoPowerStateDidChange ) .sink { [weak self] _ in self?.updateForPowerState() } .store(in: &cancellables) } private func updateForPowerState() { if ProcessInfo.processInfo.isLowPowerModeEnabled { reduceEnergyUsage() } else { restoreNormalOperation() } } private func reduceEnergyUsage() { // Increase timer intervals // Reduce animation frame rates // Defer network requests // Stop location updates if not critical // Reduce refresh frequency } } ``` ### Thermal State Response ```swift import Foundation class ThermalManager { init() { NotificationCenter.default.addObserver( self, selector: #selector(thermalStateChanged), name: ProcessInfo.thermalStateDidChangeNotification, object: nil ) } @objc private func thermalStateChanged() { switch ProcessInfo.processInfo.thermalState { case .nominal: // Normal operation restoreFullFunctionality() case .fair: // Slightly elevated, minor reduction reduceNonEssentialWork() case .serious: // Significant reduction needed suspendBackgroundTasks() reduceAnimationQuality() case .critical: // Maximum reduction minimizeAllActivity() showThermalWarningIfAppropriate() @unknown default: break } } } ``` --- ## Part 9: MetricKit Monitoring APIs ### Basic Setup ```swift import MetricKit class MetricsManager: NSObject, MXMetricManagerSubscriber { static let shared = MetricsManager() func startMonitoring() { MXMetricManager.shared.add(self) } func didReceive(_ payloads: [MXMetricPayload]) { for payload in payloads { processPayload(payload) } } func didReceive(_ payloads: [MXDiagnosticPayload]) { for payload in payloads { processDiagnostic(payload) } } } ``` ### Processing Energy Metrics ```swift func processPayload(_ payload: MXMetricPayload) { // CPU metrics if let cpu = payload.cpuMetrics { let foregroundTime = cpu.cumulativeCPUTime let backgroundTime = cpu.cumulativeCPUInstructions logMetric("cpu_foreground", value: foregroundTime) } // Location metrics if let location = payload.locationActivityMetrics { let backgroundLocationTime = location.cumulativeBackgroundLocationTime logMetric("background_location_seconds", value: backgroundLocationTime) } // Network metrics if let network = payload.networkTransferMetrics { let cellularUpload = network.cumulativeCellularUpload let cellularDownload = network.cumulativeCellularDownload let wifiUpload = network.cumulativeWiFiUpload let wifiDownload = network.cumulativeWiFiDownload logMetric("cellular_upload", value: cellularUpload) logMetric("cellular_download", value: cellularDownload) } // Disk metrics if let disk = payload.diskIOMetrics { let writes = disk.cumulativeLogicalWrites logMetric("disk_writes", value: writes) } // GPU metrics if let gpu = payload.gpuMetrics { let gpuTime = gpu.cumulativeGPUTime logMetric("gpu_time", value: gpuTime) } } ``` ### Xcode Organizer Integration View field metrics in Xcode: 1. Window → Organizer 2. Select your app 3. Click "Battery Usage" in sidebar 4. Compare versions, filter by device/OS Categories shown: - Audio - Networking - Processing (CPU + GPU) - Display - Bluetooth - Location - Camera - Torch - NFC - Other --- ## Part 10: Push Notifications APIs ### Alert Notifications Setup From WWDC20-10095: ```swift import UserNotifications class NotificationManager: NSObject, UNUserNotificationCenterDelegate { func setup() { UNUserNotificationCenter.current().delegate = self UIApplication.shared.registerForRemoteNotifications() } func requestPermission() { UNUserNotificationCenter.current().requestAuthorization( options: [.alert, .sound, .badge] ) { granted, error in print("Permission granted: \(granted)") } } } // AppDelegate func application( _ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data ) { let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() sendTokenToServer(token) } func application( _ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error ) { print("Failed to register: \(error)") } ``` ### Background Push Notifications ```swift // Handle background notification func application( _ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void ) { // Check for content-available flag guard let aps = userInfo["aps"] as? [String: Any], aps["content-available"] as? Int == 1 else { completionHandler(.noData) return } Task { do { let hasNewData = try await fetchLatestContent() completionHandler(hasNewData ? .newData : .noData) } catch { completionHandler(.failed) } } } ``` ### Server Payload Examples ```json // Alert notification (user-visible) { "aps": { "alert": { "title": "New Message", "body": "You have a new message from John" }, "sound": "default", "badge": 1 }, "message_id": "12345" } // Background notification (silent) { "aps": { "content-available": 1 }, "update_type": "new_content" } ``` ### Push Priority Headers | Priority | Header | Use Case | |----------|--------|----------| | High (10) | `apns-priority: 10` | Time-sensitive alerts | | Low (5) | `apns-priority: 5` | Deferrable updates | **Energy tip**: Use priority 5 for all non-urgent notifications. System batches low-priority pushes for energy efficiency. --- ## Troubleshooting Checklist ### Issue: App at Top of Battery Settings - [ ] Run Power Profiler to identify dominant subsystem - [ ] Check for timers without tolerance - [ ] Check for polling patterns - [ ] Check for continuous location - [ ] Check for background audio session - [ ] Verify BGTasks complete promptly ### Issue: Device Gets Hot - [ ] Check GPU Power Impact for sustained high values - [ ] Look for continuous animations - [ ] Check for blur effects over dynamic content - [ ] Verify Metal frame limiting - [ ] Check CPU for tight loops ### Issue: Background Battery Drain - [ ] Audit background modes in Info.plist - [ ] Verify audio session deactivated when not playing - [ ] Check location accuracy and stop calls - [ ] Verify beginBackgroundTask calls end promptly - [ ] Review BGTask scheduling ### Issue: High Cellular Usage - [ ] Check allowsExpensiveNetworkAccess setting - [ ] Verify discretionary flag on background downloads - [ ] Look for polling patterns - [ ] Check for large automatic downloads --- ## Expert Review Checklist ### Timers (10 items) - [ ] Tolerance ≥10% on all timers - [ ] Timers invalidated in deinit - [ ] No timers running when app backgrounded - [ ] Using Combine Timer where possible - [ ] No sub-second intervals without justification - [ ] Event-driven alternatives considered - [ ] No synchronization via timer polling - [ ] Timer invalidated before creating new one - [ ] Repeating timers have clear stop condition - [ ] Background timer usage justified ### Network (10 items) - [ ] waitsForConnectivity = true - [ ] allowsExpensiveNetworkAccess appropriate - [ ] allowsConstrainedNetworkAccess appropriate - [ ] Non-urgent downloads use discretionary - [ ] Push notifications instead of polling - [ ] Requests batched where possible - [ ] Payloads compressed - [ ] Background URLSession for large transfers - [ ] Retry logic has exponential backoff - [ ] Connection reuse via single URLSession ### Location (10 items) - [ ] Accuracy appropriate for use case - [ ] distanceFilter set - [ ] Updates stopped when not needed - [ ] pausesLocationUpdatesAutomatically = true - [ ] Background location only if essential - [ ] Significant-change for background - [ ] CLMonitor for region monitoring - [ ] Location permission matches actual need - [ ] Stationary detection utilized - [ ] Location icon explained to users ### Background Execution (10 items) - [ ] endBackgroundTask called promptly - [ ] Expiration handlers implemented - [ ] BGTasks use requiresExternalPower when possible - [ ] EMRCA principles followed - [ ] Background modes limited to needed - [ ] Audio session deactivated when idle - [ ] Progress saved incrementally - [ ] Tasks complete within time limits - [ ] Low Power Mode checked before heavy work - [ ] Thermal state monitored ### Display/GPU (10 items) - [ ] Dark Mode supported - [ ] Animations stop when view hidden - [ ] Frame rates appropriate for content - [ ] Secondary animations lower priority - [ ] Blur effects minimized - [ ] Metal has frame limiting - [ ] Brightness-independent design - [ ] No hidden animations consuming power - [ ] GPU-intensive work has visibility checks - [ ] ProMotion considered in frame rate decisions --- ## WWDC Session Reference | Session | Year | Topic | |---------|------|-------| | 226 | 2025 | Power Profiler workflow, on-device tracing | | 227 | 2025 | BGContinuedProcessingTask, EMRCA principles | | 10083 | 2022 | Dark Mode, frame rates, deferral | | 10095 | 2020 | Push notifications primer | | 707 | 2019 | Background execution advances | | 417 | 2019 | Battery life, MetricKit | --- **Last Updated**: 2025-12-26 **Platforms**: iOS 26+, iPadOS 26+