Complete energy optimization API reference - Power Profiler workflows, timer/network/location/background APIs, iOS 26 BGContinuedProcessingTask, MetricKit monitoring, with all WWDC code examples
/plugin marketplace add CharlesWiltgen/Axiom/plugin install axiom@axiom-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Complete API reference for iOS energy optimization, with code examples from WWDC sessions and Apple documentation.
Related skills: energy (decision trees, patterns), energy-diag (troubleshooting)
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.
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
| 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.
// 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)
}
}
// 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()
}
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()
}
}
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()
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()
// 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)
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()
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
}
}
}
}
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()
}
}
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)
}
}
}
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
}
}
}
| 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 |
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
}
}
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()
}
}
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)
}
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()
}
}
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 |
// 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
}
}
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
}
}
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
}
}
// 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
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)
import XCTest
class DiskWriteTests: XCTestCase {
func testDiskWritePerformance() {
measure(metrics: [XCTStorageMetric()]) {
// Code that writes to disk
saveUserData()
}
}
}
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
}
}
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
}
}
}
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)
}
}
}
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)
}
}
View field metrics in Xcode:
Categories shown:
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)")
}
// 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)
}
}
}
// 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"
}
| 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.
| 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+
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.