20 często zadawanych pytań rekrutacyjnych z iOS. iOS development to poszukiwana dziedzina skupiona na aplikacjach w Swift, SwiftUI i UIKit oraz na lifecycle, współbieżności, sieci, przechowywaniu danych, wydajności, testach i publikacji w App Store. Pytania obejmują różne poziomy, a odpowiedzi możesz ćwiczyć na głos w naszym trenerze rozmów.
1Jakie odpowiedzialności powinien mieć UIViewController w typowej architekturze iOS MVC (Model-View-Controller)?
W klasycznym wzorcu iOS MVC (Model-View-Controller) `UIViewController` pełni rolę kontrolera pośredniczącego między komponentami widoku (View) a danymi modelu (Model). Do jego głównych odpowiedzialności należy zarządzanie cyklem życia widoku (np. `viewDidLoad`, `viewWillAppear`), konfigurowanie i aktualizowanie elementów interfejsu użytkownika, obsługa bezpośrednich interakcji użytkownika (takich jak stuknięcia w przyciski, delegacje i mechanizmy target-action) oraz koordynowanie przejść i prezentacji ekranów. Odpowiedzialności niezwiązane z interfejsem użytkownika — takie jak bezpośrednia komunikacja sieciowa, utrwalanie danych czy złożona logika biznesowa — powinny być delegowane do dedykowanych serwisów lub obiektów modelu, aby zapobiec przekształceniu kontrolera widoku w tzw. Massive View Controller.
class UserProfileViewController: UIViewController {
private let userService: UserServiceProtocol
private let profileView = UserProfileView()
init(userService: UserServiceProtocol) {
self.userService = userService
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func loadView() {
view = profileView
}
override func viewDidLoad() {
super.viewDidLoad()
profileView.editButton.addTarget(self, action: #selector(didTapEdit), for: .touchUpInside)
fetchProfile()
}
private func fetchProfile() {
userService.fetchCurrentUser { [weak self] result in
DispatchQueue.main.async {
if case .success(let user) = result {
self?.profileView.configure(with: user)
}
}
}
}
@objc private func didTapEdit() {
// Handle user action or trigger navigation
}
}
2Jaka jest rola profilu provisioningowego (provisioning profile) w wydawaniu aplikacji na platformę iOS?
W procesie wydawania aplikacji na platformę iOS profil provisioningowy (provisioning profile) działa jako kryptograficznie podpisany pakiet, który łączy wymagania bezpieczeństwa Apple z plikiem binarnym aplikacji i urządzeniami docelowymi. Jego głównym zadaniem jest poinformowanie systemu operacyjnego iOS oraz usługi App Store Connect, że aplikacja ma uprawnienia do uruchamiania się zgodnie z określonymi regułami dystrybucji. Profil provisioningowy łączy kilka kluczowych elementów: identyfikator aplikacji (App ID / Bundle Identifier), autoryzowane certyfikaty podpisywania kodu potwierdzające tożsamość twórcy aplikacji, uprawnienia i możliwości (entitlements/capabilities) przyznane aplikacji (takie jak Push Notifications, Sign in with Apple czy iCloud) oraz — w profilach innych niż przeznaczone dla App Store, np. Development lub Ad-Hoc — listę dozwolonych identyfikatorów UDID (Unique Device Identifier) urządzeń. W przypadku profili wydawniczych dla App Store pojedyncze identyfikatory UDID są pomijane, ponieważ Apple zezwala na dystrybucję do dowolnego urządzenia konsumenckiego za pośrednictwem sklepu App Store.
Provisioning Profile (.mobileprovision)
├── App ID / Bundle Identifier (e.g., com.example.app)
├── Certificates (Public key / Distribution Certificate)
├── Entitlements (e.g., Push Notifications, Associated Domains)
└── Device UDIDs (Present for Development/Ad-Hoc; omitted for App Store distribution)
3Co oznacza, że aplikacja w systemie iOS wykonuje zadania w tle, i jakie podstawowe ograniczenia system nakłada na tę pracę?
Wykonywanie pracy w tle w systemie iOS oznacza uruchamianie kodu w momencie, gdy aplikacja nie jest aktywna na ekranie ani widoczna dla użytkownika. Kiedy użytkownik opuszcza aplikację, przechodzi ona ze stanu aktywnego na pierwszym planie do stanu działania w tle, a niedługo potem do stanu wstrzymania (suspended), w którym jej wykonywanie jest zatrzymane, chociaż pamięć podręczna pozostaje w pamięci RAM. Aby oszczędzać baterię, zapewnić responsywność systemu i chronić zasoby urządzenia, iOS ściśle kontroluje wykonywanie kodu w tle. System ogranicza czas wykonywania kodu po przejściu do tła (zazwyczaj do krótkiego okna czasowego wynoszącego około 30 sekund, chyba że używane są dedykowane tryby pracy w tle lub zaplanowane zadania), ogranicza priorytet procesora CPU oraz przepustowość sieci, a także może wymusić zamknięcie wstrzymanych aplikacji, jeśli na urządzeniu wystąpi presja pamięci.
4Czym jest obiekt publikujący (publisher) w frameworku Combine i czym różni się od subskrybenta (subscriber) w typowym przepływie danych w systemie iOS?
W frameworku Combine obiekt publikujący (Publisher) emituje strumień wartości w czasie i może zakończyć działanie zdarzeniem ukończenia (kończąc pracę pomyślnie lub zgłaszając błąd). Deklaruje on dwa typy skojarzone: `Output` (typ danych, które wytwarza) oraz `Failure` (typ błędu, jaki może wyemitować). Subskrybent (Subscriber) odbiera wartości oraz zdarzenia cyklu życia od obiektu publikującego. W typowym przepływie danych w systemie iOS obiekty publikujące pełnią rolę źródła lub producenta zdarzeń asynchronicznych (takich jak odpowiedzi sieciowe, powiadomienia czy dane wejściowe użytkownika), podczas gdy subskrybenci działają jako konsumenci, którzy reagują na wyemitowane wartości, obsługują błędy i odpowiadają na zakończenie strumienia (np. aktualizując stan interfejsu użytkownika lub zapisując dane w bazie danych). Subskrybent dołącza się do publikującego, otrzymuje token subskrypcji i zgłasza zapotrzebowanie na określoną liczbę elementów.
import Combine
// Publisher: emits a sequence of integers
let numberPublisher = [1, 2, 3].publisher
// Subscriber: consumes the emitted integers
let cancellable = numberPublisher.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("Finished")
case .failure(let error):
print("Error: \(error)")
}
},
receiveValue: { value in
print("Received: \(value)")
}
)
5Co oznacza składnia async/await w języku Swift i jak użyć jej do wywołania asynchronicznego API (Application Programming Interface) z poziomu aplikacji na system iOS?
`async/await` to wbudowana składnia języka Swift służąca do pisania kodu asynchronicznego w liniowy, czytelny i sekwencyjny sposób, zamiast stosowania zagnieżdżonych domknięć (closures) i funkcji zwrotnych zakończenia (completion handlers). Oznaczenie funkcji słowem kluczowym `async` informuje kompilator, że funkcja może zawiesić swoje wykonywanie podczas oczekiwania na zakończenie długotrwałych operacji (takich jak żądania sieciowe czy operacje wejścia/wyjścia na plikach). Słowo kluczowe `await` wskazuje punkt zawieszenia (suspension point): wykonywanie bieżącej funkcji zostaje wstrzymane, co zwalnia bazowy wątek do wykonywania innych zadań, a następnie wznawia się, gdy oczekiwany wynik lub błąd jest gotowy. Aby wywołać asynchroniczne API w aplikacji na iOS, wywołuje się metodę `async` poprzedzoną słowem kluczowym `await` (lub `try await`, jeśli funkcja rzuca błędy) z poziomu kontekstu asynchronicznego, na przykład wewnątrz innej funkcji `async` lub bloku `Task`.
6Kiedy należy używać UserDefaults w aplikacji na system iOS i jakich rodzajów danych nie powinno się tam przechowywać?
UserDefaults jest przeznaczone do przechowywania małych, lekkich preferencji użytkownika, ustawień i flag między uruchomieniami aplikacji (na przykład preferencji motywu, poziomu głośności czy flagi logicznej `hasSeenOnboarding`). Obsługuje typy list właściwości (property list), takie jak String, Int, Double, Bool, Date, Data, Array oraz Dictionary. NIE należy przechowywać: 1. Informacji poufnych (takich jak hasła użytkowników, klucze prywatne czy tokeny uwierzytelniające), ponieważ UserDefaults przechowuje dane w postaci niezaszyfrowanej w pliku plist w formacie zwykłego tekstu. 2. Dużych zbiorów danych, dokumentów lub plików multimedialnych (takich jak obrazy, pliki audio czy duże ładunki danych w formacie JSON – JavaScript Object Notation), ponieważ cały plik plist jest wczytywany do pamięci podczas dostępu, co prowadzi do dużego narzutu pamięciowego i spowalnia uruchamianie aplikacji.
// Saving simple preference values
UserDefaults.standard.set(true, forKey: "hasCompletedOnboarding")
UserDefaults.standard.set("dark", forKey: "preferredTheme")
// Reading values back
let hasCompletedOnboarding = UserDefaults.standard.bool(forKey: "hasCompletedOnboarding")
let theme = UserDefaults.standard.string(forKey: "preferredTheme") ?? "system"
7Czym jest ARC (Automatic Reference Counting) w języku Swift i w jaki sposób zarządza czasem życia instancji klas w aplikacji na system iOS?
ARC (Automatic Reference Counting) to działający na etapie kompilacji mechanizm zarządzania pamięcią w języku Swift, służący do śledzenia i zarządzania czasem życia instancji klas (typów referencyjnych). Za każdym razem, gdy tworzona jest nowa instancja klasy i przypisywana do silnej referencji (strong reference), ARC śledzi wewnętrzny licznik referencji (refcount) dla tej instancji. Dopóki istnieje co najmniej jedna silna referencja do instancji, ARC utrzymuje ją w pamięci. Gdy wszystkie silne referencje zostaną usunięte i licznik referencji spadnie do zera, ARC natychmiast dealokuje instancję w celu zwolnienia pamięci, automatycznie wywołując metodę `deinit` instancji tuż przed jej usunięciem. W przeciwieństwie do mechanizmu automatycznego odśmiecania pamięci (garbage collection) działającego w środowiskach uruchomieniowych takich jak Java czy .NET, ARC nie wykonuje okresowych cykli czyszczenia; kompilator języka Swift wstawia odpowiednie wywołania retain i release bezpośrednio do kodu binarnego na etapie kompilacji.
class User {
let name: String
init(name: String) {
self.name = name
print("\(name) initialized")
}
deinit {
print("\(name) deallocated")
}
}
var ref1: User? = User(name: "Alice") // count = 1
var ref2: User? = ref1 // count = 2
ref1 = nil // count = 1
ref2 = nil // count = 0 -> deinit called
8Jak wykonać proste żądanie HTTP (Hypertext Transfer Protocol) GET za pomocą URLSession w aplikacji na system iOS?
Aby wykonać proste żądanie HTTP GET w aplikacji na system iOS, tworzy się obiekt `URL` i przekazuje go do instancji `URLSession`, na przykład `URLSession.shared`. Żądanie można wykonać przy użyciu procedury obsługi zakończenia zadania danych (`URLSession.shared.dataTask(with: url) { data, response, error in ... }`) lub za pomocą nowoczesnego mechanizmu współbieżności języka Swift (`let (data, response) = try await URLSession.shared.data(from: url)`). W przypadku tradycyjnego podejścia opartego na zadaniu danych (`dataTask`) należy wywołać metodę `.resume()` na utworzonym zadaniu, aby je rozpocząć, obsłużyć ewentualny błąd transportowy, sprawdzić kod statusu `HTTPURLResponse` oraz upewnić się, że wszelkie aktualizacje interfejsu użytkownika (UI) są przekazywane do wątku głównego.
guard let url = URL(string: "https://api.example.com/items") else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print("Network error: \(error.localizedDescription)")
return
}
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
print("Server error or invalid status code")
return
}
if let data = data {
DispatchQueue.main.async {
// Update UI on main thread
}
}
}
task.resume()
9Czym jest narzędzie Instruments w Xcode i jak można go użyć do zbadania prostego problemu z wydajnością w aplikacji na system iOS?
Instruments to narzędzie firmy Apple do profilowania i analizy wydajności, dołączone do środowiska Xcode. Umożliwia ono programistom monitorowanie zachowania aplikacji w czasie działania, obciążenia procesora (CPU), alokacji pamięci, wycieków pamięci oraz wydajności renderowania interfejsu użytkownika (UI). Aby zbadać prosty problem z wydajnością (taki jak przycinanie animacji interfejsu czy wysokie zużycie CPU), uruchamia się Instruments z poziomu Xcode (poprzez Product -> Profile), wybiera odpowiedni szablon (np. Time Profiler do badania wąskich gardeł CPU lub Allocations/Leaks do analizy pamięci) i rejestruje ślad (trace) podczas odtwarzania problematycznego scenariusza użytkownika na urządzeniu. Po zakończeniu nagrywania analizuje się drzewo wywołań (call tree) oraz najbardziej obciążające ślady stosu (heaviest stack traces), aby precyzyjnie zlokalizować metody powodujące opóźnienia lub nadmierne zużycie zasobów, co pozwala zmierzyć i zidentyfikować wąskie gardło przed wprowadzeniem modyfikacji w kodzie.
1. In Xcode, select Product -> Profile (Builds with Release optimizations).
2. Select the 'Time Profiler' template.
3. Click Record and perform the sluggish action in the app.
4. Stop recording.
5. Inspect the Call Tree tab:
- Enable 'Separate by Thread' and 'Hide System Libraries'.
- Trace the heaviest call stack on the Main Thread to identify the blocking function.
10Czym jest APNs (Apple Push Notification service) i jaką rolę odgrywa w dostarczaniu powiadomień push do aplikacji w systemie iOS?
APNs (Apple Push Notification service) to chmurowa usługa pośrednicząca firmy Apple, która bezpiecznie przekazuje powiadomienia push z serwerów backendowych (serwerów dostawcy) do urządzeń Apple. Ponieważ urządzenia mobilne nie mogą utrzymywać ciągłych, otwartych połączeń gniazdowych (socket) z backendem każdej pojedynczej aplikacji bez rozładowywania baterii i nadmiernego zużycia przepustowości sieci, Apple utrzymuje jedno trwałe, energooszczędne połączenie między każdym urządzeniem a APNs. Gdy aplikacja rejestruje się do odbierania powiadomień zdalnych, APNs generuje unikalny, nieprzezroczysty token urządzenia (device token) identyfikujący tę konkretną instalację aplikacji na danym urządzeniu. Aplikacja przesyła ten token do Twojego backendowego serwera dostawcy. Gdy dostawca chce powiadomić użytkownika, tworzy ładunek (payload) i wysyła go wraz z tokenem urządzenia do APNs za pośrednictwem protokołu HTTP/2. Następnie APNs lokalizuje aktywne połączenie dla danego urządzenia i dostarcza ładunek powiadomienia push bezpośrednio do systemu iOS, który wyświetla alert lub wybudza aplikację zgodnie z konfiguracją.
11W jaki sposób przeprowadziłbyś refaktoryzację rozbudowanej klasy UIViewController, która odpowiada za aktualizacje interfejsu użytkownika UI (User Interface), walidację, komunikację sieciową oraz nawigację?
Refaktoryzację przerośniętego kontrolera (Massive View Controller) należy przeprowadzać przyrostowo, aby zminimalizować ryzyko, rozdzielając poszczególne odpowiedzialności do dedykowanych warstw: 1. Wyodrębnienie warstwy sieciowej i danych: Przenieś wywołania API (Application Programming Interface), trwałość danych i parsowanie JSON poza kontroler widoku do dedykowanych klas typu Service lub Repository, korzystając z abstrakcji opartych na protokołach. 2. Wyodrębnienie stanu prezentacji i walidacji: Wprowadź ViewModel (lub Presenter). Przenieś walidację danych wejściowych, formatowanie ciągów znaków oraz zarządzanie stanem UI do ViewModelu, co umożliwi izolowane testowanie jednostkowe tej logiki. 3. Wyodrębnienie nawigacji: Zastosuj wzorzec Coordinator (lub Router / Flow Controller), aby usunąć wywołania push/present oraz logikę przepływu nawigacji z kontrolera widoku. 4. Uproszczenie kontrolera widoku: Pozostaw w kontrolerze wyłącznie układ interfejsu użytkownika, konfigurację podelementów widoku, metody cyklu życia oraz wiązanie kontrolek UI z ViewModelem. 5. Przyrostowa realizacja z testami: Twórz testy jednostkowe dla nowo wyodrębnionych serwisów i ViewModeli w trakcie refaktoryzacji, aby zagwarantować niezmienność dotychczasowego działania aplikacji.
// 1. Extracted Navigation
protocol LoginCoordinatorProtocol: AnyObject {
func showHomeScreen()
}
// 2. Extracted Business Logic & State
final class LoginViewModel {
private let authService: AuthServiceProtocol
private weak var coordinator: LoginCoordinatorProtocol?
init(authService: AuthServiceProtocol, coordinator: LoginCoordinatorProtocol) {
self.authService = authService
self.coordinator = coordinator
}
func login(email: String, password: String) {
guard email.contains("@") else { return }
authService.login(email: email, password: password) { [weak self] result in
if case .success = result { self?.coordinator?.showHomeScreen() }
}
}
}
// 3. Lean UIViewController: Only handles UI and bindings
final class LoginViewController: UIViewController {
private let viewModel: LoginViewModel
init(viewModel: LoginViewModel) { self.viewModel = viewModel; super.init(nibName: nil, bundle: nil) }
required init?(coder: NSCoder) { fatalError() }
}
12W jaki sposób zdiagnozujesz i rozwiążesz problem z nieudanym wysłaniem archiwum iOS (archive upload failure) spowodowany błędami podpisywania kodu lub profilu provisioning?
Rozwiązywanie problemów z wysyłaniem archiwum iOS spowodowanych błędami podpisywania lub profilu provisioning obejmuje weryfikację trzech głównych obszarów: certyfikatów dystrybucyjnych, profili provisioning oraz uprawnień aplikacji (target entitlements). 1. Analiza logów diagnostycznych: Przejrzyj logi dystrybucyjne w Xcode Organizer, szczegółowe błędy walidacji podczas wysyłania lub logi konsoli CI (Continuous Integration) (`altool`/`notarytool`), aby poznać dokładną przyczynę odrzucenia. 2. Weryfikacja certyfikatu i tożsamości: Upewnij się, że ważny certyfikat Apple Distribution znajduje się w pęku kluczy (Keychain) wraz z pasującym kluczem prywatnym oraz że nie wygasł, nie został unieważniony ani nie brakuje certyfikatu pośredniczącego Apple WWDR. 3. Dopasowanie profilu provisioning: Sprawdź, czy profil używany do eksportu to profil dystrybucyjny App Store (App Store Distribution), odpowiadający dokładnie identyfikatorowi pakietu (Bundle Identifier). Jeśli projekt korzysta z takich funkcji jak powiadomienia push (Push Notifications) lub powiązane domeny (Associated Domains), upewnij się, że skonfigurowano jednoznaczny App ID zamiast niezgodnego identyfikatora z symbolem wieloznacznym (wildcard). 4. Spójność uprawnień (Entitlements): Sprawdź rozbieżności między plikiem `.entitlements` w projekcie a uprawnieniami włączonymi dla danego App ID w portalu Apple Developer. Jeśli lokalne uprawnienia deklarują funkcje niezarejestrowane w profilu na portalu, wysyłanie zakończy się niepowodzeniem. 5. Konfiguracja podpisywania: W przypadku automatycznego zarządzania podpisywaniem zweryfikuj wybrany zespół (Team) oraz dane uwierzytelniające Apple ID w ustawieniach Xcode. W przypadku podpisywania ręcznego (lub opcji eksportu w środowisku CI) upewnij się, że plik `ExportOptions.plist` prawidłowo mapuje każdy Bundle ID na właściwy certyfikat dystrybucyjny i profil.
# Decode the provisioning profile embedded in the archive
security cms -D -i MyApp.xcarchive/Products/Applications/MyApp.app/embedded.mobileprovision > profile.plist
# Read entitlements embedded in the profile
/usr/libexec/PlistBuddy -c "Print :Entitlements" profile.plist
# Inspect signature and entitlements on the built binary
codesign -d --entitlements :- MyApp.xcarchive/Products/Applications/MyApp.app
13Jak dokonać wyboru pomiędzy BGAppRefreshTask, BGProcessingTask a działającą w tle sesją URLSession na potrzeby produkcyjnej funkcji synchronizacji w tle?
Wybór właściwego interfejsu API do pracy w tle zależy od czasu trwania zadania, wymagań dotyczących zasilania/sieci oraz charakteru przesyłanych danych: 1. **BGAppRefreshTask**: Najlepszy do krótkich, lekkich aktualizacji treści (takich jak odświeżanie kanałów wiadomości, osi czasu w mediach społecznościowych czy paneli użytkownika), które trwają około 15–30 sekund. System planuje je na podstawie wzorców korzystania z aplikacji przez użytkownika, aby świeże dane były gotowe tuż przed typowym otwarciem aplikacji. 2. **BGProcessingTask**: Zaprojektowany do długotrwałych, niecierpiących zwłoki i wymagających operacji, takich jak indeksowanie danych, czyszczenie lub migracje baz danych, trenowanie modeli uczenia maszynowego (ML) czy synchronizacja dużych wolumenów danych. Może działać przez kilka minut i wymagać podłączenia urządzenia do ładowania (`requiresExternalPower = true`) oraz sieci Wi-Fi/komórkowej (`requiresNetworkConnectivity = true`), zazwyczaj wykonując się w nocy. 3. **Działająca w tle sesja URLSession (`URLSessionConfiguration.background`)**: Właściwy wybór do przesyłania dużych plików (wysyłanie zdjęć/wideo lub pobieranie dużych paczek zasobów), które muszą być kontynuowane poza procesem aplikacji, nawet jeśli zostanie ona zawieszona lub zamknięta przez system operacyjny. Oddelegowuje transfer sieciowy do demona `nsurlsessiond`, zamiast utrzymywać aktywny kod aplikacji w pamięci.
14Jak zbudować potok (pipeline) w Combine dla tekstowego pola wyszukiwania, który redukuje częstotliwość emisji zdarzeń (debounce), ignoruje duplikaty, wykonuje zapytanie sieciowe i bezpiecznie aktualizuje UI (User Interface)?
Aby zbudować bezpieczny potok wyszukiwania w Combine: 1. **Debounce i usuwanie duplikatów:** Pobierz obiekt publikujący (publisher) dla zapytania wyszukiwania (np. `$queryText`), zastosuj `.debounce(for: .milliseconds(300), scheduler: RunLoop.main)`, aby odczekać na przerwy w pisaniu, oraz `.removeDuplicates()`, aby ignorować niezmienione ciągi zapytań. 2. **Zapytanie sieciowe i anulowanie:** Przekształć każde zapytanie na obiekt publikujący zapytania sieciowego i spłaszcz strumień za pomocą `.switchToLatest()`. Automatycznie anuluje to trwające zapytania, gdy pojawi się nowe zapytanie wyszukiwania, zapobiegając wyścigom (race conditions) i nieaktualnym wynikom. 3. **Izolacja błędów:** Przechwytuj błędy sieciowe wewnątrz wewnętrznego obiektu publikującego (np. `.catch { _ in Just([]) }`), aby awarie sieci nie przerywały głównego strumienia tekstu wyszukiwania. 4. **Dostarczanie do wątku głównego:** Zastosuj `.receive(on: DispatchQueue.main)` przed aktualizacją stanu lub przypisaniem danych do właściwości interfejsu użytkownika.
15Jak wykorzystałbyś mechanizm async/await oraz anulowanie obiektów Task w języku Swift, aby zaimplementować ekran ładujący dane z sieci i zapobiec aktualizacji UI (User Interface) po opuszczeniu ekranu przez użytkownika?
Zaprojektowałbym pobieranie danych z sieci jako funkcję `async` w serwisie lub modelu widoku (view model) i uruchamiał ją z poziomu `Task`, którego czas życia jest powiązany z ekranem. W UIKit oznacza to zazwyczaj przechowywanie właściwości w rodzaju `var loadTask: Task<Void, Never>?` w kontrolerze widoku lub modelu widoku, uruchamianie jej w momencie, gdy ekran powinien załadować dane, oraz jej anulowanie w `viewWillDisappear`, `deinit` lub przed rozpoczęciem nowszego żądania, w zależności od pożądanego czasu życia. W SwiftUI preferowałbym użycie `.task` lub `.task(id:)`, ponieważ SwiftUI automatycznie anuluje zadanie, gdy widok znika lub gdy zmienia się identyfikator. Wewnątrz zadania wywoływałbym asynchroniczne API sieciowe za pomocą `try await`, obsługiwał anulowanie oddzielnie od rzeczywistych błędów i sprawdzał stan anulowania przed zastosowaniem wyników, jeśli występuje wiele operacji await lub kroków przetwarzania. Zmiany stanu UI muszą odbywać się w głównym aktorze (main actor), albo poprzez oznaczenie modelu widoku adnotacją `@MainActor`, albo za pomocą `await MainActor.run`. Aby uniknąć nieaktualnego stanu UI (stale UI), anulowałbym poprzednie zadania, unikał odłączonych/globalnych zadań (detached/global tasks) dla pracy powiązanej z konkretnym ekranem oraz opcjonalnie porównywał identyfikator żądania lub bieżącego elementu przed zastosowaniem wyniku.
@MainActor
final class UsersViewController: UIViewController {
private var loadTask: Task<Void, Never>?
private let service: UserService
override func viewDidLoad() {
super.viewDidLoad()
loadUsers()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
loadTask?.cancel()
}
private func loadUsers() {
loadTask?.cancel()
loadingView.isHidden = false
loadTask = Task { [weak self] in
do {
let users = try await self?.service.fetchUsers() ?? []
try Task.checkCancellation()
guard let self else { return }
self.loadingView.isHidden = true
self.tableModel = users
self.tableView.reloadData()
} catch is CancellationError {
// User navigated away or a new load replaced this one; do not update UI.
} catch {
guard let self else { return }
self.loadingView.isHidden = true
self.showError(error)
}
}
}
}
16Czym się kierować przy wyborze pomiędzy UserDefaults, Keychain, plikami, SQLite i Core Data dla różnych rodzajów danych aplikacji?
Wybór odpowiedniego mechanizmu przechowywania danych w iOS zależy od wrażliwości danych, ich rozmiaru, złożoności relacji, wymagań dotyczących zapytań oraz cyklu życia kopii zapasowych: 1. **Keychain**: Dla danych wrażliwych (tokeny uwierzytelniające, hasła, klucze szyfrujące, dane biometryczne). Zapewnia szyfrowanie sprzętowe, zachowuje dane po reinstalacji aplikacji i pozostaje bezpieczny nawet w przypadku naruszenia piaskownicy (sandbox). 2. **UserDefaults**: Dla lekkich, niewrażliwych preferencji i flag (np. `hasCompletedOnboarding`, preferowany motyw). Ponieważ ładuje się w całości do pamięci jako lista właściwości (property list), nie powinien być używany do dużych zbiorów danych, multimediów ani wrażliwych tokenów. 3. **System plików (Documents / Caches / Application Support)**: Dla pojedynczych plików i dużych obiektów binarnych (pobrane obrazy, audio, pliki PDF). Katalog `Documents` jest dostępny dla użytkownika i kopiowany do iCloud; `Caches` służy do przechowywania danych, które można usunąć i pobrać ponownie; `Application Support` przeznaczony jest dla trwałych plików niewidocznych bezpośrednio dla użytkownika. 4. **SQLite**: Dla ustrukturyzowanych danych tabelarycznych wymagających szybkich zapytań indeksowanych, operacji masowych lub współdzielenia bazy danych między platformami bez narzutu grafu obiektów. 5. **Core Data / SwiftData**: Dla złożonych grafów obiektów z relacjami, śledzeniem zmian, mechanizmem leniwego ładowania (faulting), zarządzaniem cofaniem zmian (undo) oraz bezpośrednią integracją z UIKit (`NSFetchedResultsController`) lub SwiftUI (`@Query`).
17W produkcyjnym ekranie UIKit korzystającym z wywołań zwrotnych (callbacks) z modelu widoku (view model), jak decydujesz, czy przechwycić self jako weak, unowned czy strong w domknięciach?
W architekturze UIKit, gdzie kontroler widoku (view controller) komunikuje się z modelem widoku (view model) za pomocą wywołań zwrotnych, strategia przechwytywania referencji wynika z prawa własności oraz czasu życia obiektów:
1. `[weak self]`: Standardowe i najbezpieczniejsze podejście dla domknięć przechowywanych lub uciekających (escaping closures), takich jak wywołania zwrotne zdarzeń modelu widoku czy asynchroniczne operacje sieciowe/bazodanowe. Ponieważ kontroler widoku jest właścicielem modelu widoku, silne przechwycenie `self` w domknięciu zapisanym przez model widoku tworzy cykl silnych referencji (`VC -> VM -> closure -> VC`). `[weak self]` zamienia `self` na typ opcjonalny (`UIViewController?`), co pozwala na bezpieczne zwolnienie `self` z pamięci i zapobiega wyciekom pamięci.
2. `[unowned self]`: Zakłada, że `self` nigdy nie będzie mieć wartości `nil` w momencie wykonywania domknięcia. W ekranach UI należy stosować je ze szczególną ostrożnością. Jeśli zadanie asynchroniczne lub wywołanie zwrotne zakończy się po zamknięciu i dealokacji kontrolera widoku, odwołanie się do `unowned self` spowoduje błąd krytyczny i awarię aplikacji w czasie wykonywania (runtime crash). Z tego względu w produkcyjnym kodzie UIKit `[weak self]` jest zazwyczaj preferowane nad `unowned`.
3. **Silne przechwycenie (domyślne, brak capture list):** Odpowiednie w przypadku domknięć nieuciekających (non-escaping, np. standardowe operacje na kolekcjach takie jak `map`/`filter`) lub gdy domknięcie jest krótkotrwałe i nie tworzy cyklu własności powracającego do `self`.
final class ProfileViewController: UIViewController {
private let viewModel: ProfileViewModel
init(viewModel: ProfileViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError() }
override func viewDidLoad() {
super.viewDidLoad()
// VM stores the callback -> capture weak self to break retain cycle
viewModel.onDataUpdated = { [weak self] state in
guard let self else { return }
self.updateUI(with: state)
}
}
private func updateUI(with state: ProfileState) { /* Update views */ }
}
18Dołączasz do dojrzałej aplikacji na system iOS z setkami ekranów, wieloma nadmiernie rozbudowanymi kontrolerami widoków (massive view controllers) i powolnymi cyklami wydań. Jak zaplanowałbyś strategię stopniowego ulepszania architektury bez wstrzymywania dostarczania nowych funkcji?
Skuteczna strategia stopniowej migracji unika przepisywania aplikacji od zera (full rewrite) i łączy refaktoryzację bezpośrednio z bieżącym dostarczaniem funkcji biznesowych (zgodnie ze wzorcem Strangler Fig). Pracę rozpoczyna się od ustalenia uzgodnionego docelowego wzorca architektury (np. MVVM lub VIPER ze wstrzykiwaniem zależności i modułowymi koordynatorami) oraz przygotowania bazowych zestawów testów (testy charakteryzujące, migawkowe/snapshot i jednostkowe) wokół legacy code przed wprowadzeniem zmian.
Priorytetyzacja powinna wynikać z częstotliwości modyfikacji kodu oraz poziomu ryzyka: refaktoryzuj ekrany, które zespoły aktywnie modyfikują lub obszary o wysokim wskaźniku błędów, zamiast stabilnego, nieruszanego kodu zastanego. Podczas refaktoryzacji nadmiernie rozbudowanych kontrolerów widoków (Massive View Controllers) oddziel logikę biznesową i prezentacji do dedykowanych obiektów ViewModel/Presenter oraz wprowadź oparte na protokołach adaptery lub koordynatory, aby odizolować stary interfejs od nowego kodu.
Na koniec utrzymaj tempo pracy i jakość poprzez nadzór architektoniczny: zapewnij wzorcowe implementacje referencyjne, egzekwuj granice modułów za pomocą linterów w procesie CI lub wytycznych code review w Pull Requestach oraz przeznacz stałą pulę czasu na dług techniczny (np. 15–20% capacity sprintu lub łączenie refaktoryzacji z powiązanymi zadaniami funkcjonalnymi), monitorując jednocześnie metryki takie jak czas budowania projektu, wskaźniki awarii i pokrycie testami.
19Wdrażasz aplikację na platformę iOS o dużym natężeniu ruchu, która wymaga migracji bazy danych oraz zmiany w backendowym API (Application Programming Interface). W jaki sposób zaprojektujesz plan wdrożenia (rollout plan), aby zminimalizować wpływ na użytkowników?
Zaprojektowanie planu wdrożenia aplikacji na platformę iOS o dużym natężeniu ruchu, która wymaga migracji bazy danych i zmian w backendowym API, wymaga rozdzielenia wdrożenia kodu od aktywacji funkcji ze względu na ograniczenia klienta iOS (asynchroniczne aktualizacje użytkowników oraz brak możliwości wymuszenia wycofania wersji binarnej po stronie klienta).
1. Kompatybilność backendu: Zastosuj strategię rozszerzania i wycofywania (expand-and-contract). Wdróż API v2 obok API v1, aby zarówno starsze, jak i zaktualizowane wersje klienta działały jednocześnie bez wprowadzania niekompatybilnych zmian (breaking changes).
2. Odporna migracja bazy danych: Upewnij się, że lokalne migracje schematu (np. SQLite, Core Data, SwiftData) są idempotentne, nieniszczące i bezpiecznie obsługują przeskoki o wiele wersji (np. aktualizację z wersji N-3 do N) bez blokowania głównego wątku i bez wywoływania pętli awarii podczas uruchamiania aplikacji (launch crash loops).
3. Dynamiczne sterowanie dostępnością (dynamic gating): Wypuszczaj nowe funkcje klienta w trybie ukrytym (dark launch) za flagami funkcji sterowanymi z serwera (feature flags) lub zdalną konfiguracją (remote configuration). Pozostaw flagę funkcji wyłączoną podczas początkowej dystrybucji.
4. Stopniowe wdrażanie i monitorowanie: Dystrybuuj aplikację za pośrednictwem mechanizmu stopniowego udostępniania w App Store (App Store Phased Release – 7-dniowe wdrożenie etapowe). Nieprzerwanie monitoruj telemetrię, wskaźniki awarii (crash rates), metryki powodzenia migracji bazy danych oraz wskaźniki błędów API. W przypadku wystąpienia anomalii wstrzymaj stopniowe wdrażanie w App Store Connect i wyłącz flagi funkcji bez konieczności awaryjnego wycofywania binarnej wersji aplikacji.
Phase 1 (Backend Expand): Deploy API v2 supporting both new and legacy payloads.
Phase 2 (Client Distribution & Migration): Release client v2.0 via App Store Phased Release (1% -> 100%). DB migrates safely on first launch. Feature flag remains OFF.
Phase 3 (Validation & Feature Enablement): Monitor crash rates & migration success. Incrementally ramp Remote Config flag (10% -> 50% -> 100%).
Phase 4 (Backend Contract): Once legacy app adoption drops below deprecation threshold, sunset API v1.
20Projektujesz synchronizację offline-first dla aplikacji do notatek, w której edycje muszą ostatecznie zsynchronizować się między urządzeniami, jednak system iOS może odroczyć lub anulować pracę w tle. Jaką architekturę wykonywania zadań w tle wybierzesz?
Architektura synchronizacji offline-first traktuje lokalną bazę danych (np. SQLite, Core Data lub SwiftData) jako natychmiastowe pojedyncze źródło prawdy dla stanu interfejsu użytkownika, podczas gdy mutacje są dopisywane do trwałej kolejki wychodzącej (outbox queue) na dysku, dzięki czemu niewysłane edycje przetrwają zakończenie procesu. Aby zarządzać oportunistycznym i niedeterministycznym wykonywaniem zadań w tle w systemie iOS, praca w tle jest zorganizowana wielopoziomowo:
1. Opróżnianie kolejki wychodzącej przy przejściu w tło (Outbox Flush on Backgrounding): Inicjowane za pomocą `UIApplication.shared.beginBackgroundTask(expirationHandler:)` w celu dokończenia trwających lub szybkich oczekujących mutacji.
2. Zaplanowana okresowa synchronizacja: Rejestrowana w `BGTaskScheduler` przy użyciu `BGAppRefreshTask` do lekkiej synchronizacji metadanych/różnic (delta sync) oraz `BGProcessingTask` do bardziej wymagających operacji synchronizacji.
3. Synchronizacja dużych zasobów: Obsługiwana poza procesem za pośrednictwem konfiguracji `URLSessionConfiguration` w tle przy użyciu przesyłania/pobierania plików.
4. Synchronizacja wyzwalana z serwera: Oportunistyczne wybudzanie za pomocą cichych powiadomień push (`content-available: 1`).
Ponieważ zadania w tle mogą zostać przerwane lub przełożone w dowolnym momencie, operacje muszą wykorzystywać generowane przez klienta klucze idempotencji (np. identyfikatory UUID), co pozwala na bezpieczne ponawianie prób bez duplikowania danych. W przypadku wywołania `expirationHandler` danego zadania, trwająca praca musi zostać natychmiast anulowana, niezatwierdzone operacje oznaczone z powrotem jako oczekujące, a następnie należy wywołać `setTaskCompleted(success: false)`. Spójność ostateczna (eventual consistency) jest utrzymywana przy użyciu mechanizmów rozwiązywania konfliktów, takich jak CRDT (Conflict-free Replicated Data Types), wektory wersji (version vectors) lub strategia ostatniego zapisu (last-write-wins) ze znacznikami usunięcia (tombstones), co gwarantuje, że zdalne delty nie nadpiszą niezatwierdzonych lokalnych mutacji z kolejki wychodzącej.
import BackgroundTasks
import Foundation
final class NoteSyncCoordinator {
static let shared = NoteSyncCoordinator()
private let syncTaskID = "com.notesapp.sync.refresh"
func registerSyncTask() {
BGTaskScheduler.shared.register(forTaskWithIdentifier: syncTaskID, using: nil) { task in
guard let refreshTask = task as? BGAppRefreshTask else { return }
self.handleAppRefresh(task: refreshTask)
}
}
func scheduleNextSync() {
let request = BGAppRefreshTaskRequest(identifier: syncTaskID)
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
try? BGTaskScheduler.shared.submit(request)
}
private func handleAppRefresh(task: BGAppRefreshTask) {
scheduleNextSync()
let syncTask = Task {
do {
try await SyncEngine.shared.flushPersistentOutboxAndFetchDeltas()
task.setTaskCompleted(success: true)
} catch {
task.setTaskCompleted(success: false)
}
}
task.expirationHandler = {
syncTask.cancel()
}
}
}