20 častých otázek k pohovoru pro iOS. iOS vývoj je žádaný obor zaměřený na aplikace ve Swiftu, SwiftUI a UIKit a řeší lifecycle, konkurenci, síť, ukládání dat, výkon, testování a publikování v App Storu. Otázky pokrývají různé úrovně a odpovědi si můžeš nahlas procvičit v našem pohovorovém trenéru.
1Jaké odpovědnosti by měl mít UIViewController v typické architektuře iOS MVC (Model-View-Controller)?
V klasické architektuře iOS MVC (Model-View-Controller) působí UIViewController jako Controller (kontroler), který zprostředkovává komunikaci mezi komponentami View a daty Modelu. Mezi jeho hlavní odpovědnosti patří správa životního cyklu zobrazení (např. viewDidLoad, viewWillAppear), inicializace a aktualizace prvků uživatelského rozhraní (UI), zpracování přímých interakcí uživatele (např. klepnutí na tlačítka, delegáty a target-actions) a řízení přechodů či prezentace obrazovek. Odpovědnosti nesouvisející s UI – jako je přímá síťová komunikace, perzistence a náročná doménová logika – by měly být delegovány na dedikované servisní nebo modelové objekty, aby se zabránilo vzniku přetíženého kontroleru (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
}
}
2Jaká je role provisioning profilu (provisioning profile) při vydávání aplikace pro iOS?
Při vydání aplikace pro iOS slouží provisioning profil (provisioning profile) jako kryptograficky podepsaný balíček, který propojuje bezpečnostní požadavky společnosti Apple s binárním souborem aplikace a cílovými zařízeními. Jeho primární rolí je sdělit operačnímu systému iOS a službě App Store Connect, že aplikace je autorizována ke spuštění podle konkrétních distribučních pravidel.
Provisioning profil v sobě sdružuje několik klíčových prvků:
- App ID (Bundle Identifier),
- autorizovaný podpisový certifikát (nebo certifikáty) potvrzující, kdo aplikaci sestavil,
- oprávnění a schopnosti (entitlements a capabilities) udělené aplikaci (jako jsou Push Notifications, Sign in with Apple nebo iCloud),
- a u profilů mimo App Store (jako jsou Development nebo Ad-Hoc) také seznam povolených identifikátorů zařízení UDID (Unique Device Identifier).
U distribučních profilů pro App Store jsou jednotlivé identifikátory zařízení UDID vynechány, protože Apple umožňuje distribuci na jakékoli koncové zařízení prostřednictvím obchodu 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 znamená, když aplikace pro iOS běží na pozadí, a jaká základní omezení pro tuto činnost systém zavádí?
Běh na pozadí v systému iOS znamená provádění kódu ve chvíli, kdy aplikace není aktivní na obrazovce ani viditelná pro uživatele. Když uživatel aplikaci opustí, přejde z aktivního stavu v popředí do pozadí a krátce poté do pozastaveného stavu (suspended), kde je její vykonávání pozastaveno, ačkoliv její data zůstávají v paměti RAM. Aby iOS šetřil výdrž baterie, zachoval odezvu systému a chránil systémové prostředky, běh na pozadí přísně reguluje. Systém omezuje dobu, po kterou může aplikace po přechodu na pozadí běžet (obvykle jde o krátké časové okno přibližně 30 sekund, pokud se nepoužijí specifické režimy běhu na pozadí nebo plánované úlohy), přiškrcuje prioritu procesoru i sítě a při nedostatku paměti může pozastavenou aplikaci zcela ukončit.
4Co je to Publisher ve frameworku Combine a jak se liší od Subscriberu v typickém datovém toku v iOS?
V rámci frameworku Combine emituje `Publisher` proud hodnot v čase a může být ukončen událostí dokončení (buď úspěšným dokončením, nebo selháním s chybou). Definuje dva asociované typy: `Output` (typ dat, která produkuje) a `Failure` (typ chyby, kterou může vyvolat). `Subscriber` přijímá hodnoty a události životního cyklu od publisheru. V typickém datovém toku v iOS fungují publishery jako zdroj nebo producent asynchronních událostí (jako jsou síťové odpovědi, notifikace nebo uživatelské vstupy), zatímco subscribeři vystupují jako konzumenti, kteří reagují na vysílané hodnoty, zpracovávají chyby a reagují na dokončení (například aktualizací stavu UI nebo zápisem do databáze). Subscriber se připojí k publisheru, obdrží token odběru (subscription) a vyžádá si požadovaný počet prvků (demand).
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 v jazyce Swift znamená async/await a jak byste tuto syntaxi použili k volání asynchronního API (Application Programming Interface) z aplikace pro iOS?
`async/await` je vestavěná syntaxe v jazyce Swift pro psaní asynchronního kódu lineárním, čitelným a sekvenčním způsobem namísto zanořených uzávěr (closures) a obsluh dokončení (completion handlers). Označení funkce klíčovým slovem `async` sděluje překladači, že funkce může pozastavit své provádění při čekání na dokončení dlouhotrvající operace (jako jsou síťové požadavky nebo souborové I/O). Klíčové slovo `await` označuje bod pozastavení (suspension point): provádění aktuální funkce se pozastaví, což uvolní podkladové vlákno pro jinou práci, a pokračuje, jakmile je očekávaný výsledek nebo chyba k dispozici. Chcete-li zavolat asynchronní API v aplikaci pro iOS, zavoláte metodu označenou `async` s předřazeným klíčovým slovem `await` (případně `try await`, pokud funkce vyhazuje výjimku) z asynchronního kontextu, například z jiné funkce typu `async` nebo uvnitř bloku `Task`.
6Kdy byste v aplikaci pro iOS použili UserDefaults a jaké druhy dat by se tam ukládat neměly?
UserDefaults je určen pro ukládání malých, nenáročných uživatelských předvoleb, nastavení a příznaků napříč spuštěními aplikace (například předvolby motivu, úroveň hlasitosti nebo logický příznak `hasSeenOnboarding`). Podporuje typy property listu, jako jsou String, Int, Double, Bool, Date, Data, Array a Dictionary.
Neměli byste ukládat:
1. Citlivé informace (jako jsou uživatelská hesla, privátní klíče nebo autentizační tokeny), protože UserDefaults ukládá data nešifrovaná v prostém textovém souboru plist.
2. Velké datové sady, dokumenty nebo multimediální soubory (jako jsou obrázky, zvukové soubory nebo objemná data JSON), protože při přístupu se celý plist načítá do paměti, což vede k vysoké paměťové režii a zpomaluje spouštění aplikace.
// 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"
7Co je ARC (Automatic Reference Counting) ve Swiftu a jak spravuje životnost instancí tříd v aplikaci pro iOS?
ARC (Automatic Reference Counting) je mechanismus správy paměti v době překladu v jazyce Swift, který slouží ke sledování a správě životnosti instancí tříd (referenčních typů). Pokaždé, když je vytvořena nová instance třídy a přiřazena k silné referenci, ARC sleduje interní čítač referencí (refcount) této instance. Dokud existuje alespoň jedna silná reference na instanci, ARC udržuje instanci v paměti. Když jsou všechny silné reference odstraněny a čítač referencí klesne na nulu, ARC instanci okamžitě uvolní z paměti a těsně před dealokací automaticky zavolá metodu `deinit` dané instance.
Na rozdíl od běhového automatického čištění paměti (garbage collection) v prostředích jako Java nebo .NET neprovádí ARC periodické cykly procházení paměti; překladač Swiftu vkládá příslušná volání retain a release do binárního kódu přímo v době překladu.
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 v aplikaci pro iOS provést jednoduchý požadavek HTTP (Hypertext Transfer Protocol) GET pomocí URLSession?
Chcete-li v aplikaci pro iOS provést jednoduchý požadavek HTTP GET, vytvoříte objekt `URL` a předáte jej instanci `URLSession`, například `URLSession.shared`. Požadavek můžete provést pomocí obslužné funkce pro dokončení datové úlohy (completion handler: `URLSession.shared.dataTask(with: url) { data, response, error in ... }`) nebo pomocí moderního asynchronního zpracování ve Swiftu (`let (data, response) = try await URLSession.shared.data(from: url)`). Při použití tradičního přístupu s datovou úlohou (data task) musíte na úloze zavolat metodu `.resume()`, aby se spustila, ošetřit případnou transportní chybu, zkontrolovat stavový kód `HTTPURLResponse` a zajistit, aby veškeré aktualizace uživatelského rozhraní byly odeslány na hlavní vlákno (main thread).
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()
9Co je nástroj Instruments v prostředí Xcode a jak byste jej použili k vyšetření jednoduchého problému s výkonem v aplikaci pro iOS?
Instruments je nástroj společnosti Apple pro profilování a analýzu výkonu, který je součástí Xcode. Umožňuje vývojářům sledovat chování za běhu, využití procesoru (CPU), alokace paměti, úniky paměti (memory leaks) a výkon vykreslování uživatelského rozhraní (UI). Chcete-li prozkoumat jednoduchý problém s výkonem (například zasekávání uživatelského rozhraní nebo vysoké zatížení procesoru), spustíte Instruments z Xcode (přes Product -> Profile), zvolíte vhodnou šablonu (například Time Profiler pro kritická místa procesoru nebo Allocations/Leaks pro problémy s pamětí) a nahrajete záznam (trace) při reprodukci problematického uživatelského scénáře na zařízení. Po nahrání analyzujete strom volání (call tree) a nejnáročnější zásobníky volání (stack traces), abyste přesně určili metody způsobující zpoždění nebo nadměrnou spotřebu prostředků, což vám umožní změřit a lokalizovat úzké hrdlo před provedením změn v kódu.
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.
10Co je APNs (Apple Push Notification service) a jakou roli hraje při doručování push oznámení do aplikace pro iOS?
APNs (Apple Push Notification service) je cloudová zprostředkující služba společnosti Apple, která bezpečně směruje push oznámení z vašich backendových serverů (poskytovatelů / provider servers) do zařízení Apple. Protože mobilní zařízení nemohou udržovat nepřetržitá otevřená soketová spojení s backendem každé jednotlivé aplikace, aniž by docházelo k vybíjení baterie a nadměrné spotřebě šířky síťového pásma, Apple udržuje mezi každým zařízením a službou APNs jediné trvalé, nízkoenergetické spojení. Když se vaše aplikace zaregistruje k odběru vzdálených oznámení, APNs vygeneruje unikátní, netransparentní token zařízení (device token), který identifikuje danou konkrétní instalaci aplikace na daném konkrétním zařízení. Aplikace tento token odešle vašemu backendovému serveru poskytovatele. Když chce poskytovatel uživatele upozornit, sestaví datovou zátěž (payload) a odešle ji spolu s tokenem zařízení službě APNs přes protokol HTTP/2. APNs následně vyhledá aktivní spojení pro dané zařízení a doručí datovou zátěž push oznámení přímo do systému iOS, který zobrazí upozornění nebo probudí aplikaci podle nastavené konfigurace.
11Jak byste refaktorovali rozsáhlý UIViewController, který zajišťuje aktualizace UI (User Interface), validaci, síťovou komunikaci a navigaci?
Refaktoring přebujelého kontroleru (Massive View Controller – MVC) by měl probíhat inkrementálně, aby se snížilo riziko a jednotlivé odpovědnosti se rozdělily do vyhrazených vrstev:
1. Vyčlenění síťové vrstvy a dat: Přesuňte volání API (Application Programming Interface), ukládání dat a parsování JSON mimo view controller do specializovaných tříd typu Service nebo Repository s protokolovými abstrakcemi.
2. Vyčlenění prezentačního stavu a validace: Zaveďte ViewModel (nebo Presenter). Přesuňte validaci vstupů, formátování řetězců a správu stavu UI do ViewModelu, což umožní tuto logiku izolovaně testovat pomocí jednotkových testů.
3. Vyčlenění navigace: Nasaďte návrhový vzor Coordinator (případně Router / Flow Controller), abyste odstranili volání push/present a logiku navigačního toku z view controlleru.
4. Ponechání štíhlého View Controlleru: Ponechte v něm pouze rozvržení UI, konfiguraci podřízených pohledů (subviews), háčky životního cyklu a vazbu ovládacích prvků UI na ViewModel.
5. Inkrementální postup s testy: Během refaktoringu doplňujte jednotkové testy pro nově vyčleněné služby a ViewModely, aby bylo zaručeno zachování stávajícího chování.
// 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() }
}
12Jak byste řešili selhání nahrávání archivu v systému iOS způsobené chybami při podepisování kódů nebo provisioningem?
Řešení selhání nahrávání archivu v iOS způsobeného problémy s podepisováním nebo provisioningem zahrnuje kontrolu tří hlavních oblastí: distribučních certifikátů, provisioning profilů a oprávnění cíle (target entitlements).
1. Kontrola diagnostických protokolů: Prozkoumejte distribuční protokoly v Xcode Organizeru, podrobné chyby validace při nahrávání nebo konzolové výstupy CI (Continuous Integration; nástroje `altool`/`notarytool`) a identifikujte přesný důvod odmítnutí.
2. Ověření certifikátů a identit: Ověřte, že je v Keychain přítomen platný certifikát Apple Distribution spolu s odpovídajícím privátním klíčem a že nevypršel, nebyl odvolán a nechybí zprostředkující certifikát Apple WWDR (Worldwide Developer Relations).
3. Shoda provisioning profilu: Ujistěte se, že profil použitý pro export je distribuční profil pro App Store odpovídající přesnému identifikátoru balíčku (Bundle Identifier). Pokud se používají funkce jako Push Notifications nebo Associated Domains, ověřte, že je nakonfigurováno explicitní App ID namísto nekompatibilního zástupného znaku (wildcard).
4. Sladění oprávnění (entitlements): Zkontrolujte neshody mezi souborem `.entitlements` cíle a funkcemi povolenými pro dané App ID na portálu Apple Developer Portal. Pokud lokální entitlements vyžadují oprávnění, která nejsou na portálu v profilu zaregistrována, nahrávání selže.
5. Konfigurace podepisování: Pokud používáte automatické podepisování, ověřte vybraný tým a přihlašovací údaje k Apple ID v nastavení Xcode. Pokud používáte manuální podepisování (nebo exportní volby v CI), ujistěte se, že `ExportOptions.plist` mapuje každé bundle ID na správný distribuční certifikát a 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 byste zvolili mezi BGAppRefreshTask, BGProcessingTask a background URLSession pro produkční funkci synchronizace na pozadí?
Volba správného API pro práci na pozadí závisí na době trvání úlohy, požadavcích na napájení či síť a povaze přenášených dat:
1. **BGAppRefreshTask**: Nejvhodnější pro krátké a nenáročné aktualizace obsahu (jako je načtení novinek, feedů sociálních sítí nebo uživatelských přehledů), které trvají přibližně 15–30 sekund. Systém je plánuje na základě vzorců používání aplikace uživatelem, aby byl čerstvý obsah připraven těsně předtím, než uživatel aplikaci obvykle otevře.
2. **BGProcessingTask**: Navržen pro dlouhotrvající, nespěchající a výpočetně náročné operace, jako je indexace dat, údržba a migrace databáze, trénování modelů strojového učení nebo synchronizace velkých objemů dat. Může běžet několik minut a může vyžadovat připojení k nabíječce (`requiresExternalPower = true`) a připojení k síti Wi-Fi či internetu (`requiresNetworkConnectivity = true`), přičemž se typicky spouští přes noc.
3. **Background URLSession (`URLSessionConfiguration.background`)**: Správná volba při přenosu velkých souborů (nahrávání fotografií/videí nebo stahování velkých balíčků dat), které musí pokračovat mimo proces aplikace i v případě, že je aplikace pozastavena nebo ukončena operačním systémem. Přenáší síťový přenos na systémového démona `nsurlsessiond`, namísto udržování kódu aplikace aktivního v paměti.
14Jak byste v frameworku Combine sestavili pipeline pro vyhledávací textové pole, která omezuje frekvenci vstupů (debounce), ignoruje duplicity, provádí síťový požadavek a bezpečně aktualizuje UI (User Interface)?
Pro vytvoření bezpečné vyhledávací pipeline v Combine:
1. **Omezení frekvence a deduplikace (Debounce & Deduplicate):** Vezměte publisher vyhledávacího dotazu (např. `$queryText`), aplikujte `.debounce(for: .milliseconds(300), scheduler: RunLoop.main)` pro vyčkání na pauzu v psaní a `.removeDuplicates()` pro ignorování nezměněných vyhledávacích řetězců.
2. **Síťový požadavek a zrušení:** Namapujte každý dotaz na publisher síťového požadavku a sloučte pomocí `.switchToLatest()`. Tím se automaticky zruší probíhající požadavky při příchodu nového vyhledávacího dotazu, což zabraňuje souběhům (race conditions) a zastaralým výsledkům.
3. **Izolace chyb:** Zachyťte síťové chyby uvnitř vnitřního publisheru (např. `.catch { _ in Just([]) }`), aby selhání sítě neukončilo vnější proud vyhledávacího textu.
4. **Doručení na hlavní vlákno:** Před aktualizací stavu nebo přiřazením do prvků uživatelského rozhraní aplikujte `.receive(on: DispatchQueue.main)`.
15Jak byste pomocí mechanismů async/await a rušení úloh (Task cancellation) ve Swiftu implementovali obrazovku, která načítá data ze sítě a zabraňuje aktualizaci UI (User Interface) poté, co uživatel odejde jinam?
Načítání ze sítě bych implementoval jako asynchronní funkci `async` v servisní vrstvě nebo view modelu a spouštěl ji z úlohy `Task`, jejíž životnost je svázána s obrazovkou. V UIKit to obvykle znamená uložit vlastnost typu `var loadTask: Task<Void, Never>?` ve view controlleru nebo view modelu, spustit ji ve chvíli, kdy se má obrazovka načíst, a zrušit ji ve `viewWillDisappear`, `deinit` nebo před spuštěním novějšího požadavku v závislosti na požadované životnosti. V SwiftUI bych preferoval použití `.task` nebo `.task(id:)`, protože SwiftUI tuto úlohu automaticky zruší, jakmile view zmizí nebo se změní daný identifikátor. Uvnitř úlohy bych volal asynchronní síťové API (Application Programming Interface) pomocí `try await`, obsloužil zrušení odděleně od skutečných chyb a před aplikováním výsledků ověřil stav zrušení, pokud probíhá více volání `await` nebo kroků zpracování. Změny stavu UI musí probíhat na hlavním aktérovi (main actor), a to buď označením celého view modelu atributem `@MainActor`, nebo použitím `await MainActor.run`. Aby se předešlo zobrazení neaktuálního stavu UI, rušil bych předchozí úlohy, vyhnul se odděleným či globálním úlohám (detached/global tasks) pro práci vázanou na obrazovku a případně před aplikováním výsledku porovnal ID (Identifier) požadavku nebo ID aktuální položky.
@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)
}
}
}
}
16Jak byste volili mezi UserDefaults, Keychain, soubory, SQLite a Core Data pro různé typy aplikačních dat?
Volba vhodného mechanismu úložiště v iOS závisí na citlivosti dat, velikosti, relační složitosti, požadavcích na dotazování a životním cyklu zálohování: 1. **Keychain**: Pro citlivá data (autentizační tokeny, hesla, šifrovací klíče, biometrická tajemství). Poskytuje hardwarově jištěné šifrování, přetrvává i po přeinstalování aplikace a zůstává zabezpečená i v narušeném prostředí sandboxu. 2. **UserDefaults**: Pro jednoduchá, necitlivá uživatelská nastavení a příznaky (např. `hasCompletedOnboarding`, preference motivu). Protože se načítá celá do paměti jako property list, neměla by se používat pro velké datasety, média ani citlivé tokeny. 3. **Souborový systém (Documents / Caches / Application Support)**: Pro samostatné soubory a velké binární objekty (stažené obrázky, zvukové soubory, PDF). Složka `Documents` je přístupná uživateli a zálohuje se na iCloud; `Caches` slouží pro smazatelný, znovu stažitelný obsah; `Application Support` je určena pro trvalé soubory skryté před uživatelem. 4. **SQLite**: Pro strukturovaná tabulková data vyžadující rychlé indexované dotazy, hromadné operace nebo sdílení databáze napříč platformami bez režie objektového grafu. 5. **Core Data / SwiftData**: Pro složité objektové grafy s relacemi, sledováním změn, líným načítáním (faulting), správou operací zpět (undo) a přímou integrací s UIKitem (`NSFetchedResultsController`) nebo SwiftUI (`@Query`).
17Jak byste se na produkční obrazovce v UIKit se zpětnými voláními (callbacks) z view modelu rozhodovali, zda v obslužných uzávěrách zachytit self jako weak, unowned, nebo strong?
V architektuře UIKit, kde view controller komunikuje s view modelem pomocí zpětných volání, je strategie zachytávání určena vlastnictvím a životním cyklem:
1. `[weak self]`: Standardní a nejbezpečnější přístup pro ukládané nebo unikající uzávěry (escaping closures), jako jsou zpětná volání událostí z view modelu nebo asynchronní obsluhy dokončení síťových a datových operací. Protože view controller vlastní view model, silné zachycení `self` v uzávěře uložené ve view modelu vytvoří cyklus silných referencí (`VC -> VM -> closure -> VC`). `[weak self]` převede `self` na volitelný typ (`UIViewController?`), což umožní korektní dealokaci `self` a zabrání únikům paměti (memory leaks).
2. `[unowned self]`: Předpokládá, že `self` nebude v okamžiku vykonání uzávěry nikdy `nil`. Na obrazovkách UI by se mělo používat s extrémní opatrností. Pokud se asynchronní úloha nebo zpětné volání dokončí poté, co byl view controller zavřen a dealokován, přístup k `unowned self` způsobí pád aplikace za běhu (fatal runtime crash). Z tohoto důvodu je v produkčních UI zpětných voláních v UIKit obecně preferován `[weak self]` před `unowned`.
3. **Silné zachycení (výchozí stav, bez seznamu zachycení):** Vhodné v případech, kdy je uzávěra neunikající (non-escaping, např. standardní operace nad kolekcemi jako `map`/`filter`) nebo když má uzávěra krátkou životnost a nevytváří vlastnický cyklus zpět k `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 */ }
}
18Připojujete se k vyzrálé aplikaci pro iOS se stovkami obrazovek, mnoha masivními view controllery a pomalými cykly vydávání. Jak byste naplánovali strategii inkrementálního vylepšování architektury, aniž byste zastavili dodávání nových funkcí?
Efektivní strategie inkrementální migrace se vyhýbá kompletním přepisům od nuly a spojuje refaktorování přímo s probíhajícím dodáváním funkcí (podle návrhového vzoru Strangler Fig). Začíná se definováním odsouhlaseného plánu cílové architektury (např. MVVM nebo VIPER se vkládáním závislostí – dependency injection a modulárními koordinátory) a vytvořením základní sady testů (charakterizační, snapshotové a jednotkové testy) kolem starého kódu dříve, než se do něj zasáhne.
Stanovení priorit by se mělo řídit frekvencí změn a rizikovostí: refaktorujte obrazovky, které týmy aktivně upravují, nebo oblasti s vysokou chybovostí, nikoli stabilní starší kód. Při refaktorování masivních view controllerů oddělte byznysovou a prezentační logiku do dedikovaných ViewModelů/Presenterů a zaveďte adaptéry nebo koordinátory založené na protokolech, aby se staré UI izolovalo od nového kódu.
Rychlost vývoje a kvalitu udržujte pomocí architektonického dohledu (governance): připravte referenční ukázkové implementace (golden sample), vynucujte hranice pomocí CI linterů nebo pokynů pro PR (Pull Request) a vyhraďte trvalou kapacitu na řešení technického dluhu (např. 15–20 % kapacity nebo spojení refaktorování se související prací na funkcích) a současně sledujte metriky jako doba sestavení, míra pádů a pokrytí testy.
19Vydáváte vysoce zatíženou aplikaci pro iOS s migrací databáze a změnou backendového API (Application Programming Interface); jak byste navrhli plán postupného nasazení, abyste minimalizovali dopad na uživatele?
Návrh plánu nasazení pro vysoce zatíženou aplikaci pro iOS zahrnující migrace databáze a změny backendového API vyžaduje oddělení samotného nasazení kódu od aktivace funkcí, a to kvůli omezením iOS klientů (asynchronní aktualizace ze strany uživatelů a nemožnost vynutit vrácení klientské binárky na předchozí verzi – rollback).
1. Zpětná kompatibilita backendu: Implementujte strategii „expand and contract“ (rozšířit a následně zúžit). Nasaďte API v2 souběžně s API v1, aby starší i aktualizované verze klientů fungovaly současně bez nekompatibilních změn (breaking changes).
2. Odolná migrace databáze: Zajistěte, aby lokální migrace schématu (např. SQLite, Core Data, SwiftData) byly idempotentní, nedestruktivní a bezpečně zvládaly přechody přes více verzí (např. upgrade z verze N-3 na N), aniž by blokovaly hlavní vlákno nebo způsobovaly pády aplikace při spuštění ve smyčce (crash loops).
3. Dynamické řízení dostupnosti (Dynamic Gating): Vydávejte nové klientské funkce skryté za serverem řízenými přepínači funkcí (feature flags) či vzdálenou konfigurací. Během počáteční distribuce nechte přepínač vypnutý.
4. Fázované nasazení a monitoring: Distribuujte aplikaci prostřednictvím funkce App Store Phased Release (sedmidenní postupné uvolňování). Průběžně monitorujte telemetrii, míru pádů, metriky úspěšnosti migrace databáze a chybovost API. Pokud se objeví anomálie, pozastavte fázované vydávání v App Store Connect a vypněte přepínače funkcí, aniž by bylo nutné provádět nouzové vydání opravné binárky.
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.
20Navrhujete offline-first synchronizaci pro aplikaci na poznámky, kde se úpravy musí nakonec synchronizovat napříč zařízeními, ale systém iOS může práci na pozadí odložit nebo zrušit. Jakou architekturu provádění úloh na pozadí byste zvolili?
Architektura offline-first synchronizace přistupuje k lokální databázi (např. SQLite, Core Data nebo SwiftData) jako k okamžitému jedinému zdroji pravdy pro stav UI, zatímco mutace jsou zapisovány do perzistentní fronty k odeslání (outbox queue) na disku, takže neodeslané úpravy přežijí ukončení procesu. Aby bylo možné zvládnout oportunistické a nedeterministické provádění úloh na pozadí v systému iOS, je práce na pozadí strukturována do několika úrovní:
1. Odeslání fronty při přechodu na pozadí (Outbox Flush): Spouští se přes `UIApplication.shared.beginBackgroundTask(expirationHandler:)` pro dokončení rozpracovaných nebo rychlých čekajících mutací.
2. Plánovaná periodická synchronizace: Zaregistrovaná v `BGTaskScheduler` pomocí `BGAppRefreshTask` pro odlehčenou synchronizaci metadat a rozdílů (deltas) a `BGProcessingTask` pro náročnější synchronizační operace.
3. Synchronizace velkých datových souborů: Řešena mimo hlavní proces pomocí `URLSessionConfiguration` na pozadí s využitím nahrávání/stahování souborů.
4. Serverem iniciovaná synchronizace: Oportunistické probuzení pomocí tichých push notifikací (`content-available: 1`).
Protože úlohy na pozadí mohou být kdykoli přerušeny nebo přeplánovány, operace musí používat klientem generované klíče idempotence (např. UUID), které umožňují bezpečné opakování požadavků bez duplikace dat. Pokud je vyvolán `expirationHandler` úlohy, rozpracovaná práce musí být okamžitě zrušena, nepotvrzené operace označeny zpět jako čekající a musí být zavoláno `setTaskCompleted(success: false)`. Výsledná konzistence (eventual consistency) je udržována pomocí mechanismů řešení konfliktů, jako jsou CRDT, vektorové hodiny (version vectors) nebo strategie last-write-wins s mazacími značkami (tombstones), což zajišťuje, že vzdálené rozdíly nepřepíší nepotvrzené lokální mutace ve frontě.
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()
}
}
}