Get the FREE Ultimate OpenClaw Setup Guide →

ax-energy-ref

npx machina-cli add skill Kasempiternal/axiom-v2/ax-energy-ref --openclaw
Files (1)
SKILL.md
19.8 KB

Energy API Reference

Quick Patterns

Timer APIs

// NSTimer with tolerance
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
    self?.updateUI()
}
timer.tolerance = 0.1  // 10% minimum

// Combine Timer
Timer.publish(every: 1.0, tolerance: 0.1, on: .main, in: .default)
    .autoconnect()
    .sink { [weak self] _ in self?.refresh() }
    .store(in: &cancellables)

// DispatchSource timer with leeway
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now(), repeating: .seconds(1), leeway: .milliseconds(100))
timer.setEventHandler { [weak self] in self?.performWork() }
timer.resume()

URLSession Energy-Conscious Config

let config = URLSessionConfiguration.default
config.waitsForConnectivity = true
config.allowsExpensiveNetworkAccess = false
config.allowsConstrainedNetworkAccess = false
let session = URLSession(configuration: config)

Discretionary Background Download

let config = URLSessionConfiguration.background(withIdentifier: "com.app.downloads")
config.isDiscretionary = true
config.sessionSendsLaunchEvents = true
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)

let task = session.downloadTask(with: url)
task.earliestBeginDate = Date(timeIntervalSinceNow: 2 * 60 * 60)
task.countOfBytesClientExpectsToSend = 200
task.countOfBytesClientExpectsToReceive = 500_000
task.resume()

Location Accuracy Options

ConstantAccuracyBattery ImpactUse Case
kCLLocationAccuracyBestForNavigation~1mExtremeTurn-by-turn only
kCLLocationAccuracyBest~10mVery HighFitness tracking
kCLLocationAccuracyNearestTenMeters~10mHighPrecise positioning
kCLLocationAccuracyHundredMeters~100mMediumStore locators
kCLLocationAccuracyKilometer~1kmLowWeather, general
kCLLocationAccuracyThreeKilometers~3kmVery LowRegional content

Decision Tree

Need energy API reference?
|
|-- Timer APIs -> Part 1
|-- Network efficiency -> Part 2
|-- Location efficiency -> Part 3
|-- Background execution -> Part 4
|-- Display/GPU efficiency -> Part 5
|-- Disk I/O -> Part 6
|-- Low Power Mode / Thermal -> Part 7
|-- MetricKit monitoring -> Part 8
+-- Push notifications -> Part 9

Anti-Patterns

DispatchSource timer without leeway. Forces exact wake, same problem as NSTimer without tolerance.

Background URLSession without discretionary. System treats as urgent, wastes energy on immediate delivery.

CLLocationManager without distanceFilter. Receives update on every tiny movement. Set distanceFilter to reduce frequency.

beginBackgroundTask without endBackgroundTask. Task runs until system expiration (~30s), wasting energy.

Playing silent audio for keep-alive. Anti-pattern. Use proper BGTask APIs instead.


Deep Patterns

Part 1: Timer Efficiency APIs

NSTimer with Tolerance

let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
    self?.updateUI()
}
timer.tolerance = 0.1  // 10% minimum recommended
RunLoop.current.add(timer, forMode: .common)

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

DispatchSource Timer (Low-Level)

let queue = DispatchQueue(label: "com.app.timer")
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now(), repeating: .seconds(1), leeway: .milliseconds(100))
timer.setEventHandler { [weak self] in self?.performWork() }
timer.resume()
timer.cancel()  // When done

Event-Driven Alternative (File Monitoring)

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 2: Network Efficiency APIs

URLSession Configuration

// Energy-conscious defaults
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

let config = URLSessionConfiguration.background(withIdentifier: "com.app.downloads")
config.isDiscretionary = true              // System chooses optimal time
config.sessionSendsLaunchEvents = true
config.timeoutIntervalForResource = 24 * 60 * 60  // 24 hours
config.timeoutIntervalForRequest = 60

let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)

let task = session.downloadTask(with: url)
task.earliestBeginDate = Date(timeIntervalSinceNow: 2 * 60 * 60)
task.countOfBytesClientExpectsToSend = 200
task.countOfBytesClientExpectsToReceive = 500_000
task.resume()

Background Session Delegate

class DownloadDelegate: NSObject, URLSessionDownloadDelegate {
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
                    didFinishDownloadingTo location: URL) {
        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) {
        DispatchQueue.main.async {
            if let handler = AppDelegate.shared.backgroundCompletionHandler {
                handler()
                AppDelegate.shared.backgroundCompletionHandler = nil
            }
        }
    }
}

Part 3: Location Efficiency APIs

CLLocationManager Configuration

import CoreLocation

class LocationService: NSObject, CLLocationManagerDelegate {
    private let manager = CLLocationManager()

    func configure() {
        manager.delegate = self
        manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
        manager.distanceFilter = 100
        manager.pausesLocationUpdatesAutomatically = true

        // Background (if needed)
        manager.allowsBackgroundLocationUpdates = true
        manager.showsBackgroundLocationIndicator = true
    }

    func startTracking() {
        manager.requestWhenInUseAuthorization()
        manager.startUpdatingLocation()
    }

    func startSignificantChangeTracking() {
        manager.startMonitoringSignificantLocationChanges()
    }

    func stopTracking() {
        manager.stopUpdatingLocation()
        manager.stopMonitoringSignificantLocationChanges()
    }
}

iOS 26+ CLLocationUpdate (Async API)

func trackLocation() async throws {
    for try await update in CLLocationUpdate.liveUpdates() {
        if update.stationary { break }  // System pauses automatically
        if let location = update.location { handleLocation(location) }
    }
}

CLMonitor for Region Monitoring

func setupRegionMonitoring() async {
    let monitor = CLMonitor("significant-changes")
    let condition = CLMonitor.CircularGeographicCondition(
        center: currentLocation.coordinate, radius: 500
    )
    await monitor.add(condition, identifier: "home-region")

    for try await event in monitor.events {
        switch event.state {
        case .satisfied: handleRegionEntry()
        case .unsatisfied: handleRegionExit()
        default: break
        }
    }
}

Part 4: Background Execution APIs

beginBackgroundTask (Short Tasks)

var backgroundTask: UIBackgroundTaskIdentifier = .invalid

func applicationDidEnterBackground(_ application: UIApplication) {
    backgroundTask = application.beginBackgroundTask(withName: "Save State") { [weak self] in
        self?.endBackgroundTask()
    }
    saveState()
    endBackgroundTask()
}

private func endBackgroundTask() {
    guard backgroundTask != .invalid else { return }
    UIApplication.shared.endBackgroundTask(backgroundTask)
    backgroundTask = .invalid
}

BGAppRefreshTask

import BackgroundTasks

// Register at launch
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.app.refresh", using: nil) { task in
    self.handleAppRefresh(task: task as! BGAppRefreshTask)
}

// Schedule
func scheduleAppRefresh() {
    let request = BGAppRefreshTaskRequest(identifier: "com.app.refresh")
    request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
    try? BGTaskScheduler.shared.submit(request)
}

// Handle
func handleAppRefresh(task: BGAppRefreshTask) {
    scheduleAppRefresh()  // Schedule next
    let fetchTask = Task {
        do {
            let hasNewData = try await fetchLatestData()
            task.setTaskCompleted(success: hasNewData)
        } catch { task.setTaskCompleted(success: false) }
    }
    task.expirationHandler = { fetchTask.cancel() }
}

BGProcessingTask

BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.app.maintenance", using: nil) { task in
    self.handleMaintenance(task: task as! BGProcessingTask)
}

func scheduleMaintenance() {
    let request = BGProcessingTaskRequest(identifier: "com.app.maintenance")
    request.requiresNetworkConnectivity = true
    request.requiresExternalPower = true
    try? BGTaskScheduler.shared.submit(request)
}

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

// Info.plist: Add to BGTaskSchedulerPermittedIdentifiers

BGTaskScheduler.shared.register("com.app.export") { task in
    let continuedTask = task as! BGContinuedProcessingTask
    var shouldContinue = true
    continuedTask.expirationHandler = { shouldContinue = false }

    continuedTask.progress.totalUnitCount = 100
    for i in 0..<100 {
        guard shouldContinue else { break }
        performExportStep(i)
        continuedTask.progress.completedUnitCount = Int64(i + 1)
    }
    continuedTask.setTaskCompleted(success: shouldContinue)
}

// Submit
let request = BGContinuedProcessingTaskRequest(
    identifier: "com.app.export",
    title: "Exporting Photos",
    subtitle: "0 of 100 photos"
)
request.strategy = .fail  // Fail if can't start immediately
try? BGTaskScheduler.shared.submit(request)

EMRCA Principles (WWDC25-227)

PrincipleMeaningImplementation
EfficientLightweight, purpose-drivenDo one thing well
MinimalKeep work to minimumDon't expand scope
ResilientSave progress, handle expirationCheckpoint frequently
CourteousHonor preferencesCheck Low Power Mode
AdaptiveWork with systemDon't fight constraints

Part 5: Display & GPU Efficiency APIs

Dark Mode Support

let isDarkMode = traitCollection.userInterfaceStyle == .dark

// Dynamic colors
let dynamicColor = UIColor { traitCollection in
    traitCollection.userInterfaceStyle == .dark ? .black : .white
    // True black on OLED = pixels off = 0 power
}

// React to changes
override func traitCollectionDidChange(_ prev: UITraitCollection?) {
    super.traitCollectionDidChange(prev)
    if traitCollection.hasDifferentColorAppearance(comparedTo: prev) {
        updateColorsForAppearance()
    }
}

Frame Rate Control

class AnimationController {
    private var displayLink: CADisplayLink?

    func startAnimation() {
        displayLink = CADisplayLink(target: self, selector: #selector(update))
        displayLink?.preferredFrameRateRange = CAFrameRateRange(
            minimum: 10, maximum: 30, preferred: 30
        )
        displayLink?.add(to: .current, forMode: .default)
    }

    func stopAnimation() {
        displayLink?.invalidate()
        displayLink = nil
    }
}

Stop Animations When Not Visible

class AnimatedViewController: UIViewController {
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        startAnimations()
    }
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        stopAnimations()  // Critical for energy
    }
}

Part 6: Disk I/O Efficiency APIs

Batch Writes

// BAD: Multiple small writes
for item in items {
    try JSONEncoder().encode(item).write(to: fileURL)
}

// GOOD: Single batched write
try JSONEncoder().encode(items).write(to: fileURL)

SQLite WAL Mode

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

func testDiskWritePerformance() {
    measure(metrics: [XCTStorageMetric()]) { saveUserData() }
}

Part 7: Low Power Mode & Thermal APIs

Low Power Mode Detection

class PowerStateManager {
    private var cancellables = Set<AnyCancellable>()

    init() {
        updateForPowerState()
        NotificationCenter.default.publisher(for: .NSProcessInfoPowerStateDidChange)
            .sink { [weak self] _ in self?.updateForPowerState() }
            .store(in: &cancellables)
    }

    private func updateForPowerState() {
        if ProcessInfo.processInfo.isLowPowerModeEnabled {
            // Increase timer intervals, reduce frame rates
            // Defer network, stop non-critical location
        }
    }
}

Thermal State Response

NotificationCenter.default.addObserver(
    self, selector: #selector(thermalStateChanged),
    name: ProcessInfo.thermalStateDidChangeNotification, object: nil
)

@objc private func thermalStateChanged() {
    switch ProcessInfo.processInfo.thermalState {
    case .nominal: restoreFullFunctionality()
    case .fair: reduceNonEssentialWork()
    case .serious: suspendBackgroundTasks(); reduceAnimationQuality()
    case .critical: minimizeAllActivity()
    @unknown default: break
    }
}

Part 8: MetricKit Energy Monitoring

import MetricKit

class MetricsManager: NSObject, MXMetricManagerSubscriber {
    func startMonitoring() { MXMetricManager.shared.add(self) }

    func didReceive(_ payloads: [MXMetricPayload]) {
        for payload in payloads {
            if let cpu = payload.cpuMetrics {
                logMetric("cpu_foreground", value: cpu.cumulativeCPUTime)
            }
            if let location = payload.locationActivityMetrics {
                logMetric("background_location", value: location.cumulativeBackgroundLocationTime)
            }
            if let network = payload.networkTransferMetrics {
                logMetric("cellular_upload", value: network.cumulativeCellularUpload)
                logMetric("cellular_download", value: network.cumulativeCellularDownload)
            }
            if let disk = payload.diskIOMetrics {
                logMetric("disk_writes", value: disk.cumulativeLogicalWrites)
            }
            if let gpu = payload.gpuMetrics {
                logMetric("gpu_time", value: gpu.cumulativeGPUTime)
            }
        }
    }
}

Xcode Organizer > Battery Usage categories: Audio, Networking, Processing (CPU + GPU), Display, Bluetooth, Location, Camera, Torch, NFC, Other.

Part 9: Push Notification APIs

Registration

import UserNotifications

UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in }
UIApplication.shared.registerForRemoteNotifications()

// AppDelegate
func application(_ app: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken token: Data) {
    let tokenString = token.map { String(format: "%02.2hhx", $0) }.joined()
    sendTokenToServer(tokenString)
}

Background Push Handler

func application(_ application: UIApplication,
                 didReceiveRemoteNotification userInfo: [AnyHashable: Any],
                 fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    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 Payloads

// Alert (user-visible)
{ "aps": { "alert": { "title": "New Message", "body": "From John" }, "sound": "default" } }

// Background (silent)
{ "aps": { "content-available": 1 }, "update_type": "new_content" }

Push Priority

PriorityHeaderUse Case
High (10)apns-priority: 10Time-sensitive alerts
Low (5)apns-priority: 5Deferrable updates (energy efficient)

Power Profiler Recording

Tethered (with Mac)

1. Connect iPhone wirelessly (Xcode > Window > Devices and Simulators > Connect via network)
2. Xcode > Product > Profile (Cmd+I) > Blank template
3. "+" > Add "Power Profiler" (optionally "CPU Profiler")
4. Select app > Record > Use normally 2-3 min > Stop
5. Expand track: CPU, GPU, Display, Network lanes

On-Device (without Mac, WWDC25-226)

1. Settings > Developer Mode > Enable
2. Settings > Developer > Performance Trace > Enable
3. Control Center > Add Performance Trace shortcut
4. Record up to 10 hours > Share via AirDrop

Diagnostics

Expert Review Checklist (50 items)

Timers (10): Tolerance >= 10%, invalidated in deinit, no background timers, Combine preferred, no sub-second without justification, event-driven considered, no sync via polling, invalidated before recreating, clear stop condition, background usage justified.

Network (10): waitsForConnectivity, allowsExpensiveNetworkAccess appropriate, allowsConstrainedNetworkAccess appropriate, discretionary for non-urgent, push not poll, batched requests, compressed payloads, background URLSession for large transfers, exponential backoff, connection reuse.

Location (10): Accuracy appropriate, distanceFilter set, updates stopped when done, pausesLocationUpdatesAutomatically = true, background only if essential, significant-change for background, CLMonitor for regions, permission matches need, stationary detection used, icon explained to users.

Background (10): endBackgroundTask called promptly, expiration handlers implemented, requiresExternalPower when possible, EMRCA followed, modes limited, audio deactivated when idle, progress saved incrementally, within time limits, Low Power Mode checked, thermal monitored.

Display/GPU (10): Dark Mode supported, animations stop when hidden, frame rates appropriate, secondary animations lower priority, blur minimized, Metal frame limiting, brightness-independent design, no hidden animations, GPU visibility checks, ProMotion considered.


Related

WWDC: 2019-417, 2019-707, 2020-10095, 2022-10083, 2025-226, 2025-227

Docs: /documentation/backgroundtasks, /documentation/corelocation, /documentation/metrickit, /documentation/usernotifications

Skills: ax-energy, ax-performance, ax-networking

Source

git clone https://github.com/Kasempiternal/axiom-v2/blob/main/axiom-plugin/skills/ax-energy-ref/SKILL.mdView on GitHub

Overview

ax-energy-ref provides practical patterns to optimize energy use in iOS apps. It covers timer APIs, energy-conscious networking, discretionary background downloads, location accuracy options, and energy monitoring with MetricKit and Power Profiler, plus push notification setup. Using these patterns helps reduce wakeups, limit background work, and profile energy impact.

How This Skill Works

The skill compiles concrete Swift patterns (NSTimer with tolerance, Combine Timer, and DispatchSource timers) and configuration knobs (URLSession waitsForConnectivity, allowsExpensiveNetworkAccess, and allowsConstrainedNetworkAccess). It also demonstrates discretionary background download setup and location accuracy choices, and references MetricKit energy monitoring and BGContinuedProcessingTask guidance for iOS 26. A developer can implement these patterns to minimize energy use while maintaining responsiveness.

When to Use It

  • When you need periodic tasks with reduced wakeups (timers with tolerance or Combine timers)
  • When you want energy-conscious network activity (URLSession energy-conscious config)
  • When background work should be batched and deferred (discretionary downloads)
  • When your app uses location data and you want to balance accuracy with battery life
  • When you need energy visibility and profiling (MetricKit, Power Profiler, BG tasks)

Quick Start

  1. Step 1: Choose the energy patterns (timer tolerance, discretionary downloads, energy-conscious networking)
  2. Step 2: Implement the Swift patterns (NSTimer with tolerance, URLSession config, and DispatchSource timer with leeway)
  3. Step 3: Validate energy impact with MetricKit or Power Profiler and iterate

Best Practices

  • Prefer timer tolerance and leeway to coalesce work and reduce wakeups
  • Use discretionary background downloads to batch network activity
  • Configure URLSession with waitsForConnectivity and restrict expensive/constrained networks
  • Choose appropriate CLLocationManager accuracy and set distanceFilter to limit updates
  • Profile energy with MetricKit/Power Profiler and adjust patterns before release

Example Use Cases

  • Poll a UI dashboard every second using NSTimer with a 0.1s tolerance and invalidate on deinit
  • Background asset syncing using URLSessionConfiguration.background with isDiscretionary = true
  • Discretionary background download with earliestBeginDate to batch across the day
  • Fitness/location track app using kCLLocationAccuracyBestForNavigation for precise routes when needed, with distanceFilter to reduce updates
  • Use MetricKit energy monitoring to identify hotspots and tune timers, downloads, and location updates

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers