This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
1104 lines
28 KiB
Markdown
1104 lines
28 KiB
Markdown
---
|
|
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()
|
|
```
|
|
|
|
> For DispatchSourceTimer lifecycle safety and crash prevention, see `axiom-timer-patterns`.
|
|
|
|
### Event-Driven Alternative to Timers
|
|
|
|
From Energy Efficiency Guide: Prefer dispatch sources over polling.
|
|
|
|
```swift
|
|
// Monitor file changes instead of polling
|
|
let fileDescriptor = open(filePath.path, O_EVTONLY)
|
|
let source = DispatchSource.makeFileSystemObjectSource(
|
|
fileDescriptor: fileDescriptor,
|
|
eventMask: [.write, .delete],
|
|
queue: .main
|
|
)
|
|
|
|
source.setEventHandler { [weak self] in
|
|
self?.handleFileChange()
|
|
}
|
|
|
|
source.setCancelHandler {
|
|
close(fileDescriptor)
|
|
}
|
|
|
|
source.resume()
|
|
```
|
|
|
|
---
|
|
|
|
## Part 3: Network Efficiency APIs
|
|
|
|
### URLSession Configuration
|
|
|
|
```swift
|
|
// Standard configuration with energy-conscious settings
|
|
let config = URLSessionConfiguration.default
|
|
config.waitsForConnectivity = true // Don't fail immediately
|
|
config.allowsExpensiveNetworkAccess = false // Prefer WiFi
|
|
config.allowsConstrainedNetworkAccess = false // Respect Low Data Mode
|
|
|
|
let session = URLSession(configuration: config)
|
|
```
|
|
|
|
### Discretionary Background Downloads
|
|
|
|
From WWDC22-10083:
|
|
|
|
```swift
|
|
// Background session for non-urgent downloads
|
|
let config = URLSessionConfiguration.background(
|
|
withIdentifier: "com.app.downloads"
|
|
)
|
|
config.isDiscretionary = true // System chooses optimal time
|
|
config.sessionSendsLaunchEvents = true
|
|
|
|
// Set timeouts
|
|
config.timeoutIntervalForResource = 24 * 60 * 60 // 24 hours
|
|
config.timeoutIntervalForRequest = 60
|
|
|
|
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
|
|
|
|
// Create download task with scheduling hints
|
|
let task = session.downloadTask(with: url)
|
|
task.earliestBeginDate = Date(timeIntervalSinceNow: 2 * 60 * 60) // 2 hours from now
|
|
task.countOfBytesClientExpectsToSend = 200 // Small request
|
|
task.countOfBytesClientExpectsToReceive = 500_000 // 500KB response
|
|
|
|
task.resume()
|
|
```
|
|
|
|
### Background Session Delegate
|
|
|
|
```swift
|
|
class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
|
|
func urlSession(
|
|
_ session: URLSession,
|
|
downloadTask: URLSessionDownloadTask,
|
|
didFinishDownloadingTo location: URL
|
|
) {
|
|
// Move file from temp location
|
|
let destination = FileManager.default.urls(
|
|
for: .documentDirectory,
|
|
in: .userDomainMask
|
|
)[0].appendingPathComponent("downloaded.data")
|
|
|
|
try? FileManager.default.moveItem(at: location, to: destination)
|
|
}
|
|
|
|
func urlSessionDidFinishEvents(
|
|
forBackgroundURLSession session: URLSession
|
|
) {
|
|
// Notify app delegate to call completion handler
|
|
DispatchQueue.main.async {
|
|
if let handler = AppDelegate.shared.backgroundCompletionHandler {
|
|
handler()
|
|
AppDelegate.shared.backgroundCompletionHandler = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Part 4: Location Efficiency APIs
|
|
|
|
### CLLocationManager Configuration
|
|
|
|
```swift
|
|
import CoreLocation
|
|
|
|
class LocationService: NSObject, CLLocationManagerDelegate {
|
|
private let manager = CLLocationManager()
|
|
|
|
func configure() {
|
|
manager.delegate = self
|
|
|
|
// Use appropriate accuracy
|
|
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
|
|
|
|
// Reduce update frequency
|
|
manager.distanceFilter = 100 // Update every 100 meters
|
|
|
|
// Allow indicator pause when stationary
|
|
manager.pausesLocationUpdatesAutomatically = true
|
|
|
|
// For background updates (if needed)
|
|
manager.allowsBackgroundLocationUpdates = true
|
|
manager.showsBackgroundLocationIndicator = true
|
|
}
|
|
|
|
func startTracking() {
|
|
manager.requestWhenInUseAuthorization()
|
|
manager.startUpdatingLocation()
|
|
}
|
|
|
|
func startSignificantChangeTracking() {
|
|
// Much more energy efficient for background
|
|
manager.startMonitoringSignificantLocationChanges()
|
|
}
|
|
|
|
func stopTracking() {
|
|
manager.stopUpdatingLocation()
|
|
manager.stopMonitoringSignificantLocationChanges()
|
|
}
|
|
}
|
|
```
|
|
|
|
### iOS 26+ CLLocationUpdate (Modern Async API)
|
|
|
|
```swift
|
|
import CoreLocation
|
|
|
|
func trackLocation() async throws {
|
|
for try await update in CLLocationUpdate.liveUpdates() {
|
|
// Check if device became stationary
|
|
if update.stationary {
|
|
// System pauses updates automatically
|
|
// Consider switching to region monitoring
|
|
break
|
|
}
|
|
|
|
if let location = update.location {
|
|
handleLocation(location)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### CLMonitor for Significant Changes
|
|
|
|
```swift
|
|
import CoreLocation
|
|
|
|
func setupRegionMonitoring() async {
|
|
let monitor = CLMonitor("significant-changes")
|
|
|
|
// Add condition to monitor
|
|
let condition = CLMonitor.CircularGeographicCondition(
|
|
center: currentLocation.coordinate,
|
|
radius: 500 // 500 meter radius
|
|
)
|
|
await monitor.add(condition, identifier: "home-region")
|
|
|
|
// React to events
|
|
for try await event in monitor.events {
|
|
switch event.state {
|
|
case .satisfied:
|
|
// Entered region
|
|
handleRegionEntry()
|
|
case .unsatisfied:
|
|
// Exited region
|
|
handleRegionExit()
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Location Accuracy Options
|
|
|
|
| Constant | Accuracy | Battery Impact | Use Case |
|
|
|----------|----------|----------------|----------|
|
|
| `kCLLocationAccuracyBestForNavigation` | ~1m | Extreme | Turn-by-turn only |
|
|
| `kCLLocationAccuracyBest` | ~10m | Very High | Fitness tracking |
|
|
| `kCLLocationAccuracyNearestTenMeters` | ~10m | High | Precise positioning |
|
|
| `kCLLocationAccuracyHundredMeters` | ~100m | Medium | Store locators |
|
|
| `kCLLocationAccuracyKilometer` | ~1km | Low | Weather, general |
|
|
| `kCLLocationAccuracyThreeKilometers` | ~3km | Very Low | Regional content |
|
|
|
|
---
|
|
|
|
## Part 5: Background Execution APIs
|
|
|
|
### beginBackgroundTask (Short Tasks)
|
|
|
|
```swift
|
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
|
var backgroundTask: UIBackgroundTaskIdentifier = .invalid
|
|
|
|
func applicationDidEnterBackground(_ application: UIApplication) {
|
|
backgroundTask = application.beginBackgroundTask(withName: "Save State") {
|
|
// Expiration handler - clean up
|
|
self.endBackgroundTask()
|
|
}
|
|
|
|
// Perform quick work
|
|
saveState()
|
|
|
|
// End immediately when done
|
|
endBackgroundTask()
|
|
}
|
|
|
|
private func endBackgroundTask() {
|
|
guard backgroundTask != .invalid else { return }
|
|
UIApplication.shared.endBackgroundTask(backgroundTask)
|
|
backgroundTask = .invalid
|
|
}
|
|
}
|
|
```
|
|
|
|
### BGAppRefreshTask
|
|
|
|
```swift
|
|
import BackgroundTasks
|
|
|
|
// Register at app launch
|
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
|
|
|
BGTaskScheduler.shared.register(
|
|
forTaskWithIdentifier: "com.app.refresh",
|
|
using: nil
|
|
) { task in
|
|
self.handleAppRefresh(task: task as! BGAppRefreshTask)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// Schedule refresh
|
|
func scheduleAppRefresh() {
|
|
let request = BGAppRefreshTaskRequest(identifier: "com.app.refresh")
|
|
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15 min
|
|
|
|
try? BGTaskScheduler.shared.submit(request)
|
|
}
|
|
|
|
// Handle refresh
|
|
func handleAppRefresh(task: BGAppRefreshTask) {
|
|
scheduleAppRefresh() // Schedule next refresh
|
|
|
|
let fetchTask = Task {
|
|
do {
|
|
let hasNewData = try await fetchLatestData()
|
|
task.setTaskCompleted(success: hasNewData)
|
|
} catch {
|
|
task.setTaskCompleted(success: false)
|
|
}
|
|
}
|
|
|
|
task.expirationHandler = {
|
|
fetchTask.cancel()
|
|
}
|
|
}
|
|
```
|
|
|
|
### BGProcessingTask
|
|
|
|
```swift
|
|
import BackgroundTasks
|
|
|
|
// Register
|
|
BGTaskScheduler.shared.register(
|
|
forTaskWithIdentifier: "com.app.maintenance",
|
|
using: nil
|
|
) { task in
|
|
self.handleMaintenance(task: task as! BGProcessingTask)
|
|
}
|
|
|
|
// Schedule with requirements
|
|
func scheduleMaintenance() {
|
|
let request = BGProcessingTaskRequest(identifier: "com.app.maintenance")
|
|
request.requiresNetworkConnectivity = true
|
|
request.requiresExternalPower = true // Only when charging
|
|
|
|
try? BGTaskScheduler.shared.submit(request)
|
|
}
|
|
|
|
// Handle
|
|
func handleMaintenance(task: BGProcessingTask) {
|
|
let operation = MaintenanceOperation()
|
|
|
|
task.expirationHandler = {
|
|
operation.cancel()
|
|
}
|
|
|
|
operation.completionBlock = {
|
|
task.setTaskCompleted(success: !operation.isCancelled)
|
|
}
|
|
|
|
OperationQueue.main.addOperation(operation)
|
|
}
|
|
```
|
|
|
|
### iOS 26+ BGContinuedProcessingTask
|
|
|
|
From WWDC25-227: Continue user-initiated tasks with system UI.
|
|
|
|
```swift
|
|
import BackgroundTasks
|
|
|
|
// Info.plist: Add identifier to BGTaskSchedulerPermittedIdentifiers
|
|
// "com.app.export" or "com.app.exports.*" for wildcards
|
|
|
|
// Register handler (can be dynamic, not just at launch)
|
|
func setupExportHandler() {
|
|
BGTaskScheduler.shared.register("com.app.export") { task in
|
|
let continuedTask = task as! BGContinuedProcessingTask
|
|
|
|
var shouldContinue = true
|
|
continuedTask.expirationHandler = {
|
|
shouldContinue = false
|
|
}
|
|
|
|
// Report progress
|
|
continuedTask.progress.totalUnitCount = 100
|
|
continuedTask.progress.completedUnitCount = 0
|
|
|
|
// Perform work
|
|
for i in 0..<100 {
|
|
guard shouldContinue else { break }
|
|
|
|
performExportStep(i)
|
|
continuedTask.progress.completedUnitCount = Int64(i + 1)
|
|
}
|
|
|
|
continuedTask.setTaskCompleted(success: shouldContinue)
|
|
}
|
|
}
|
|
|
|
// Submit request
|
|
func startExport() {
|
|
let request = BGContinuedProcessingTaskRequest(
|
|
identifier: "com.app.export",
|
|
title: "Exporting Photos",
|
|
subtitle: "0 of 100 photos"
|
|
)
|
|
|
|
// Submission strategy
|
|
request.strategy = .fail // Fail if can't start immediately
|
|
// or default: queue if can't start
|
|
|
|
do {
|
|
try BGTaskScheduler.shared.submit(request)
|
|
} catch {
|
|
// Handle submission failure
|
|
showExportNotAvailable()
|
|
}
|
|
}
|
|
```
|
|
|
|
### EMRCA Principles (from WWDC25-227)
|
|
|
|
Background tasks must be:
|
|
|
|
| Principle | Meaning | Implementation |
|
|
|-----------|---------|----------------|
|
|
| **E**fficient | Lightweight, purpose-driven | Do one thing well |
|
|
| **M**inimal | Keep work to minimum | Don't expand scope |
|
|
| **R**esilient | Save progress, handle expiration | Checkpoint frequently |
|
|
| **C**ourteous | Honor preferences | Check Low Power Mode |
|
|
| **A**daptive | Work with system | Don't fight constraints |
|
|
|
|
---
|
|
|
|
## Part 6: Display & GPU Efficiency APIs
|
|
|
|
### Dark Mode Support
|
|
|
|
```swift
|
|
// Check current appearance
|
|
let isDarkMode = traitCollection.userInterfaceStyle == .dark
|
|
|
|
// React to appearance changes
|
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
super.traitCollectionDidChange(previousTraitCollection)
|
|
|
|
if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
|
|
updateColorsForAppearance()
|
|
}
|
|
}
|
|
|
|
// Use dynamic colors
|
|
let dynamicColor = UIColor { traitCollection in
|
|
switch traitCollection.userInterfaceStyle {
|
|
case .dark:
|
|
return UIColor.black // OLED: True black = pixels off = 0 power
|
|
default:
|
|
return UIColor.white
|
|
}
|
|
}
|
|
```
|
|
|
|
### Frame Rate Control with CADisplayLink
|
|
|
|
From WWDC22-10083:
|
|
|
|
```swift
|
|
class AnimationController {
|
|
private var displayLink: CADisplayLink?
|
|
|
|
func startAnimation() {
|
|
displayLink = CADisplayLink(target: self, selector: #selector(update))
|
|
|
|
// Control frame rate
|
|
displayLink?.preferredFrameRateRange = CAFrameRateRange(
|
|
minimum: 10, // Minimum acceptable
|
|
maximum: 30, // Maximum needed
|
|
preferred: 30 // Ideal rate
|
|
)
|
|
|
|
displayLink?.add(to: .current, forMode: .default)
|
|
}
|
|
|
|
@objc private func update(_ displayLink: CADisplayLink) {
|
|
// Update animation
|
|
updateAnimationFrame()
|
|
}
|
|
|
|
func stopAnimation() {
|
|
displayLink?.invalidate()
|
|
displayLink = nil
|
|
}
|
|
}
|
|
```
|
|
|
|
### Stop Animations When Not Visible
|
|
|
|
```swift
|
|
class AnimatedViewController: UIViewController {
|
|
private var animator: UIViewPropertyAnimator?
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
startAnimations()
|
|
}
|
|
|
|
override func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
stopAnimations() // Critical for energy
|
|
}
|
|
|
|
private func stopAnimations() {
|
|
animator?.stopAnimation(true)
|
|
animator = nil
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Part 7: Disk I/O Efficiency APIs
|
|
|
|
### Batch Writes
|
|
|
|
```swift
|
|
// BAD: Multiple small writes
|
|
for item in items {
|
|
let data = try JSONEncoder().encode(item)
|
|
try data.write(to: fileURL) // Writes each item separately
|
|
}
|
|
|
|
// GOOD: Single batched write
|
|
let allData = try JSONEncoder().encode(items)
|
|
try allData.write(to: fileURL) // One write operation
|
|
```
|
|
|
|
### SQLite WAL Mode
|
|
|
|
```swift
|
|
import SQLite3
|
|
|
|
// Enable Write-Ahead Logging
|
|
var db: OpaquePointer?
|
|
sqlite3_open(dbPath, &db)
|
|
|
|
var statement: OpaquePointer?
|
|
sqlite3_prepare_v2(db, "PRAGMA journal_mode=WAL", -1, &statement, nil)
|
|
sqlite3_step(statement)
|
|
sqlite3_finalize(statement)
|
|
```
|
|
|
|
### XCTStorageMetric for Testing
|
|
|
|
```swift
|
|
import XCTest
|
|
|
|
class DiskWriteTests: XCTestCase {
|
|
func testDiskWritePerformance() {
|
|
measure(metrics: [XCTStorageMetric()]) {
|
|
// Code that writes to disk
|
|
saveUserData()
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Part 8: Low Power Mode & Thermal Response APIs
|
|
|
|
### Low Power Mode Detection
|
|
|
|
```swift
|
|
import Foundation
|
|
|
|
class PowerStateManager {
|
|
private var cancellables = Set<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+
|