Czy powinieneś używać tak wielu zależności?
Każdy iOS developer zna ten moment: zaczynasz nowy projekt, otwierasz Package.swift i zastanawiasz się czy dodać bibliotekę czy napisać samemu?
Osobiście większość kodu wolę pisać samemu i uważam że stosowanie wielu popularnych bibliotek to przerost formy nad treścią. Lubię być niezależny, ale nie oznacza to też, że lubię utrudniać sobie życie.
W tym artykule podzielę się kryteriami decyzyjnymi, które stosuję, pokażę konkretne przykłady i opowiem o lekcjach wyniesionych z bolesnych migracji.
Ukryte koszty zależności
Zanim przejdziemy do kryteriów, warto uświadomić sobie czym tak naprawdę płacimy za każdą bibliotekę w projekcie.
Rozmiar binarki
To najbardziej oczywisty koszt. Dodajesz Alamofire żeby wysłać 3 requesty? Właśnie dorzuciłeś ~2MB do swojej aplikacji. Kingfisher na podstawowe cachowanie obrazków? Kolejne ~3MB.
W czasach 5G wydaje się to nieistotne, ale App Store nadal pokazuje rozmiar aplikacji, a użytkownicy nadal zwracają na to uwagę, szczególnie na rynkach z droższym transferem danych. Wyobraź sobie taką sytuację, jesteś na wakacjach gdzieś na odludziu, dostęp do internetu jest słaby, ale chcesz pobrać aplikację w konkretnym celu, masz do wyboru dwie. Jedna waży 250MB, a druga 80MB, którą wybierzesz?
Czas kompilacji
Każda zależność to kod, który musi się skompilować. W małych projektach jest to prawie niezauważalne, ale przy 20-30 bibliotekach clean build potrafi trwać minuty. To realny koszt w czasie developmentu.
Podatność na ataki
W aplikacjach security-sensitive każda zewnętrzna biblioteka to potencjalny wektor ataku. Supply chain attacks nie są teorią, zdarzały się i będą się zdarzać. Audytowanie kodu 15 bibliotek to zupełnie inna skala problemu niż audytowanie własnych 2000 linii.
Dług technologiczny
To koszt, który ujawnia się z opóźnieniem. Biblioteka przestaje być rozwijana, autor zmienia API w major wersji, pojawia się konflikt z inną zależnością. Nagle prosty upgrade staje się zadaniem na tygodnie.
Horror story: RxSwift dla jednego Observable
Kilka lat temu dołączyłem do projektu, który miał dodany RxSwift jako zależność. Duża aplikacja, kilkadziesiąt tysięcy linii kodu. Naturalnie założyłem, że RxSwift jest tutaj fundamentem architektury: bindingi UI, streamy danych, skomplikowane operatory.
Postanowiłem przejrzeć gdzie faktycznie używamy Rx.
Były to 3 miejsca. Trzy BehaviorRelay w warstwie ustawień użytkownika. Coś w stylu:
class SettingsManager {
let isDarkModeEnabled = BehaviorRelay<Bool>(value: false)
let fontSize = BehaviorRelay<FontSize>(value: .medium)
let notificationsEnabled = BehaviorRelay<Bool>(value: true)
}
To wszystko. Żadnych skomplikowanych operatorów. Żadnego flatMapLatest. Żadnego combineLatest z pięcioma źródłami. Trzy obserwowalne wartości...
A koszt?
- ~3MB do binarki (RxSwift + RxCocoa + RxRelay),
- Wydłużony czas kompilacji: Rx to dużo generycznego kodu,
- Próg wejścia dla nowych developerów: "musisz znać Rx żeby pracować w tym projekcie" (dla trzech property),
- Aktualizacje - Rx musi nadążać za zmianami w Swift.
Jak to się stało? Klasyka. Ktoś na początku projektu znał Rx, użył go do pierwszej implementowanej funkcjonalności, a potem projekt rósł w innym kierunku, a zależność została.
Co można było wtedy zrobić zamiast tego?
Prosty, natywny Observable na ~25 linii:
@propertyWrapper
final class Observable<Value> {
var wrappedValue: Value {
get { value }
set {
value = newValue
observers.values.forEach { $0(newValue) }
}
}
var projectedValue: Observable<Value> { self }
private var value: Value
private var observers: [UUID: (Value) -> Void] = [:]
init(wrappedValue: Value) {
self.value = wrappedValue
}
func observe(_ observer: @escaping (Value) -> Void) -> UUID {
let id = UUID()
observers[id] = observer
observer(value) // emit current value
return id
}
func removeObserver(_ id: UUID) {
observers.removeValue(forKey: id)
}
}
Użycie:
class SettingsManager {
@Observable var isDarkModeEnabled = false
@Observable var fontSize: FontSize = .medium
}
// Subskrypcja
let id = settingsManager.$isDarkModeEnabled.observe { isDark in
print("Dark mode: \(isDark)")
}
// Cleanup
settingsManager.$isDarkModeEnabled.removeObserver(id)
Zero zależności. Pełna kontrola. Każdy programista zrozumie co się dzieje.
A jeśli potrzebujesz czegoś bardziej rozbudowanego, to od iOS 13 masz Combine, który jest wbudowany w system i oferuje wszystko co RxSwift, bez dodatkowych megabajtów.
Case study: Alamofire vs URLSession
Alamofire to chyba najpopularniejsza biblioteka w ekosystemie iOS. Większość tutoriali o networkingu ją wykorzystuje. Ale czy faktycznie jej potrzebujesz?
Co oferuje Alamofire?
- Eleganckie, chainowalne API,
- Automatyczna walidacja odpowiedzi,
- Logika ponawiania,
- Request/response interceptory,
- Upload/download ze śledzeniem progresu,
- Certificate pinning,
- Network reachability.
Czego większość projektów faktycznie używa?
- Wysłanie GET/POST requesta,
- Dekodowanie JSON do Codable,
- Obsługa błędów,
- Może proste ponawianie,
Natywne rozwiązanie (prosty przykład)
// Prosty klient API w ~50 liniach
enum APIError: Error {
case invalidURL
case noData
case decodingError(Error)
case httpError(statusCode: Int)
case networkError(Error)
}
final class APIClient {
private let session: URLSession
private let decoder: JSONDecoder
init(session: URLSession = .shared, decoder: JSONDecoder = JSONDecoder()) {
self.session = session
self.decoder = decoder
}
func request<T: Decodable>(
url: String,
method: String = "GET",
body: Data? = nil,
headers: [String: String] = [:]
) async throws -> T {
guard let url = URL(string: url) else {
throw APIError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = method
request.httpBody = body
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
if body != nil {
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
}
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.noData
}
guard (200...299).contains(httpResponse.statusCode) else {
throw APIError.httpError(statusCode: httpResponse.statusCode)
}
do {
return try decoder.decode(T.self, from: data)
} catch {
throw APIError.decodingError(error)
}
}
}
// Użycie
let client = APIClient()
let user: User = try await client.request(url: "https://api.example.com/user/1")
Kiedy Alamofire ma sens?
- Potrzebujesz zaawansowanego retry z exponential backoff,
- Masz skomplikowany flow autentykacji z odświeżaniem tokenów,
- Potrzebujesz interceptorów do logowania/modyfikacji requestów,
- Robisz dużo upload/download ze śledzeniem postępu.
Kiedy natywne rozwiązanie wystarczy?
- Standardowe CRUD operacje,
- Prosty flow autentykacji,
- Kilka lub kilkanaście endpointów,
- Aplikacja, gdzie networking nie jest główną funkcjonalnością.
Kryteria decyzyjne
Po latach prób i błędów wypracowałem sobie checklistę, którą przechodzę przed dodaniem każdej zależności do projektu:
1. Czy rozwiązuję rzeczywisty problem?
Brzmi banalnie, ale zaskakująco często dodajemy biblioteki "na zapas" albo dlatego, że tak robią wszyscy. Zanim sięgniesz po zależność, upewnij się że:
- Masz konkretny problem do rozwiązania - nie hipotetyczny,
- Natywne API faktycznie nie wystarczy (sprawdź dokumentację Apple, często dodają nowe możliwości),
- Problem jest na tyle złożony, że własna implementacja zajmie bardzo dużo czasu,
2. Jaki jest koszt własnej implementacji?
Policz realistycznie:
- Ile czasu zajmie napisanie?
- Ile czasu zajmie przetestowanie przypadków brzegowych?
- Czy masz wiedzę domenową (np. kryptografia, przetwarzanie obrazów)?
Jeśli odpowiedź na ostatnie pytanie to "nie" wtedy biblioteka jest prawdopodobnie lepszym wyborem. Nie pisz własnej kryptografii.
3. Jak wygląda kod i obsługa biblioteki?
Czerwone flagi:
- Ostatni commit > 6 miesięcy temu (chyba że biblioteka jest "skończona"),
- Jeden contributor,
- Dużo otwartych issues bez odpowiedzi,
- Brak dokumentacji migracji między wersjami,
- Brak testów.
Zielone flagi:
- Aktywny development,
- Kilku maintainerów lub backing dużej firmy,
- Semantic versioning,
- Changelog,
- Dobra dokumentacja.
4. Ile z tej biblioteki faktycznie użyję?
To kluczowe pytanie. Jeśli z Alamofire używasz tylko AF.request() to prawdopodobnie nie potrzebujesz Alamofire.
Jeśli używasz mniej niż 20% możliwości biblioteki, rozważ własną implementację tego fragmentu.
5. Jaki jest plan B?
Zawsze zadaj sobie pytanie: co jeśli ta biblioteka przestanie być rozwijana za 2 lata?
- Czy będę w stanie ją zforkować i utrzymywać?
- Czy migracja do alternatywy będzie prosta?
- Czy mogę ograniczyć coupling tak, żeby wymiana była lokalna?
Strategia ograniczania ryzyka
Nawet jeśli zdecydujesz się na bibliotekę, możesz ograniczyć ryzyko.
Wrapper/Adapter pattern
Nie używaj biblioteki bezpośrednio w całym kodzie. Stwórz własną warstwę abstrakcji:
// Zamiast używać Kingfisher bezpośrednio w 50 miejscach:
imageView.kf.setImage(with: url)
// Stwórz własny interfejs:
protocol ImageLoader {
func loadImage(from url: URL, into imageView: UIImageView)
}
final class KingfisherImageLoader: ImageLoader {
func loadImage(from url: URL, into imageView: UIImageView) {
imageView.kf.setImage(with: url)
}
}
// Jeśli Kingfisher zniknie, piszesz nową implementację:
final class NativeImageLoader: ImageLoader {
func loadImage(from url: URL, into imageView: UIImageView) {
// URLSession + cache
}
}
Koszt? Kilkanaście linii kodu. Zysk? Możliwość wymiany biblioteki w jeden dzień zamiast tygodnia.
Lockowanie wersji
Używaj konkretnych wersji, nie zakresów:
// Ryzykowne
.package(url: "...", from: "5.0.0")
// Bezpieczniejsze
.package(url: "...", exact: "5.6.2")
Tak, to wymaga ręcznych aktualizacji. Ale przynajmniej wiesz kiedy i co się zmienia - masz pełną kontrolę.
Narzędzia i proces audytu zależności
Teoria teorią, ale jak w praktyce sprawdzić stan swoich zależności? Raz na jakiś czas można przeprowadzić prosty audyt.
Krok 1: Przegląd drzewa zależności
Zacznij od zobaczenia co tak naprawdę masz w projekcie, w tym zależności przechodnie (dependencies Twoich dependencies):
swift package show-dependencies
Dla projektu z Xcode:
xcodebuild -project MyApp.xcodeproj -showdependencies
Często okazuje się, że jedna biblioteka ciągnie za sobą kilka innych. To dobry moment żeby zadać sobie pytanie: czy wiedziałem, że to wszystko jest w moim kodzie?
Krok 2: Analiza rozmiaru binarki
Chcesz wiedzieć ile kosztuje Cię każda biblioteka w MB? Emerge Tools (emergetools.com) oferuje darmową analizę dla open source projektów. Wrzucasz IPA, dostajesz breakdown:
- Rozmiar każdego frameworka,
- Porównanie między wersjami,
- Podział na kod vs zasoby.
Alternatywnie, prosty test ręczny: usuń bibliotekę, zbuduj archiwum, porównaj rozmiar. Czasochłonne, ale działa.
Dla szybkiego podglądu lokalnie możesz też sprawdzić folder DerivedData po buildzie:
# Po buildzie projektu
find ~/Library/Developer/Xcode/DerivedData/MyApp-*/Build/Products -name "*.framework" -exec du -sh {} \;
Krok 3: Sprawdzanie zdrowia bibliotek
Dla każdej zależności sprawdź na GitHubie:
Ostatnia aktywność:
- Ostatni commit < 3 miesiące → aktywnie rozwijana,
- 3-12 miesięcy → może być stabilna, może porzucona,
- więcej niż 12 miesięcy → czerwona flaga (chyba że to mała, "skończona" biblioteka).
Bus factor:
- 1 contributor → ryzyko,
- 3+ aktywnych → bezpieczniej,
- Backing firmy (Alamofire → Alamofire Software Foundation) → jeszcze lepiej.
Issues i PR-y:
- Dużo otwartych issues bez odpowiedzi → maintainer nie wyrabia,
- PR-y wiszące miesiącami → rozwój utknął.
Aby nie sprawdzać tego wszystkiego ręcznie co tydzień, warto skorzystać z narzędzi, które automatyzują wykrywanie starych i porzuconych paczek:
- Dependabot (wbudowany w GitHub) - automatycznie otwiera pull requesty z aktualizacjami zależności Swift (od 2023 działa bardzo dobrze z SPM), zgłasza też security alerts. Konfiguracja przez prosty plik
.github/dependabot.yml. - Swift Package Index - świetne miejsce do szybkiego sprawdzenia statusu paczki (ostatnie releasy, liczba gwiazdek, activity score). Można też użyć CLI typu
swift-outdated, które pokaże Ci lokalnie, które pakiety są przestarzałe względem wymagań wPackage.swift.
Dzięki nim audyt staje się dużo mniej bolesny - zamiast scrollować 20 repo na GitHubie, dostajesz powiadomienia i gotowe PR-y.
Krok 4: Analiza czasu kompilacji
Sprawdź, które zależności najbardziej spowalniają build:
# Włącz szczegółowe logi czasu kompilacji
defaults write com.apple.dt.Xcode ShowBuildOperationDuration -bool YES
Po buildzie w Xcode, w Build Log zobaczysz czas każdego targetu.
Albo z linii poleceń:
xcodebuild -project MyApp.xcodeproj \
-scheme MyApp \
-showBuildTimingSummary \
clean build
Na końcu dostaniesz podsumowanie - jeśli jakaś biblioteka kompiluje się więcej niż 30 sekund, a używasz z niej jedną funkcję, warto się zastanowić.
Moja checklista kwartalnego audytu
- Uruchom
swift package show-dependencies- czy są niespodzianki? - Sprawdź GitHub każdej zależności - ostatni commit, open issues,
- Porównaj rozmiar IPA z poprzednim okresem,
- Przejrzyj czas kompilacji, czy coś się pogorszyło?
- Dla każdej biblioteki: czy nadal jej używam? Czy jest natywna alternatywa?
Takie sprawdzenie co jakiś czas może zaoszczędzić kupe czasu przy następnej aktualizacji iOS.
Biblioteki, które warto rozważyć vs napisać samemu
Na podstawie mojego doświadczenia:
Raczej weź bibliotekę
- Kryptografia - nigdy nie pisz własnej,
- Obsługa dat/stref czasowych - zbyt wiele przypadków brzegowych,
- Parsowanie skomplikowanych formatów (XML, Markdown) - mnóstwo przypadków brzegowych,
- Analytics SDK - integracja z backendem providera.
Raczej napisz sam
- Prosty networking - URLSession + async/await w zupełności wystarczą,
- Podstawowy image cache - NSCache + FileManager,
- Proste UI komponenty - UIKit/SwiftUI są wystarczająco elastyczne,
- Dependency injection - protokoły + konstruktory albo prosty ServiceLocator, nie potrzebujesz Swinject,
- Logowanie - OSLog, nie potrzebujesz CocoaLumberjack,
Jakie biblioteki często pojawiają się w moich projektach?
Jest pewien zbiór zależności, który często powtarza się w projektach, w których biorę udział, a mianowicie:
- Firebase/Sentry,
- Lottie,
- Inject,
- SwiftLint,
- Swift Collections,
- Quick + Nimble (ostatnio rzadziej).
Podsumowanie
Nie chodzi o to, żeby unikać bibliotek za wszelką cenę. Chodzi o świadome decyzje.
Każda zależność to trade-off: czas zaoszczędzony teraz kontra potencjalny dług technologiczny w przyszłości. Czasem ten trade-off jest oczywisty (kryptografia). Czasem wymaga przemyślenia (networking). Czasem odpowiedź brzmi "napisz sam" (prosty DI).
Moja zasada: domyślnie natywne rozwiązanie, biblioteka gdy jest konkretny powód.
Ten powód to nie wszyscy tego używają ani tak jest w tutorialu.
To: rozwiązuje złożony problem, którego sam nie chcę/nie umiem rozwiązać, jest aktywnie rozwijana, a coupling mogę ograniczyć wrapperem.
Ile zależności ma Twój obecny projekt? Kiedy ostatnio sprawdzałeś, czy wszystkie są potrzebne?

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ł: