Claude-skill-registry axiom-energy-ref
Complete energy optimization API reference - Power Profiler workflows, timer/network/location/background APIs, iOS 26 BGContinuedProcessingTask, MetricKit monitoring, with all WWDC code examples
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/axiom-energy" ~/.claude/skills/majiayu000-claude-skill-registry-axiom-energy-ref && rm -rf "$T"
skills/data/axiom-energy/SKILL.mdEnergy 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)
// 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
// 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
import Combine class ViewModel: ObservableObject { private var cancellables = Set<AnyCancellable>() 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:
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()
Event-Driven Alternative to Timers
From Energy Efficiency Guide: Prefer dispatch sources over polling.
// 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
// 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:
// 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
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
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)
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
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 |
|---|---|---|---|
| ~1m | Extreme | Turn-by-turn only |
| ~10m | Very High | Fitness tracking |
| ~10m | High | Precise positioning |
| ~100m | Medium | Store locators |
| ~1km | Low | Weather, general |
| ~3km | Very Low | Regional content |
Part 5: Background Execution APIs
beginBackgroundTask (Short Tasks)
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
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
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.
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 |
|---|---|---|
| Efficient | Lightweight, purpose-driven | Do one thing well |
| Minimal | Keep work to minimum | Don't expand scope |
| Resilient | Save progress, handle expiration | Checkpoint frequently |
| Courteous | Honor preferences | Check Low Power Mode |
| Adaptive | Work with system | Don't fight constraints |
Part 6: Display & GPU Efficiency APIs
Dark Mode Support
// 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:
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
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
// 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
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
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
import Foundation class PowerStateManager { private var cancellables = Set<AnyCancellable>() 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
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
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
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:
- Window → Organizer
- Select your app
- Click "Battery Usage" in sidebar
- 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:
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
// 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
// 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) | | Time-sensitive alerts |
| Low (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+