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)
-
Connect iPhone wirelessly to Xcode
- Xcode → Window → Devices and Simulators
- Enable "Connect via network" for your device
-
Profile your app
- Xcode → Product → Profile (Cmd+I)
- Select Blank template
- Click "+" → Add "Power Profiler"
- Optionally add "CPU Profiler" for correlation
-
Record
- Select your app from target dropdown
- Click Record (red button)
- Use app normally for 2-3 minutes
- Click Stop
-
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.
-
Enable Developer Mode Settings → Privacy & Security → Developer Mode → Enable
-
Enable Performance Trace Settings → Developer → Performance Trace → Enable Set tracing mode to "Power Profiler" Toggle ON your app in the app list
-
Add Control Center shortcut Control Center → Tap "+" → Add a Control → Performance Trace
-
Record Swipe down → Tap Performance Trace icon → Start Use app (can record up to 10 hours) Tap Performance Trace icon → Stop
-
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()
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.
// 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
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)
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) 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+