- Published on
> Detecting Low Power Mode in SwiftUI and Adapting UI Performance
- Authors

- Name
- Mick MacCallum
- @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.
// Continue_Learning
Building Interactive Glass Controls in SwiftUI
Make your glass elements respond to touch with scaling, shimmer effects, and touch-point illumination using the interactive() modifier and glass button styles.
Creating Morphing Glass Transitions with glassEffectID
The glassEffectID modifier enables glass views to smoothly morph into one another during state changes, creating fluid transitions that feel native to iOS 26.
Coordinating Glass Elements with GlassEffectContainer
When you have multiple glass elements that should blend and animate together, GlassEffectContainer coordinates their rendering for seamless visual results.
// Stay Updated
Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.