iOS Development

Dlaczego w Twojej aplikacji iOS ciągle pojawiają się nowe bugi?

7 min readRafał Dubiel
#iOS#Bugs#Architecture

Kiedyś, gdy zaczynałem jako junior iOS developer, często zdarzało mi się, że QA zgłaszał mi błąd, poprawiałem go, a na jego miejsce pojawiały się dwa kolejne. Zastanawiałem się wtedy - czy to ja jestem tak nieuważny, czy może problem leży gdzie indziej? Odpowiedź przyszła z czasem.

Architektura to fundament

Zacznijmy od rzeczy najbardziej fundamentalnej. Większość problemów, z którymi borykają się zespoły iOS, nie wynika z trudności technicznych samej platformy. Wynika z decyzji architektonicznych podjętych (lub niepodjętych) na samym początku projektu.

Widziałem dziesiątki projektów, gdzie architektura „wyłaniała się" organicznie, czyli po prostu nikt jej świadomie nie zaprojektował. Efekt? Massive View Controllers liczące tysiące linii, ViewModele które robią wszystko, modele danych pomieszane z logiką biznesową, sieciową i prezentacyjną. Problem polega na tym, że w takim kodzie zmiana jednej rzeczy pociąga za sobą kaskadę nieprzewidzianych efektów. Naprawiasz jeden bug, tworzysz trzy kolejne. Brzmi znajomo?

Moje pięć głównych przyczyn błędów:

  1. Brak jasnego podziału odpowiedzialności

    Kiedy jedna klasa zajmuje się pobieraniem danych, ich transformacją, walidacją, cachowaniem i prezentacją, testowanie staje się koszmarem, a każda modyfikacja to ryzyko. Single Responsibility Principle istnieje nie dlatego, że ładnie brzmi w książkach, ale dlatego, że drastycznie redukuje skalę potencjalnych błędów.

  2. Nieprzemyślane zarządzanie stanem

    W aplikacjach mobilnych stan to wszystko - stan widoku, stan sesji użytkownika, stan połączenia sieciowego, stan cache'a. Kiedy ten sam kawałek informacji żyje w trzech różnych miejscach i jest synchronizowany „na piechotę", to tylko kwestia czasu, kiedy te miejsca zaczną sobie przeczyć. Widziałem aplikacje, gdzie użytkownik był jednocześnie zalogowany i wylogowany, w zależności od tego, który ekran otworzyłeś.

  3. Tight coupling z frameworkami i zależnościami zewnętrznymi

    Kiedy logika biznesowa jest spleciona z UIKit, CoreData czy konkretną biblioteką sieciową, każda zmiana frameworka (a Apple lubi zmieniać API) staje się operacją na otwartym sercu. Testowanie takiego kodu jest mega trudne, co sprawia, że większość zespołów po prostu rezygnuje z testów jednostkowych.

  4. Brak strategii obsługi błędów

    W swojej karierze często widziałem kod, gdzie błędy są po prostu pomijane albo niedostatecznie obsłużone: pusty blok catch, ignorowany Result.failure, opcjonalne force-unwrapowane „bo przecież zawsze tam coś będzie". A potem przychodzi edge case, którego nikt nie przewidział, i aplikacja się crashuje bez żadnej informacji dlaczego.

  5. Ignorowanie cyklu życia aplikacji

    iOS ma swoje specyficzne wymagania dotyczące cyklu życia aplikacji i kontrolerów widoku. Aplikacja może zostać zawieszona, wznowiona, zabita w tle. Widok może zostać zwolniony z pamięci i odtworzony. Połączenie sieciowe może zniknąć w trakcie operacji. Kod, który nie uwzględnia tych scenariuszy, działa świetnie na urządzeniu developera i sypie się u użytkowników.

Co powinieneś zrobić, żeby uniknąć błędów i po prostu ułatwić sobie życie?

  1. Zainwestuj czas w architekturę na starcie

    Nie chodzi o wybór między TCA, VIPER czy Clean Architecture - każde z tych podejść może działać dobrze. Chodzi o świadome przemyślenie, jak będzie płynąć informacja w aplikacji, gdzie będzie żyła logika biznesowa, jak będą komunikowały się moduły. Godzina spędzona na whiteboard na początku projektu oszczędza tygodnie debugowania później.

  2. Stosuj dependency injection konsekwentnie

    To nie jest akademicki wymysł - to praktyczne narzędzie, które pozwala testować komponenty w izolacji i wymienić implementacje bez przepisywania połowy aplikacji. Kiedy ViewModele dostają swoje zależności z zewnątrz zamiast tworzyć je wewnętrznie, nagle możesz napisać test, który wykonuje się w milisekundach i sprawdza konkretny scenariusz.

  3. Traktuj błędy priorytetowo

    Każda operacja, która może się nie udać, powinna mieć jawnie zamodelowaną ścieżkę błędu. Swift daje nam do tego świetne narzędzia - Result, throws, async/await z obsługą błędów. Używajmy ich zamiast udawać, że błędy nie istnieją.

  4. Modeluj stany jawnie

    Zamiast pięciu rozrzuconych booleanów, użyj enuma z associated values. Zamiast opcjonalnych pól, które „są zawsze wypełnione po zalogowaniu", stwórz osobne typy dla użytkownika zalogowanego i niezalogowanego. Kompilator staje się Twoim sojusznikiem i wyłapuje całe klasy błędów, zanim kod trafi na urządzenie.

  5. Pisz testy dla logiki biznesowej

    Nie mówię o 100% code coverage - mówię o pokryciu testami tych miejsc, gdzie naprawdę podejmowane są decyzje. Jeśli masz funkcję, która decyduje, czy użytkownik może wykonać transakcję na podstawie stanu konta, limitu dziennego i historii operacji, to ta funkcja zasługuje na testy. I jeśli Twoja architektura nie pozwala Ci napisać takiego testu bez mockowania połowy iOS SDK, to znak, że coś jest nie tak z architekturą, nie z testami.

  6. Traktuj code review poważnie - w obie strony

    Code review to nie formalność do odhaczenia ani polowanie na literówki. To moment, w którym druga para oczu może wyłapać błąd logiczny, który Tobie umknął po kilku godzinach siedzenia w tym samym kodzie. Ale działa to tylko wtedy, gdy reviewer faktycznie rozumie kontekst zmian i ma czas się w nie wgryźć. Zdawkowe "LGTM" po trzydziestu sekundach nikomu nie pomaga. Z drugiej strony - ucz się z review swojego kodu. Jeśli ktoś wyłapuje u Ciebie powtarzające się problemy, to sygnał do zmiany nawyków, nie powód do frustracji.

  7. Wykorzystuj AI jako pierwszą linię obrony

    Narzędzia takie jak GitHub Copilot czy Claude potrafią wyłapać potencjalne problemy jeszcze zanim kod trafi na review. Nieobsłużony optional, brakujący przypadek w switchu, race condition przy dostępie do współdzielonego stanu - AI coraz lepiej rozpoznaje te wzorce. Nie traktuj tego jako zamiennika dla własnego myślenia ani dla code review, ale jako dodatkowe zabezpieczenie. Czasem świeże "spojrzenie" - nawet jeśli to algorytm - wyłapie coś, co Ty i Twój zespół przeoczyliście przez przyzwyczajenie.

  8. Dokumentuj podjęte decyzje, nie tylko kod

    Kod mówi Ci, jak coś działa. Nie mówi Ci, dlaczego tak, a nie inaczej. Za pół roku, kiedy wrócisz do projektu (albo ktoś nowy dołączy do zespołu), nikt nie będzie pamiętał kontekstu. Dlaczego ten moduł jest wydzielony do osobnego frameworka? Dlaczego używamy tego konkretnego podejścia do cachowania?

    Nie musi to być rozbudowana dokumentacja. Wystarczy prosty plik markdown w repozytorium - tzw. Architecture Decision Record (ADR). Krótki opis problemu, rozważane opcje, podjęta decyzja i jej uzasadnienie. Tyle. To oszczędza godziny dyskusji w stylu "czemu to tak jest?" i zapobiega sytuacjom, gdzie ktoś "naprawia" coś, co było świadomą decyzją, tworząc nowe problemy.

Jak zidentyfikować problemy w istniejącej aplikacji?

Czasem wiesz, że coś jest nie tak, ale trudno wskazać palcem gdzie dokładnie. Na szczęście nie musisz polegać tylko na intuicji - są narzędzia, które pomogą Ci zdiagnozować stan projektu.

  • Statyczna analiza kodu

Zacznij od narzędzi takich jak SwiftLint czy Periphery. SwiftLint wyłapie niespójności stylistyczne i potencjalne problemy, ale też pokaże Ci metryki typu długość funkcji czy złożoność cyklomatyczna. Jeśli funkcja ma więcej niż 50 linii albo złożoność powyżej 10 to czerwona flaga. Periphery z kolei znajdzie martwy kod, którego nikt nie używa, a który straszy w projekcie od trzech lat.

  • Test coverage - ale z głową

Sprawdź pokrycie testami, ale nie fiksuj się na liczbie procentowej. 80% coverage, gdzie testy sprawdzają tylko gettery i settery jest bezwartościowe. Lepsze 30% coverage, które pokrywa faktyczną logikę biznesową. Zadaj sobie pytanie: czy testy złapałyby buga, gdybym zmienił warunek w kluczowym if-ie?

  • Xcode Instruments i Memory Graph

Memory leaki i retain cycle to cisi zabójcy stabilności. Regularnie odpalaj Memory Graph Debugger i sprawdzaj, czy obiekty zwalniają się tak, jak powinny. Jeśli ViewModel żyje dłużej niż ekran, który go stworzył to masz problem.

  • Metryki z produkcji

Firebase Crashlytics, Sentry, czy nawet podstawowe metryki z App Store Connect. Crash-free rate poniżej 99% to znak, że coś wymaga uwagi. Śledź też, które ekrany generują najwięcej crashy - często wskazują na moduły z największym długiem technicznym.

Podsumowanie

Większość bugów nie bierze się z tego, że iOS jest trudny albo że Swift ma jakieś pułapki. Bierze się z decyzji, które podejmujemy (lub których unikamy) każdego dnia - z pójścia na skróty, z kopiowania kodu zamiast abstrakcji, z odkładania refactoringu „na później", z braku testów, bo „nie ma czasu".

Paradoksalnie, robienie rzeczy „porządnie" rzadko zajmuje więcej czasu. Zajmuje więcej czasu na początku, ale oszczędza go wielokrotnie później. Projekt z jasną architekturą i dobrym pokryciem testami pozwala wprowadzać zmiany pewnie i szybko. Projekt bez tych rzeczy z czasem staje się coraz wolniejszy, bo każda zmiana wymaga coraz więcej ostrożności i ręcznego testowania.

Oczywiście, błędy będą się zdarzać. Ale jest ogromna różnica między sporadycznym bugiem w skomplikowanym edge case'ie a systemowym generowaniem problemów przez architekturę, która od początku była na to skazana.

Najgorsze bugi to nie te, które crashują aplikację. Najgorsze są te, które sprawiają, że użytkownik myśli: ta aplikacja jest po prostu słaba i już nigdy nie wraca...

Rafał Dubiel

Rafał Dubiel

Senior iOS Developer z ponad 8-letnim doświadczeniem w tworzeniu aplikacji mobilnych. Pasjonat czystej architektury, Swift i dzielenia się wiedzą ze społecznością.

Udostępnij artykuł:

Dlaczego w Twojej aplikacji iOS ciągle pojawiają się nowe bugi? | FractalDev Blog