Cel programisty: wydajna aplikacja Swift, która szanuje baterię
Programista iOS najczęściej optymalizuje kod pod płynność interfejsu i szybkość działania. Użytkownik widzi jednak coś jeszcze: jak szybko procenty baterii znikają z ekranu. Jeśli aplikacja w Swift działa gładko, ale po kilkunastu minutach używania „zjada” 10–20% baterii, ocena w App Store poleci w dół, a retention dramatycznie spadnie. Kluczowe staje się więc projektowanie architektury, która nie budzi procesora bez potrzeby, nie utrzymuje zasobów w tle na siłę i maksymalnie wykorzystuje mechanizmy oszczędzania energii wbudowane w iOS.
Oszczędzanie energii to nie pojedynczy trik, ale cały zestaw decyzji: od wyboru trybu lokalizacji, przez sposób odświeżania UI, aż po to, jak skonstruowane są żądania sieciowe. Im wcześniej w procesie projektowym uwzględni się te aspekty, tym mniej bolesna będzie późniejsza optymalizacja.
Jak iOS zużywa energię – fundament pod decyzje architektoniczne
Główne źródła zużycia energii w aplikacjach mobilnych
Zanim zacznie się optymalizować konkretny kod w Swift, warto zrozumieć główne kategorie „pożeraczy” baterii w iOS. Pozwala to świadomie wybierać algorytmy, biblioteki i wzorce architektoniczne.
Najważniejsze komponenty sprzętowe i systemowe wpływające na baterię to:
- CPU (procesor) – każdy cykl CPU zużywa energię. Intensywne przetwarzanie, częste wybudzanie, aktywne czekanie w pętlach i zbyt częste timery przekładają się na stałe zużycie.
- GPU (grafika) – złożone hierarchie widoków, kosztowne efekty (cienie, rozmycia, maski), animacje w wysokiej częstotliwości zwiększają obciążenie GPU i CPU, a więc i baterii.
- Sieć (modem komórkowy i Wi‑Fi) – każde wybudzenie modemu, zestawienie połączenia, wiele małych requestów i transmisja dużych danych są bardzo kosztowne energetycznie.
- GPS i inne czujniki (Core Location, Core Motion, Bluetooth) – ciągłe śledzenie pozycji, skanowanie beaconów czy odczyt akcelerometru w wysokiej częstotliwości szybko wyczerpują baterię.
- Pamięć masowa (dysk flash) – intensywne zapisywanie i odczytywanie, szczególnie w małych porcjach, generuje dodatkowe wybudzenia i obciążenie kontrolera pamięci.
Do tego dochodzi warstwa systemowa: zarządzanie procesami w tle, harmonogramowanie zadań, zarządzanie energią przez iOS (tryb Low Power, throttling). Programista nie ma bezpośredniej kontroli nad każdym z tych elementów, natomiast ma ogromny wpływ na częstotliwość i sposób ich użycia.
Co realnie kontroluje programista, a czego nie przeskoczy
Część aspektów jest poza zasięgiem aplikacji: jakość baterii, temperatura urządzenia, inne procesy działające w tle. Nie da się „wymusić” na iOS, żeby nie usypiał aplikacji, gdy użytkownik przełącza się w inne miejsce. Mimo to w ramach API Swift i frameworków Apple istnieje szeroki margines decyzyjności.
Programista realnie kontroluje m.in.:
- Częstotliwość wybudzeń CPU – przez sposób używania
Timer,DispatchSourceTimer, pętli, obserwatorów. - Wzorce dostępu do sieci – batching requestów, caching, użycie
URLSessionConfiguration.background. - Tryby lokalizacji – wybór między ciągłym śledzeniem a np. significant location changes.
- Strategię aktualizacji UI – odświeżanie tylko tego, co się faktycznie zmieniło, unikanie zbędnych
reloadData(). - Planowanie zadań w tle – używanie
BGTaskScheduler, background fetch, notyfikacje push zamiast agresywnego pollingu.
Tego z kolei przeskoczyć się nie da:
- Polityki iOS dotyczącej uśpienia aplikacji w tle i ograniczeń czasowych dla background tasks.
- Wyboru użytkownika, który włączy tryb Low Power i ograniczy tła aplikacji.
- Globalnej polityki systemu co do priorytetyzacji procesów – system może zabić proces, jeśli potrzebuje pamięci lub energii.
Dlatego architektura aplikacji w Swift powinna być projektowana pod założenie, że kod działa „w krótkich oknach czasowych”, maksymalnie dużo deleguje do systemowych mechanizmów (np. push, background transfers) i nie próbuje walczyć z polityką iOS.
Optymalizacja pod wydajność vs pod baterię
Wydajność i energooszczędność często idą w parze, ale nie zawsze. Przykładowo: prefetching dużej ilości danych na starcie może przyspieszyć działanie aplikacji później, ale oznacza duży pik obciążenia CPU i sieci na początku, co uderza w baterię. Z drugiej strony bardzo agresywne „leniwe” ładowanie wszystkiego w małych porcjach może generować zbyt wiele wybudzeń i requestów, co finalnie również jest niekorzystne.
Typowe przykłady rozjazdu celów:
- Prekomputacja vs on-demand – komputacja wszystkiego na start minimalizuje lagi w UI, ale zwiększa czas aktywnego CPU podczas uruchamiania.
- Częste odświeżanie danych – zapewnia świeżość treści, ale oznacza regularne wybudzanie CPU i modemu (polling API).
- Wysoka częstotliwość animacji – wygląda świetnie na nowych urządzeniach, ale generuje stałe obciążenie GPU i CPU.
W projektowaniu pod baterię dochodzi więc jeszcze jeden wymiar: jak często coś się dzieje i czy można to zgrupować, opóźnić albo scedować na system. Typowa strategia to przejście z modelu „ciągłego monitorowania” na „reakcję na zdarzenia” tam, gdzie to możliwe (np. powiadomienia push zamiast pollingu).

Narzędzia do mierzenia zużycia energii: Instruments, Xcode i realne testy
Instruments – Energy Log krok po kroku
Bez pomiaru optymalizacja baterii to zgadywanie. Najważniejszym narzędziem jest tutaj Instruments – Energy Log, który pokazuje, jak aplikacja zachowuje się z punktu widzenia zużycia energii. Uruchomienie jest proste:
- W Xcode wybierz Product > Profile.
- W Instruments wybierz szablon Energy Log.
- Podłącz urządzenie fizyczne (symulator nie oddaje realnych warunków energetycznych).
- Uruchom nagrywanie i wykonaj typowe scenariusze użytkowe (scrollowanie, przełączanie ekranów, pobieranie danych).
Energy Log prezentuje m.in.:
- CPU Usage – procentowe użycie procesora przez proces.
- Wakeups – liczba wybudzeń CPU spowodowanych przez timer, I/O, itp.
- GPU Usage – przybliżone wykorzystanie GPU.
- Network – aktywność sieciowa (uplink/downlink).
- Location – wykorzystanie usług lokalizacyjnych.
- Energy Impact – syntetyczny wskaźnik Apple łączący różne metryki w jedną wartość.
Podczas analizy warto zwracać uwagę na korelację: kiedy rośnie CPU, czy w tym samym czasie skacze aktywność sieciowa? Czy podczas scrollowania listy GPU nie osiąga wysokich wartości? Czy po zamknięciu ekranu z mapą nadal występują piki „Location” i „Network”?
Jak czytać „energy impact” i unikać fałszywych alarmów
„Energy Impact” to uśredniona metryka, która może wprowadzać w błąd, jeśli jest interpretowana bez kontekstu. Wysoki chwilowy „energy impact” w momencie startu aplikacji lub podczas intensywnego zadania (np. eksport wideo) nie jest problemem sam w sobie – problemem jest stałe wysokie obciążenie w normalnej pracy.
Kilka zasad interpretacji:
- Analizuj wykres w czasie, a nie pojedynczy punkt – liczy się obszar pod krzywą, nie krótkotrwały pik.
- Patrz na wzorce – czy co kilka sekund następuje wybudzenie CPU? Czy co chwilę odpala się mały request sieciowy?
- Porównaj różne ekrany – ekran główny aplikacji nie powinien mieć podobnego „energy impact” co ciężki ekran z mapą i wideo.
- Łącz metrykę „Energy Impact” z Wakeups – duża liczba wybudzeń często oznacza złe użycie timerów lub pollingu.
Przykładowo: aplikacja newsowa podczas ładowania artykułów na starcie może mieć przez kilka sekund wysoki „Energy Impact”, ale później powinna zejść do bardzo niskich wartości, szczególnie gdy użytkownik czyta statyczny tekst. Jeśli wykres utrzymuje się na średnim poziomie, prawdopodobnie w tle pracują zbędne timery, animacje lub niepotrzebne odświeżenia.
Symulacje vs testy na prawdziwym urządzeniu
Symulator iOS na Macu nie odzwierciedla ani zużycia energii, ani zachowania sprzętowych komponentów takich jak modem, GPS czy czujniki ruchu. Do badania energooszczędności potrzebne są realne urządzenia, najlepiej więcej niż jedno (starsze i nowsze modele).
Przygotowując sesję testową:
- Wyłącz kabel zasilający – testuj na baterii, aby system nie wchodził w tryb zachowania pełnej wydajności kosztem energii.
- Ustaw różne tryby sieci: Wi‑Fi, LTE/5G, słaby zasięg. Modem komórkowy w słabym zasięgu zużywa znacznie więcej energii.
- Przetestuj zachowanie w trybie Low Power Mode – włącz go w ustawieniach baterii i sprawdź, czy aplikacja nie łamie zasad (np. próbuje forsować częsty background fetch).
- Uwzględnij typowe scenariusze użytkowe: kilkuminutowe korzystanie, praca w tle przez godzinę, intensywny scroll + wideo, sporadyczne użycie przez cały dzień.
Dobrym zwyczajem jest zapisanie konkretnych scenariuszy i ich kroków (np. otwórz listę, przewiń do końca, odczekaj 30 s, wróć), aby można było powtarzać testy w różnych wersjach aplikacji i porównywać wyniki.
Łączenie Energy Log z innymi narzędziami Instruments
Samo zauważenie, że „coś zjada baterię” to dopiero początek. Często trzeba zejść poziom niżej, by znaleźć konkretny kod odpowiedzialny za zużycie. Tutaj przydatne są inne narzędzia Instruments:
- Time Profiler – identyfikuje gorące ścieżki kodu, które zużywają najwięcej czasu CPU. Połączenie: jeśli Energy Log pokazuje wysokie CPU podczas scrollowania, Time Profiler wskaże, która metoda w trakcie
cellForRowAtlublayoutSubviewsjest winna. - Allocations – pomaga znaleźć nadmierne alokacje pamięci, które pośrednio zwiększają zaangażowanie CPU i GC (ARC). Wywoływanie wielu krótkotrwałych obiektów w pętlach to klasyczny scenariusz utraty efektywności.
- Network – pozwala skorelować skoki zużycia energii z konkretnymi żądaniami sieciowymi; widać liczbę requestów, ich rozmiar, czas trwania.
Efektywnym podejściem jest: najpierw Energy Log dla wykrycia momentów wzrostu zużycia energii, potem Time Profiler i Network do zidentyfikowania konkretnych funkcji lub wywołań API generujących te piki.
CPU i wątki w Swift – jak nie wybudzać procesora bez potrzeby
GCD, OperationQueue i priorytety zadań w kontekście energii
Grand Central Dispatch (GCD) i OperationQueue są kluczowymi narzędziami w Swift do zarządzania współbieżnością. Oprócz kwestii bezpieczeństwa wątków i wydajności, istotne jest ich przełożenie na energię. Każde zadanie na kolejce to potencjalne wybudzenie CPU. Zbyt agresywne rozbijanie pracy na wiele małych zadań może być kosztowne.
Przy projektowaniu kolejek i priorytetów warto uwzględnić:
- QoS (Quality of Service) –
.userInteractive,.userInitiated,.utility,.background. Ustawienie wszystkiego na.userInitiatedmoże poprawić subiektywną responsywność, ale powoduje większe zużycie energii. - Kierowanie zadań w tło – prace, które nie są krytyczne dla natychmiastowego UX (np. zapisywanie logów, wysyłanie statystyk) powinny iść na kolejki z QoS
.backgroundlub.utility. - Unikanie nadmiaru wątków – tworzenie wielu własnych
OperationQueuez różnymi konfiguracjami rzadko ma sens. Lepiej użyć globalnych kolejek GCD z odpowiednim QoS i kontrolować concurrency.
Przykład energooszczędnego użycia GCD:
Przykład zarządzania kolejkami z uwzględnieniem QoS
Poniższy kod pokazuje, jak rozdzielić zadania pod względem ich wpływu na UX i baterię:
let imageProcessingQueue = DispatchQueue(
label: "com.example.app.imageProcessing",
qos: .utility,
attributes: [.concurrent]
)
func loadAndProcessImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
guard let data = try? Data(contentsOf: url),
let image = UIImage(data: data) else {
DispatchQueue.main.async { completion(nil) }
return
}
imageProcessingQueue.async {
let processed = process(image) // np. filtr, resize
DispatchQueue.main.async {
completion(processed)
}
}
}
}
Krytyczne dla odczuwalnej szybkości jest pobranie i wstępne utworzenie obrazu, dlatego użyty jest QoS .userInitiated. Cięższe, ale mniej pilne filtrowanie przesunięto na kolejkę z QoS .utility, dzięki czemu system może lepiej zbalansować obciążenie i zużycie energii.
Batchowanie zadań i minimalizacja wybudzeń CPU
Energia tracona jest nie tylko na intensywną pracę CPU, ale także na częste wybudzanie procesora z uśpienia. Lepsze jest jedno dłuższe wybudzenie niż wiele krótkich, rozstrzelonych w czasie. W praktyce oznacza to grupowanie zadań:
- zamiast co 5 sekund wykonywać małe zadanie w tle, zsynchronizować wiele operacji co np. 1 minutę,
- zamiast kilku timerów w różnych miejscach, mieć jeden współdzielony mechanizm harmonogramowania,
- zamiast osobnego
DispatchSourceTimerdla każdej funkcji, użyć jednego timera i listy zaplanowanych „slotów”.
Prosty przykład batchowania zapisów do dysku:
final class BatchedLogger {
private var messages: [String] = []
private let queue = DispatchQueue(label: "com.example.app.logger", qos: .background)
private var saveWorkItem: DispatchWorkItem?
func log(_ message: String) {
queue.async { [weak self] in
guard let self = self else { return }
self.messages.append(message)
self.scheduleSave()
}
}
private func scheduleSave() {
saveWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
self?.flushToDisk()
}
saveWorkItem = workItem
// Opóźnienie 10 sekund redukuje liczbę zapisów i wybudzeń CPU
queue.asyncAfter(deadline: .now() + 10, execute: workItem)
}
private func flushToDisk() {
guard !messages.isEmpty else { return }
let toSave = messages
messages.removeAll()
// zapis do pliku / bazy
// ...
}
}
Dzięki opóźnieniu zamiast wielu małych, natychmiastowych zapisów, system dostaje jeden zbiorczy blok pracy. W wielu przypadkach dodatkowe 5–10 sekund opóźnienia jest akceptowalne z perspektywy produktu, a bardzo korzystne dla energii.
Unikanie „busy waiting” i niepotrzebnych pętli
Nawet krótkie pętle typu „czekamy aż coś się stanie” potrafią utrzymywać CPU w stanie aktywnym znacznie dłużej niż potrzeba. Zamiast tego trzeba opierać się na callbackach, async/await i mechanizmach zdarzeniowych.
Antywzorzec:
// NIE RÓB TAK
while !flag {
// „krótki” sleep nadal budzi CPU co chwilę
usleep(10_000)
}
performAction()
Lepsze podejście przy użyciu asynchronicznego API:
func waitForCondition() async {
await withCheckedContinuation { continuation in
observer.onConditionMet = {
continuation.resume()
}
}
}
Task.detached(priority: .background) {
await waitForCondition()
await performAction()
}
CPU wybudza się tylko wtedy, gdy faktycznie nadejdzie zdarzenie, bez aktywnego „podglądania” stanu w pętli.
Taski Swift Concurrency i priorytety a zużycie baterii
Swift Concurrency (async/await, Task) też wiąże się z decyzjami energetycznymi. Każdy task ma priorytet, który w uproszczeniu mapuje się na QoS. Jeśli wszystko tworzone jest jako Task(priority: .userInitiated), system ma niewielkie pole manewru przy oszczędzaniu energii.
Rozsądne mapowanie:
.userInitiated– akcje wywołane bezpośrednio przez użytkownika, widoczne w UI..utility– operacje, które mogą chwilę poczekać (np. prefetching, obliczenia analityczne)..background– prace niemające natychmiastowego wpływu na UX (np. synchronizacja statystyk).
func syncAnalyticsEvents() {
Task.detached(priority: .background) {
let events = await storage.pendingEvents()
guard !events.isEmpty else { return }
try? await analyticsAPI.send(events)
}
}
Przeniesienie zadań „miękkich” do niższego priorytetu pozwala systemowi je opóźnić lub zgrupować, co zmniejsza liczbę wybudzeń i ogólny „energy impact”.
Timery, DispatchSourceTimer i RunLoop z myślą o baterii
Timery to jedno z częstszych źródeł niepotrzebnych wybudzeń CPU. Kilka osobnych timerów odpalanych co 1–2 sekundy potrafi skutecznie uniemożliwić procesorowi wejście w stan głębszego uśpienia.
Kilka praktycznych zasad:
- jeśli timer sprawdza stan, który zmienia się rzadko, spróbować zastąpić go callbackiem lub notyfikacją,
- używać interwałów rzędu dziesiątek sekund/minut tam, gdzie precyzja co do sekundy nie ma znaczenia,
- używać tolerancji czasowej (
tolerance) – system może wtedy skorelować wybudzenia kilku timerów.
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .background))
timer.schedule(deadline: .now() + 30,
repeating: .seconds(60),
leeway: .seconds(10)) // tolerancja => łatwiejsze grupowanie wybudzeń
timer.setEventHandler { [weak self] in
self?.performPeriodicSync()
}
timer.resume()
Ustawienie leeway na kilka–kilkanaście sekund często nie wpływa na produkt, a pozwala systemowi „podciągnąć” różne operacje do jednego wybudzenia procesora.

Rysowanie i animacje – kiedy UI zaczyna kosztować baterię
Frame rate, odświeżanie i koszty niepotrzebnych animacji
GPU i CPU, pracujące nad płynnym UI, to jedno z większych źródeł zużycia energii, zwłaszcza w aplikacjach o intensywnym interfejsie. Kluczowy jest balans między płynnością a oszczędnością: nie każdy element musi być animowany w 60 FPS.
Typowe problemy:
- animacje działające w nieskończoność (np. pulsujące przyciski, gradienty),
- ciężkie efekty (cienie, blur, maski) odświeżane przy każdym ruchu,
- częste „layout thrashing” – powtarzające się przebudowy układu podczas jednego cyklu runloopa.
Jeżeli animacja ma jedynie przyciągnąć uwagę podczas akcji użytkownika, lepiej, by trwała krótko i zatrzymywała się w spoczynku, zamiast operować bez końca w tle.
Preferowanie animacji „systemowych” i Core Animation
Animacje deklaratywne z użyciem Core Animation (UIView.animate, CALayer) często są tańsze energetycznie niż ręczne przeliczanie klatek w pętlach (np. CADisplayLink z własną logiką layoutu). System potrafi je optymalizować i grupować.
// Deklaratywna animacja, delegowana do Core Animation
UIView.animate(withDuration: 0.3,
delay: 0,
options: [.curveEaseInOut, .beginFromCurrentState]) {
view.alpha = 1.0
view.transform = CGAffineTransform.identity
}
Jeśli pojawia się pokusa, by w CADisplayLink przy każdej klatce zmieniać layout widoku lub wykonywać złożone obliczenia, dobrze jest upewnić się, że nie da się tego zapisać jako zwykłej animacji właściwości warstwy.
Redukcja złożoności widoków i warstw
Każda podwarstwa, maska, efekt cienia czy zaokrąglenie rogów to dodatkowa praca dla GPU i CPU. Pojedyncze użycie nie robi różnicy, ale setki komórek tabeli lub kolekcji z ciężkimi widokami już tak.
Kilka praktyk przyjaznych baterii:
- ogranicz liczbę zagnieżdżonych widoków – uprość hierarchie,
- stosuj statyczne, prerenderowane grafiki tam, gdzie to możliwe, zamiast dynamicznego rysowania skomplikowanych kształtów,
- unikaj „żywych” blurów na dużej powierzchni, jeśli nie są krytyczne dla UX,
- cache’uj wyniki rysowania w
CALayer.contentslub w obrazach.
Przykład: zamiast rysować kształt karty z cieniem dla każdej komórki w draw(_:) przy scrollowaniu, można przygotować statyczny obraz tła i jedynie nakładać na niego dane tekstowe.
Reuse komórek i przygotowanie layoutu poza główną ścieżką scrollowania
Scrollowanie list to jeden z częstszych scenariuszy użytkowych. Jeśli każda komórka przy wyświetlaniu wykonuje skomplikowaną logikę (formatowanie dat, pobieranie obrazków, tworzenie atrybutów tekstu), CPU się rozgrzewa, a bateria topnieje.
Ważne elementy:
- agresywny reuse komórek (
dequeueReusableCell) i lekkie klasy komórek, - prekonfigurowanie
NSAttributedStringi innych cięższych zasobów przed wejściem na ekran, - prefetching danych (
UITableViewDataSourcePrefetching,UICollectionViewDataSourcePrefetching), aby unikać szczytów CPU w czasie szczytu scrollowania.
class ArticlesDataSource: NSObject, UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView,
prefetchRowsAt indexPaths: [IndexPath]) {
// lekki prefetch danych i obrazów
prefetcher.prefetchItems(at: indexPaths.map(.row))
}
}
Równomierne rozłożenie pracy w czasie zmniejsza pikowe obciążenie energetyczne i poprawia płynność.
Oszczędne korzystanie z CADisplayLink i timerów animacji
CADisplayLink wywołuje funkcję przy każdej klatce odświeżania ekranu. To mocne narzędzie, ale łatwo z nim przesadzić.
Jeśli display link aktualizuje coś, co nie musi się zmieniać w każdej klatce (np. niewielki progres), lepiej obniżyć częstotliwość aktualizacji albo korzystać z UIViewPropertyAnimator lub prostych animacji Core Animation.
// Jeśli już CADisplayLink, to z kontrolą cyklu życia
final class FPSMeter {
private var displayLink: CADisplayLink?
func start() {
guard displayLink == nil else { return }
let link = CADisplayLink(target: self, selector: #selector(step))
link.add(to: .main, forMode: .common)
displayLink = link
}
func stop() {
displayLink?.invalidate()
displayLink = nil
}
@objc private func step(link: CADisplayLink) {
// minimalna logika, bez ciężkich obliczeń
}
}
Najczęstszy błąd: zapomnieć wyłączyć CADisplayLink po zejściu ekranu z ekranu nawigacji. Z punktu widzenia baterii oznacza to nieustanne wybudzanie CPU i GPU nawet wtedy, gdy użytkownik patrzy na coś zupełnie innego.
Dynamiczny content a tryb niskiego zużycia energii
iOS udostępnia powiadomienie o trybie Low Power Mode (.NSProcessInfoPowerStateDidChange). Można je wykorzystać do redukcji kosztownych wizualnie elementów wtedy, gdy użytkownik świadomie oszczędza baterię.
final class PowerModeController {
init() {
NotificationCenter.default.addObserver(
self,
selector: #selector(powerStateChanged),
name: .NSProcessInfoPowerStateDidChange,
object: nil
)
}
@objc private func powerStateChanged() {
let isLowPower = ProcessInfo.processInfo.isLowPowerModeEnabled
UISettings.shared.setReducedAnimations(isLowPower)
}
}
final class UISettings {
static let shared = UISettings()
private(set) var reducedAnimations = false
func setReducedAnimations(_ reduced: Bool) {
reducedAnimations = reduced
}
}
W komponentach UI można wtedy warunkowo pomijać dekoracyjne animacje, intensywne efekty czy automatyczne odtwarzanie wideo, gdy UISettings.shared.reducedAnimations == true. To prosty, ale wymierny gest w stronę użytkownika, którego bateria jest na wyczerpaniu.
Sieć i API w Swift: mniej połączeń, mniej pobrań, więcej baterii
Model pracy radia komórkowego i Wi‑Fi a energochłonność
Transmisja danych jest kosztowna energetycznie, bo wybudza i utrzymuje w aktywnym stanie modem komórkowy lub układ Wi‑Fi. Z perspektywy baterii ważniejsze od „ile MB” bywa „jak często” i „jak długo” urządzenie trzyma radio w stanie aktywnym.
Typowy antywzorzec to częste, krótkie requesty (np. co 10–15 sekund) wykonywane osobno. Każde takie wybudzenie uruchamia łańcuch: CPU + radio + ewentualne GPU przy odświeżeniu interfejsu. Jeśli te żądania dadzą się zgrupować, modem może „obudzić się” rzadziej, a potem wrócić do niskiego zużycia.
Batchowanie i koaleskacja requestów
Dobrym punktem wyjścia jest zastanowienie się, które operacje sieciowe muszą być wykonane natychmiast, a które mogą poczekać. Wiele akcji można skleić w paczki lub obsłużyć „przy okazji” innego requestu.
- zamiast kilku oddzielnych endpointów po dane ekranu startowego – jeden endpoint zwracający skonsolidowaną odpowiedź,
- zamiast wysyłki logów natychmiast po każdym zdarzeniu – buforowanie i wysyłanie co X minut albo przy dogodnym momencie (np. Wi‑Fi + ładowanie),
- zamiast polling co kilka sekund – mechanizmy push lub dłuższe interwały, jeśli produkt na to pozwala.
Prosty bufor na zdarzenia użytkownika, synchronizowany rzadziej, potrafi obniżyć liczbę requestów o rząd wielkości.
URLSession skonfigurowane pod oszczędność energii
Samo użycie URLSession nie gwarantuje efektywnego korzystania z sieci. Konfiguracja sesji i polityka retry mogą mieć duży wpływ na baterię.
final class NetworkClient {
static let shared = NetworkClient()
private let session: URLSession
private init() {
let config = URLSessionConfiguration.default
config.waitsForConnectivity = true
config.allowsExpensiveNetworkAccess = false
config.allowsConstrainedNetworkAccess = true
config.httpMaximumConnectionsPerHost = 4
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 60
session = URLSession(configuration: config)
}
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
try await session.data(for: request)
}
}
Kilka ustawień ma szczególne znaczenie:
waitsForConnectivity– zamiast natychmiastowych retry, system poczeka na sensowne warunki sieciowe, co redukuje „puste” próby,allowsExpensiveNetworkAccess– przy wartościfalsesystem może blokować żądania na 3G/5G lub w roamingu; mniej nieskutecznych wybudzeń,httpMaximumConnectionsPerHost– zbyt wiele równoległych połączeń zwiększa pikowe zużycie energii.
Transfer w tle z URLSessionConfiguration.background
Duże lub dłuższe transfery (pobieranie plików, backupy, synchronizacja zdjęć) warto przenieść do sesji w tle. System sam planuje takie zadania tak, by pasowały do aktualnych warunków (Wi‑Fi, ładowanie, aktywność urządzenia).
final class BackgroundDownloader: NSObject, URLSessionDownloadDelegate {
static let shared = BackgroundDownloader()
private lazy var session: URLSession = {
let config = URLSessionConfiguration.background(withIdentifier: "com.example.app.bg.download")
config.sessionSendsLaunchEvents = true
config.isDiscretionary = true // system dobierze moment pobierania
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
func enqueueDownload(from url: URL) {
let task = session.downloadTask(with: url)
task.resume()
}
// URLSessionDownloadDelegate...
}
isDiscretionary = true oddaje inicjatywę systemowi: iOS może przesunąć pobieranie na moment, kiedy urządzenie jest podłączone do prądu i ma dobre Wi‑Fi. Z perspektywy baterii to duża korzyść, jeśli użytkownik nie oczekuje natychmiastowego efektu.
Cache, etag i warunkowe zapytania
Najbardziej ekologiczny request to ten, którego nie trzeba wysyłać. HTTP oferuje mechanizmy pozwalające unikać pełnego pobierania danych, gdy serwer niczego nie zmienił.
Kluczowe elementy po stronie klienta:
- respektowanie nagłówków
Cache-Control,Expires,ETag, - ustawianie własnych polityk cache (np. dyskowy cache obrazów, JSON‑ów),
- warunkowe żądania
If-None-Match,If-Modified-Since.
Dobrze skonfigurowany URLCache może sprawić, że część ekranów otwiera się wyłącznie z lokalnego cache, a sieć wykorzystywana jest tylko sporadycznie.
let cache = URLCache(memoryCapacity: 50 * 1024 * 1024,
diskCapacity: 200 * 1024 * 1024,
diskPath: "network-cache")
let config = URLSessionConfiguration.default
config.urlCache = cache
config.requestCachePolicy = .useProtocolCachePolicy
Istotne jest też niewyłączanie cache „na wszelki wypadek” (reloadIgnoringLocalCacheData) tam, gdzie nie ma takiej potrzeby. Każde zignorowanie cache to potencjalny dodatkowy request i kilka milidżuli mniej w baterii.
Oszczędne odświeżanie danych i polling
Mechaniczne odświeżanie danych co kilka sekund zjada baterię, szczególnie przy transmisji komórkowej. Lepiej reagować na realne potrzeby: interakcję użytkownika, powrót na ekran, zmiany stanu aplikacji.
Prosty schemat dla ekranu z danymi z API może wyglądać tak:
- odśwież przy pierwszym wejściu na ekran,
- odśwież przy powrocie na ekran po dłuższej przerwie (np. > 2–5 minut w tle),
- odśwież po jawnej akcji użytkownika (pull‑to‑refresh).
Jeśli produkt wymaga prawie czasu rzeczywistego, warto przemyśleć dłuższy interwał (np. co 30–60 sekund) zamiast kilku sekund, albo przejść na mechanizmy push (np. WebSocket, APNs + lokalny update).
Adaptacja do warunków sieci i Low Data Mode
iOS udostępnia informację o „trybie oszczędzania danych” (Low Data Mode). To naturalne miejsce na redukcję aktywności sieciowej, co pośrednio pomaga baterii.
extension NetworkClient {
var isLowDataModeEnabled: Bool {
session.configuration.allowsConstrainedNetworkAccess == false
}
}
Praktyczne zastosowania:
- niższa jakość obrazów lub wideo,
- brak automatycznego odświeżania feedu w tle,
- odroczenie niepilnych synchronizacji na czas Wi‑Fi.
Zależnie od typu aplikacji, taka adaptacja może być agresywniejsza w połączeniu z Low Power Mode – użytkownik często oczekuje wtedy, że aplikacje „odpuszczą sobie” wygodę na rzecz czasu pracy.
Lokalizacja, Bluetooth i czujniki – jak nie blokować baterii hardwarem
Tryby lokalizacji w Core Location a koszt energetyczny
Lokalizacja GPS to jeden z największych pojedynczych drenaży baterii w telefonie. Core Location oferuje kilka trybów, które różnią się dokładnością i kosztem energetycznym.
Najważniejsze tryby:
startUpdatingLocation()z wysoką dokładnością – ciągłe śledzenie, duży koszt; sens tylko w aplikacjach typu nawigacja, sport,startMonitoringSignificantLocationChanges()– aktualizacja co większe przemieszczenie, niższy koszt, dobra baza do pasywnego logowania lokalizacji,startMonitoringVisits()– wykrywanie wejścia/wyjścia z „miejsc” (dom, praca), bardzo oszczędne, ale mniej szczegółowe,- monitorowanie regionów (
CLRegion) – wyzwalacze przy wejściu/wyjściu z obszaru, optymalne przy geofencingu.
Jeśli produkt nie wymaga ciągłego śledzenia, nie warto utrzymywać startUpdatingLocation() dłużej niż kilka–kilkanaście sekund potrzebnych do złapania jednej pozycji.
Dobieranie dokładności i filtrowanie zmian
Każde zwiększenie dokładności lokalizacji zwiększa koszt energetyczny: GPS + Wi‑Fi + sieć komórkowa pracują intensywniej i częściej. Dobrze jest zacząć od możliwie niskiej dokładności, a dopiero na żądanie użytkownika przejść do wyższej.
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
locationManager.distanceFilter = 100 // metry
distanceFilter pozwala odfiltrować drobne wahania i zmniejszyć liczbę callbacków. Jeśli aplikacja notuje przebytą trasę, potrzebuje czegoś innego niż aplikacja z prognozą pogody. W tym drugim przypadku pojedyncza lokalizacja raz na jakiś czas jest wystarczająca.
Jednorazowe pobranie lokalizacji zamiast ciągłego śledzenia
Popularny błąd: użycie startUpdatingLocation() tam, gdzie wystarczy „raz i koniec”. W nowszych API można skorzystać z wygodniejszych metod jednorazowych.
final class OneShotLocationProvider: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
private var completion: ((CLLocation?) -> Void)?
func requestLocation(completion: @escaping (CLLocation?) -> Void) {
self.completion = completion
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyKilometer
manager.requestWhenInUseAuthorization()
manager.requestLocation()
}
func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
completion?(locations.last)
completion = nil
}
func locationManager(_ manager: CLLocationManager,
didFailWithError error: Error) {
completion?(nil)
completion = nil
}
}
requestLocation() pobiera lokalizację i automatycznie zatrzymuje aktualizacje, bez konieczności ręcznego stopUpdatingLocation(). Dla jednorazowych odczytów to prostsze i bezpieczniejsze energetycznie.
Monitorowanie regionów i „significant location changes”
Jeśli aplikacja ma reagować na większe zmiany położenia (np. powiadomienie po dotarciu do pracy), rozsądniej jest użyć startMonitoringForRegion(_:) i startMonitoringSignificantLocationChanges(). System łączy wtedy Twoje potrzeby z innymi aplikacjami i komponentami, zamiast utrzymywać osobny, ciągły strumień danych z GPS.
let officeCenter = CLLocationCoordinate2D(latitude: 52.0, longitude: 21.0)
let region = CLCircularRegion(center: officeCenter,
radius: 200,
identifier: "office-region")
region.notifyOnEntry = true
region.notifyOnExit = false
locationManager.startMonitoring(for: region)
Regiony są utrzymywane nawet po restarcie systemu, a koszt energetyczny pozostaje znacznie niższy niż przy ciągłym odświeżaniu pozycji.
Zamykanie lokalizacji gdy nie jest potrzebna
Niezależnie od wybranego trybu, kluczowe jest odcinanie się od lokalizacji, kiedy aplikacja nie potrzebuje danych. Typowe miejsca to:
viewDidDisappearekranów wykorzystujących GPS – jeśli śledzenie ma sens tylko przy aktywnym widoku,- przejście aplikacji w tło, o ile produkt nie wymaga background location,
- zakończenie aktywności sportowej, nawigacji, itp.
Pojedyncza zapomniana linijka stopUpdatingLocation() potrafi wyczyścić baterię w kilka godzin, szczególnie przy złym zasięgu, gdy modem pracuje intensywniej.
Bluetooth: CoreBluetooth i iBeacon z rozwagą
Bluetooth Low Energy (BLE) jest tańszy energetycznie niż klasyczna transmisja, ale ciągłe skanowanie i nadawanie również kosztuje. W aplikacjach wykorzystujących CoreBluetooth lub iBeacon opłaca się wejść na poziom scenariuszy użytkownika.
Przykładowe zasady:
- skanowanie BLE ograniczone do czasu, gdy użytkownik jest na ekranie, który tej funkcji potrzebuje,
- redukowanie mocy nadawania i częstotliwości ogłoszeń po stronie urządzenia peryferyjnego (jeśli masz na nie wpływ),
- zatrzymywanie skanowania po wykryciu oczekiwanego urządzenia i wznowienie dopiero przy realnej potrzebie.
W przypadku iBeacon lepiej korzystać z monitorowania regionów beaconów niż ciągłego skanowania własnymi filtrami, bo i tutaj system wykonuje za kulisami dużo optymalizacji.
Czujniki ruchu, krokomierz i żyroskop
Akcelerometr i żyroskop w trybie „ciągłego strumienia” mogą być zauważalnym obciążeniem, zwłaszcza w połączeniu z przetwarzaniem danych na bieżąco. Dla wielu zastosowań wystarczy jednak warstwa agregacji udostępniana przez system.
Dobrym przykładem jest CMPedometer, który pozwala pobierać liczbę kroków lub dane o dystansie bez ręcznego subskrybowania surowych wartości akcelerometru – system i tak je przetwarza na potrzeby aplikacji zdrowotnych.
let pedometer = CMPedometer()
pedometer.startUpdates(from: Date()) { data, error in
// lekkie przetwarzanie danych, unikanie złożonych operacji w tle
}
Jeśli aplikacja wymaga krótkich pomiarów (np. kalibracja, prosty gest), rozsądniej jest uruchomić CMMotionManager tylko na kilka sekund niż trzymać go aktywnego godzinami w tle.
Redukowanie częstotliwości odczytów z czujników
Najczęściej zadawane pytania (FAQ)
Jak sprawdzić, czy moja aplikacja Swift faktycznie zużywa dużo baterii?
Podstawowe narzędzie to Instruments z szablonem Energy Log. Projekt uruchamiasz z Xcode przez Product > Profile, wybierasz Energy Log i podłączasz fizyczne urządzenie (symulator nie pokazuje realnego zużycia energii). Następnie odtwarzasz typowe scenariusze użycia: start, logowanie, scrollowanie list, przełączanie zakładek, pobieranie danych.
Podczas analizy skup się na kilku wykresach: CPU Usage, Wakeups, Network, Location oraz syntetycznym Energy Impact. Jeśli przy zwykłym czytaniu treści (np. ekran z artykułem lub statyczną listą) wykres utrzymuje się na średnim lub wysokim poziomie, aplikacja najpewniej generuje zbyt częste wybudzenia procesora, requesty sieciowe albo niepotrzebnie używa usług lokalizacyjnych.
Jakie elementy iOS najbardziej „zjadają” baterię w aplikacjach Swift?
Największy wpływ mają: CPU, GPU, sieć, GPS/czujniki oraz operacje na pamięci masowej. Dla programisty przekłada się to na konkretne decyzje: jak często budzisz CPU timerami, ile pracy wykonujesz przy każdej akcji użytkownika, jak złożony jest interfejs (cienie, rozmycia, animacje), jak duże i jak częste są requesty sieciowe oraz czy zapisujesz dane w małych porcjach czy w batchach.
W praktyce najczęstsze „pożeracze” to: częsty polling API małymi requestami, ciągłe śledzenie lokalizacji w tle, animacje działające non stop, intensywne scrollowanie źle zoptymalizowanych list oraz nieprzemyślane zapisy do bazy/dysku przy każdej drobnej zmianie stanu.
Jak projektować architekturę aplikacji iOS, żeby była oszczędna dla baterii?
Punkt wyjścia to założenie, że kod działa w krótkich oknach czasowych i nie „walczy” z polityką iOS. Jeśli coś możesz delegować na system – zrób to. Przykłady: zamiast cyklicznego pollingu API użyj pushy, zamiast ręcznego trzymania aplikacji przy życiu w tle korzystaj z BGTaskScheduler i background transfers w URLSession.
Dobrze działa podejście event-driven: reagowanie na zdarzenia zamiast ciągłego monitorowania. Dane odświeżasz w momencie wejścia na ekran lub na żądanie użytkownika, a nie co 30 sekund w tle. UI aktualizujesz tylko tam, gdzie nastąpiła realna zmiana, unikając pełnych reloadów widoków „na wszelki wypadek”. Do tego dochodzi batche’owanie operacji sieciowych i zapisu na dysk, aby zmniejszyć liczbę wybudzeń CPU i modemu.
Jak używać lokalizacji w iOS, żeby nie drenować baterii?
Najważniejszy wybór to tryb lokalizacji. Jeśli aplikacja nie wymaga precyzyjnego, ciągłego śledzenia, lepiej sięgnąć po tryby mniej agresywne: significant location changes, region monitoring czy aktualizacje tylko na pierwszym planie. Stałe odpytywanie GPS w tle jest potrzebne naprawdę rzadko (np. nawigacja), a koszt energetyczny jest bardzo wysoki.
Dobrym wzorcem jest czasowe włączanie lokalizacji: uruchamiasz Core Location tylko na czas konkretnej akcji (np. znalezienie najbliższego punktu, jednorazowe oznaczenie zdjęcia) i od razu ją wyłączasz. W instrumentach Energy Log obserwuj, czy po wyjściu z ekranu z mapą nie widać już aktywności Location – jeśli nadal są piki, coś nie zostało poprawnie zwolnione.
Jak pogodzić wydajność aplikacji z oszczędzaniem baterii?
Wydajność i energooszczędność często idą w parze, ale konflikt pojawia się przy strategiach typu prefetching vs on-demand. Jeśli „na dzień dobry” pobierzesz i przeliczysz wszystko, interfejs później będzie szybki, ale start aplikacji mocno obciąży CPU i sieć. Z drugiej strony ekstremalne dzielenie pracy na dziesiątki małych kroków może skończyć się lawiną wybudzeń i requestów, co również nie służy baterii.
Rozsądne podejście to hybryda: część kluczowych danych ładujesz wcześniej, resztę na żądanie, ale z batchowaniem. Dane odświeżasz przy istotnych momentach (np. powrót do aplikacji, wejście na ekran listy) zamiast w stałych interwałach w tle. Animacje ograniczasz do tych, które realnie poprawiają UX, i nie ustawiasz niepotrzebnie wysokich częstotliwości odświeżania.
Co mogę kontrolować jako programista iOS, a czego nie przeskoczę w kontekście baterii?
Masz pełną kontrolę nad tym, jak często budzisz CPU (timery, obserwatorzy, pętle), jak planujesz zadania w tle (BGTaskScheduler, background fetch), jakie tryby lokalizacji wybierasz oraz jak wyglądają wzorce komunikacji z API (polling vs push, batchowanie requestów, caching). Do tego dochodzi sposób aktualizacji UI i strategia pracy z pamięcią masową.
Nie przeskoczysz natomiast polityk iOS: system może usypiać lub zabijać proces w tle, ograniczać czas działania background tasks, wprowadzać throttling w trybie Low Power i priorytetyzować inne procesy. Architektura powinna to akceptować – aplikacja musi działać poprawnie, nawet jeśli system często ją wznawia i ubija, a nie próbować to obchodzić nieudokumentowanymi trikami.
Jak ograniczyć liczbę wybudzeń CPU (wakeups) w aplikacji Swift?
Najczęstszy winowajca to źle używane timery i agresywny polling. Zamiast timerów co kilka sekund lepiej korzystać z mechanizmów zdarzeniowych (notyfikacje push, callbacks z URLSession, NSNotificationCenter). Jeśli timer jest konieczny, ustaw dłuższe interwały i tolerancję (tolerance), żeby system mógł zgrupować wybudzenia.
Drugi obszar to sieć: wiele małych requestów generuje serię wybudzeń modemu i CPU. Lepszym rozwiązaniem jest łączenie zapytań w batch, użycie background transfers tam, gdzie to możliwe, oraz solidny caching. W Energy Log obserwuj metrykę Wakeups – jeśli przy „spokojnym” scenariuszu (np. użytkownik czyta treść) wykres jest nadal gęsty, trzeba przejrzeć timery, obserwatory i sposób odświeżania danych.







Bardzo ciekawy artykuł! Zawsze zastanawiałem się, jak zmaksymalizować wydajność moich aplikacji na iOS, nie „zjadając” przy tym baterii. Teraz, dzięki omówionym w artykule technikom napisania efektywnego kodu w Swift, mam nadzieję, że mój projekt będzie działał płynniej i nie obciąża baterii mojego iPhone’a. Dzięki za podzielenie się tymi cennymi wskazówkami!
Komentowanie treści jest możliwe wyłącznie dla zalogowanych osób.