home / skills / charleswiltgen / axiom / axiom-energy-ref

This skill guides iOS energy optimization with Power Profiler workflows, timers, network, location, and background APIs, using WWDC examples to reduce power

npx playbooks add skill charleswiltgen/axiom --skill axiom-energy-ref

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
27.6 KB
---
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<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:

```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()
```

### 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<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

```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+

Overview

This skill is a compact, battle-tested reference for energy optimization across modern xOS platforms. It consolidates Power Profiler workflows, timer/network/location/background APIs, iOS 26 BGContinuedProcessingTask, and MetricKit monitoring with WWDC and Apple example patterns. Use it as a checklist and quick code resource when reducing battery and thermal impact in apps.

How this skill works

The skill describes how to record and interpret Power Profiler traces both tethered and on-device, and maps profiler lanes to common causes. It presents efficient timer patterns (Timer tolerance, DispatchSource, Combine), network strategies (discretionary background sessions, waitsForConnectivity), and location options (accuracy, distanceFilter, significant-change APIs). It also covers background execution: beginBackgroundTask, BGAppRefresh, BGProcessing, and the iOS 26 BGContinuedProcessingTask with practical submission and handler patterns.

When to use it

  • When you need to measure and compare app power impact before and after changes
  • When replacing polling with event-driven or tolerant timers to save CPU
  • When scheduling downloads or uploads that can be deferred to low-cost windows
  • When choosing location accuracy and update frequency for background scenarios
  • When implementing background work that must run reliably and efficiently (BGAppRefresh, BGProcessing, BGContinuedProcessingTask)

Best practices

  • Record traces on-device (wireless) and compare before/after on the same device; profiler scores are relative
  • Prefer leeway/tolerance on timers (≥10%) and use dispatch sources or event-driven mechanisms instead of frequent polling
  • Use discretionary background URLSession for non-urgent transfers and set earliestBeginDate and expected byte counts
  • Lower location accuracy and increase distanceFilter when precise positioning isn’t required; use significant-change or region monitoring for background
  • Schedule background tasks with appropriate requirements (network, external power) and handle expiration by cancelling work and reporting success/failure

Example use cases

  • Reduce CPU power by switching a list from VStack to LazyVStack to avoid rendering work
  • Replace a 1s polling Timer with a DispatchSource timer using 10% leeway or convert to event-driven file system monitoring
  • Schedule large media downloads as discretionary background tasks with earliestBeginDate and isDiscretionary enabled
  • Switch from continuous location updates to significant-change monitoring for background geofencing and battery savings
  • Use BGContinuedProcessingTask to continue a user-initiated export with progress reporting and system-hosted UI on iOS 26+

FAQ

Are Power Profiler lane values absolute energy measurements?

No — they are comparative scores. Use consistent device and conditions to compare before/after traces.

What tolerance should I set on timers?

Use at least 10% tolerance when possible; tolerate batching improves energy efficiency significantly.

When should I use BGProcessing vs BGAppRefresh?

Use BGAppRefresh for short, opportunistic fetches and BGProcessing for longer maintenance tasks that may require network or external power.