BS
BleepingSwift
Published on

> Detecting Low Power Mode in SwiftUI and Adapting UI Performance

Authors
  • avatar
    Name
    Mick MacCallum
    Twitter
    @0x7fs

When users enable Low Power Mode on their iPhone, they're signaling that battery life is more important than performance. Respectful apps should detect this state and automatically reduce resource-intensive operations like animations, background refresh, and visual effects.

iOS provides APIs to detect Low Power Mode, but SwiftUI doesn't expose them through the environment system. We'll create a custom environment value to reactively respond to power mode changes throughout your app.

Why This Matters

Low Power Mode disables or reduces:

  • Mail fetch
  • Background app refresh
  • Automatic downloads
  • Some visual effects
  • Auto-lock (goes to 30 seconds)
  • 5G (except for video streaming and large downloads)

Your app should follow the same principle: reduce battery drain when users need it most.

Detecting Low Power Mode

iOS exposes Low Power Mode through ProcessInfo:

import Foundation
import Combine

class PowerModeMonitor: ObservableObject {
    @Published var isLowPowerModeEnabled: Bool

    private var cancellable: AnyCancellable?

    init() {
        // Get initial state
        self.isLowPowerModeEnabled = ProcessInfo.processInfo.isLowPowerModeEnabled

        // Listen for changes
        cancellable = NotificationCenter.default
            .publisher(for: Notification.Name.NSProcessInfoPowerStateDidChange)
            .map { _ in ProcessInfo.processInfo.isLowPowerModeEnabled }
            .assign(to: \.isLowPowerModeEnabled, on: self)
    }
}

Creating a Custom Environment Value

To make Low Power Mode accessible throughout your SwiftUI views, create a custom environment value:

import SwiftUI

private struct LowPowerModeKey: EnvironmentKey {
    static let defaultValue = false
}

extension EnvironmentValues {
    var isLowPowerModeEnabled: Bool {
        get { self[LowPowerModeKey.self] }
        set { self[LowPowerModeKey.self] = newValue }
    }
}

Setting Up in Your App

Inject the power mode monitor at your app's root:

@main
struct MyApp: App {
    @StateObject private var powerModeMonitor = PowerModeMonitor()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.isLowPowerModeEnabled, powerModeMonitor.isLowPowerModeEnabled)
        }
    }
}

Using Low Power Mode in Views

Now you can access Low Power Mode in any view:

struct AdaptiveAnimationView: View {
    @Environment(\.isLowPowerModeEnabled) var isLowPowerModeEnabled

    var body: some View {
        VStack {
            if isLowPowerModeEnabled {
                Image(systemName: "battery.25")
                    .font(.system(size: 60))
                    .foregroundColor(.yellow)
            } else {
                Image(systemName: "bolt.fill")
                    .font(.system(size: 60))
                    .foregroundColor(.green)
                    .symbolEffect(.pulse) // Disable animation in low power
            }

            Text(isLowPowerModeEnabled ? "Low Power Mode" : "Normal Mode")
                .font(.headline)
        }
    }
}

Conditional Animations

Reduce or disable animations when battery is low:

struct AnimatedCardView: View {
    @Environment(\.isLowPowerModeEnabled) var isLowPowerModeEnabled
    @State private var isExpanded = false

    var body: some View {
        VStack {
            CardContent()
        }
        .frame(height: isExpanded ? 300 : 100)
        .animation(
            isLowPowerModeEnabled ? .none : .spring(response: 0.6, dampingFraction: 0.8),
            value: isExpanded
        )
        .onTapGesture {
            isExpanded.toggle()
        }
    }
}

Adaptive Refresh Rates

Reduce how often you refresh data or update UI:

struct LiveDataView: View {
    @Environment(\.isLowPowerModeEnabled) var isLowPowerModeEnabled
    @State private var data: String = "Loading..."

    // Computed refresh interval based on power mode
    var refreshInterval: TimeInterval {
        isLowPowerModeEnabled ? 10.0 : 2.0
    }

    var body: some View {
        VStack {
            Text(data)
                .font(.title)
                .padding()

            Text("Refreshing every \(Int(refreshInterval))s")
                .font(.caption)
                .foregroundColor(.secondary)
        }
        .task {
            await startRefreshLoop()
        }
    }

    func startRefreshLoop() async {
        while !Task.isCancelled {
            await fetchData()
            try? await Task.sleep(nanoseconds: UInt64(refreshInterval * 1_000_000_000))
        }
    }

    func fetchData() async {
        // Simulate data fetch
        data = "Updated at \(Date().formatted(date: .omitted, time: .standard))"
    }
}

Reducing Visual Effects

Disable expensive visual effects like blurs and shadows:

struct AdaptiveBackgroundView: View {
    @Environment(\.isLowPowerModeEnabled) var isLowPowerModeEnabled

    var body: some View {
        ZStack {
            if isLowPowerModeEnabled {
                // Simple solid background
                Color.gray.opacity(0.2)
            } else {
                // Expensive blur effect
                Color.clear
                    .background(.ultraThinMaterial)
            }

            VStack {
                Text("Content")
                    .font(.title)
            }
        }
        .shadow(radius: isLowPowerModeEnabled ? 0 : 10)
    }
}

Throttling Network Requests

Reduce background network activity:

class DataService: ObservableObject {
    @Published var items: [Item] = []
    private var isLowPowerMode = false

    func updatePowerMode(_ isLowPower: Bool) {
        isLowPowerMode = isLowPower
    }

    func fetchItems() async {
        // Skip non-critical background fetches in low power mode
        guard !isLowPowerMode else {
            print("Skipping background fetch - Low Power Mode enabled")
            return
        }

        // Perform fetch
        do {
            let newItems = try await api.fetchItems()
            await MainActor.run {
                self.items = newItems
            }
        } catch {
            print("Fetch error: \(error)")
        }
    }
}

struct DataView: View {
    @StateObject private var dataService = DataService()
    @Environment(\.isLowPowerModeEnabled) var isLowPowerModeEnabled

    var body: some View {
        List(dataService.items) { item in
            ItemRow(item: item)
        }
        .onChange(of: isLowPowerModeEnabled) { _, newValue in
            dataService.updatePowerMode(newValue)
        }
    }
}

Creating an Adaptive View Modifier

Build a reusable modifier for common adaptations:

struct AdaptivePowerMode: ViewModifier {
    @Environment(\.isLowPowerModeEnabled) var isLowPowerModeEnabled

    let normalAnimation: Animation
    let reducedAnimation: Animation

    func body(content: Content) -> some View {
        content
            .animation(
                isLowPowerModeEnabled ? reducedAnimation : normalAnimation,
                value: isLowPowerModeEnabled
            )
    }
}

extension View {
    func adaptiveAnimation(
        normal: Animation = .spring(),
        reduced: Animation = .linear(duration: 0.2)
    ) -> some View {
        modifier(AdaptivePowerMode(
            normalAnimation: normal,
            reducedAnimation: reduced
        ))
    }
}

// Usage
struct MyView: View {
    @State private var scale: CGFloat = 1.0

    var body: some View {
        Circle()
            .scaleEffect(scale)
            .adaptiveAnimation()
            .onTapGesture {
                scale = scale == 1.0 ? 1.5 : 1.0
            }
    }
}

Showing Power Mode Status

Inform users that your app is being battery-conscious:

struct PowerModeIndicator: View {
    @Environment(\.isLowPowerModeEnabled) var isLowPowerModeEnabled

    var body: some View {
        VStack {
            if isLowPowerModeEnabled {
                HStack {
                    Image(systemName: "battery.25")
                    Text("Low Power Mode - Reduced animations")
                        .font(.caption)
                }
                .foregroundColor(.orange)
                .padding(8)
                .background(Color.orange.opacity(0.1))
                .cornerRadius(8)
                .transition(.move(edge: .top).combined(with: .opacity))
            }
        }
        .animation(.easeInOut, value: isLowPowerModeEnabled)
    }
}

Best Practices

What to Reduce:

  • Complex animations and transitions
  • Background refresh frequency
  • Location updates accuracy
  • Non-critical network requests
  • Visual effects (blur, shadows)
  • Particle effects
  • Video auto-play

What to Keep:

  • Core functionality
  • User-initiated actions
  • Critical notifications
  • Accessibility features
  • Data sync (but less frequently)

Testing: Enable Low Power Mode on your device (Settings > Battery > Low Power Mode) and verify your app's behavior.

Complete Example

Here's a full example showing multiple adaptations:

struct AdaptiveDashboard: View {
    @Environment(\.isLowPowerModeEnabled) var isLowPowerModeEnabled
    @State private var stats: DashboardStats?

    var refreshInterval: TimeInterval {
        isLowPowerModeEnabled ? 30.0 : 5.0
    }

    var body: some View {
        ScrollView {
            VStack(spacing: 20) {
                PowerModeIndicator()

                // Stats cards
                if let stats = stats {
                    HStack(spacing: 16) {
                        StatCard(title: "Revenue", value: stats.revenue)
                        StatCard(title: "Users", value: "\(stats.users)")
                    }
                    .animation(
                        isLowPowerModeEnabled ? .none : .spring(),
                        value: stats
                    )
                }

                // Chart with conditional effects
                ChartView(data: stats?.chartData ?? [])
                    .shadow(radius: isLowPowerModeEnabled ? 0 : 5)
            }
            .padding()
        }
        .background(
            isLowPowerModeEnabled
                ? Color.gray.opacity(0.05)
                : Color.clear.background(.ultraThinMaterial)
        )
        .task {
            await refreshData()
        }
    }

    func refreshData() async {
        while !Task.isCancelled {
            stats = await fetchDashboardStats()
            try? await Task.sleep(nanoseconds: UInt64(refreshInterval * 1_000_000_000))
        }
    }
}

By respecting Low Power Mode, your app becomes a better iOS citizen, preserving battery life when users need it most while still providing full functionality.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.