20 često postavljanih pitanja za iOS Developer intervju. Pitanja pokrivaju različite nivoe, a odgovore možete vežbati naglas u našem treneru za intervjue.
1Koje odgovornosti treba da ima UIViewController u tipičnoj iOS MVC (Model-View-Controller) arhitekturi?
U klasičnoj iOS MVC (Model-View-Controller) arhitekturi, UIViewController deluje kao Controller (kontroler) koji posreduje između View komponenti i podataka iz Model sloja. Njegove primarne odgovornosti obuhvataju upravljanje životnim vekom prikaza (npr. viewDidLoad, viewWillAppear), podešavanje i ažuriranje UI elemenata, obradu direktnih korisničkih interakcija (kao što su dodiri dugmadi, delegati i target-action mehanizmi), kao i koordinaciju prelaza ili prikaza ekrana. Odgovornosti koje nisu vezane za korisnički interfejs — kao što su direktna mrežna komunikacija, perzistencija i složena poslovna logika — treba delegirati namenskim servisima ili model objektima kako bi se sprečilo da kontroler prikaza postane 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
}
}
2Koja je uloga profila za konfigurisanje uređaja (provisioning profile) prilikom objavljivanja iOS aplikacije?
Prilikom objavljivanja iOS aplikacije, provisioning profile funkcioniše kao kriptografski potpisan paket koji povezuje Apple-ove bezbednosne zahteve sa binarnim fajlom aplikacije i ciljnim uređajima. Njegova primarna uloga jeste da obavesti iOS operativni sistem i App Store Connect da je aplikacija ovlašćena za pokretanje u okviru specifičnih pravila distribucije.
Provisioning profile objedinjuje nekoliko kritičnih elemenata: identifikator aplikacije (App ID / Bundle Identifier), ovlašćeni sertifikat za potpisivanje koji potvrđuje ko je izgradio aplikaciju, dozvole i mogućnosti dodeljene aplikaciji (entitlements i capabilities, kao što su Push Notifications, Sign in with Apple ili iCloud), i — u profilima koji nisu namenjeni za App Store (kao što su Development ili Ad-Hoc) — listu dozvoljenih UDID identifikatora uređaja. Kod profila za objavljivanje na App Store-u, pojedinačni UDID-jevi uređaja se izostavljaju jer Apple dozvoljava distribuciju na bilo koji uređaj korisnika putem App Store platforme.
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)
3Šta znači kada iOS aplikacija izvršava rad u pozadini i koja osnovna ograničenja sistem postavlja na taj rad?
Izvršavanje rada u pozadini na iOS sistemu podrazumeva pokretanje koda u trenucima kada aplikacija nije aktivna na ekranu niti vidljiva korisniku. Kada korisnik napusti aplikaciju, ona prelazi iz aktivnog stanja u prvom planu u pozadinu, a ubrzo nakon toga u suspendovano stanje u kome je njeno izvršavanje pauzirano, iako njeni podaci ostaju u RAM memoriji.
Radi očuvanja baterije, odzivnosti sistema i resursa uređaja, iOS strogo kontroliše izvršavanje u pozadini. Sistem ograničava vreme tokom kog aplikacija može da izvršava kod nakon prelaska u pozadinu (obično na kratak vremenski prozor od približno 30 sekundi, osim ako se ne koriste specifični režimi rada u pozadini ili zakazani zadaci), smanjuje prioritet procesora (CPU) i mreže, i može ugasiti suspendovane aplikacije ukoliko dođe do nedostatka memorije na uređaju.
4Šta je Combine izdavač (publisher) i kako se razlikuje od pretplatnika (subscriber) u tipičnom toku podataka na iOS platformi?
U radnom okviru Combine, `Publisher` emituje tok vrednosti tokom vremena i može se završiti događajem dovršetka (bilo uspešnim završetkom ili neuspehom sa greškom). On deklariše dva pridružena tipa: `Output` (tip podataka koje proizvodi) i `Failure` (tip greške koju može emitovati). `Subscriber` prima vrednosti i događaje životnog veka od izdavača. U tipičnom toku podataka na iOS-u, izdavači deluju kao izvor ili proizvođač asinhronih događaja (kao što su mrežni odgovori, obaveštenja ili korisnički unosi), dok pretplatnici deluju kao potrošači koji reaguju na emitovane vrednosti, obrađuju greške i reaguju na završetak (kao što je ažuriranje stanja korisničkog interfejsa ili upisivanje u bazu podataka). Pretplatnik se povezuje sa izdavačem, dobija token pretplate i šalje zahteve za elementima.
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)")
}
)
5Šta označava async/await u programskom jeziku Swift i kako biste ga iskoristili za pozivanje asinhronog API-ja (Application Programming Interface) iz iOS aplikacije?
`async/await` je ugrađena sintaksa jezika Swift za pisanje asinhronog koda na linearan, čitljiv i sekvencijalan način, umesto korišćenja ugnježdenih zatvaranja (closures) i funkcija za obradu završetka (completion handlers).
Označavanje funkcije ključnom rečju `async` saopštava prevodiocu (kompajleru) da funkcija može pauzirati svoje izvršavanje dok čeka završetak dugotrajnog posla (kao što su mrežni zahtevi ili ulazno-izlazne operacije nad datotekama). Ključna reč `await` označava tačku suspenzije: izvršavanje trenutne funkcije se pauzira, čime se oslobađa odgovarajuća nit za obavljanje drugog posla, i nastavlja se kada očekivani rezultat ili greška postanu spremni.
Da biste pozvali asinhroni API u iOS aplikaciji, pozivate `async` metodu ispred koje stoji `await` (ili `try await` ako funkcija izbacuje greške) unutar asinhronog konteksta, kao što je druga `async` funkcija ili `Task` blok.
6Kada biste koristili UserDefaults u iOS aplikaciji i koje vrste podataka ne bi trebalo tu čuvati?
UserDefaults je namenjen za čuvanje malih, jednostavnih korisničkih podešavanja, postavki i zastavica između pokretanja aplikacije (na primer, izbor teme, nivo jačine zvuka ili logička zastavica `hasSeenOnboarding`). Podržava tipove sa liste svojstava kao što su String, Int, Double, Bool, Date, Data, Array i Dictionary. NE bi trebalo da čuvate: 1. Osetljive informacije (kao što su korisničke lozinke, privatni ključevi ili tokeni za autentifikaciju) zato što UserDefaults čuva podatke nešifrovano u običnom tekstualnom plist fajlu. 2. Velike skupove podataka, dokumente ili medijske fajlove (poput slika, audio-zapisa ili velikih JSON paketa) zato što se čitav plist učitava u memoriju prilikom pristupa, što dovodi do velikog memorijskog zauzeća i usporava pokretanje aplikacije.
// 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"
7Šta je ARC (Automatic Reference Counting) u jeziku Swift i kako on upravlja životnim vekom instanci klasa u iOS aplikaciji?
ARC (Automatic Reference Counting) je Swift mehanizam za upravljanje memorijom tokom faze prevođenja (compile-time), koji prati i upravlja životnim vekom instanci klasa (referentnih tipova). Svaki put kada se kreira nova instanca klase i dodeli jakoj referenci (strong reference), ARC prati interni brojač referenci za tu instancu. Sve dok postoji bar jedna jaka referenca na instancu, ARC je zadržava u memoriji. Kada se uklone sve jake reference i brojač referenci padne na nulu, ARC odmah oslobađa instancu kako bi oslobodio memoriju, automatski pozivajući metodu `deinit` neposredno pre dealokacije. Za razliku od sakupljača smeća (Garbage Collector) u izvršnim okruženjima kao što su Java ili .NET, ARC ne izvršava periodične cikluse čišćenja; Swift kompilator umeće odgovarajuće retain i release pozive u binarni kod tokom kompilacije.
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
8Kako se izvršava jednostavan HTTP (Hypertext Transfer Protocol) GET zahtev pomoću URLSession u iOS aplikaciji?
Da biste izvršili jednostavan HTTP GET zahtev u iOS aplikaciji, kreirate `URL` objekat i prosleđujete ga instanci `URLSession`, kao što je `URLSession.shared`. Zahtev možete izvršiti pomoću funkcije za obradu završetka zadatka za preuzimanje podataka (`URLSession.shared.dataTask(with: url) { data, response, error in ... }`) ili korišćenjem modernog Swift asinhronog modela (`let (data, response) = try await URLSession.shared.data(from: url)`). Kada koristite tradicionalni pristup sa zadatkom za podatke, morate pozvati `.resume()` na zadatku da biste ga pokrenuli, obraditi greške u prenosu, proveriti statusni kod `HTTPURLResponse` i osigurati da se sva ažuriranja korisničkog interfejsa šalju na glavnu nit.
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()
9Šta je Instruments u Xcode-u i kako biste ga iskoristili za ispitivanje jednostavnog problema sa performansama u iOS aplikaciji?
Instruments je Apple-ov alat za profilisanje i analizu performansi koji dolazi uz Xcode. Omogućava programerima da prate ponašanje tokom izvršavanja, zauzeće procesora, alokacije memorije, curenje memorije i performanse iscrtavanja korisničkog interfejsa. Da biste ispitali jednostavan problem sa performansama (kao što je seckanje korisničkog interfejsa ili visoko zauzeće procesora), pokrećete Instruments iz Xcode-a (putem Product -> Profile), birate odgovarajući šablon (kao što je Time Profiler za uska grla procesora ili Allocations/Leaks za probleme sa memorijom) i snimate trag dok reprodukujete problematičan korisnički tok na uređaju. Nakon snimanja, analizirate stablo poziva i najopterećenije stekove poziva da biste precizno utvrdili metode koje uzrokuju kašnjenja ili prekomernu potrošnju resursa, što vam omogućava da izmerite i locirate usko grlo pre unošenja izmena u kôd.
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.
10Šta je APNs (Apple Push Notification service) i koju ulogu ima u isporuci push obaveštenja iOS aplikaciji?
APNs (Apple Push Notification service) je Apple-ova posrednička usluga u oblaku koja bezbedno usmerava push obaveštenja sa vaših backend servera (serverskih provajdera) ka Apple uređajima. Pošto mobilni uređaji ne mogu održavati stalne, otvorene socket veze ka backendu svake pojedinačne aplikacije bez pražnjenja baterije i prekomerne potrošnje mrežnog protoka, Apple održava jednu trajnu vezu niske potrošnje energije između svakog uređaja i APNs-a. Kada se vaša aplikacija registruje za udaljena obaveštenja, APNs generiše jedinstveni, neprozirni token uređaja koji identifikuje tu specifičnu instalaciju aplikacije na tom konkretnom uređaju. Aplikacija šalje ovaj token vašem backend serveru. Kada provajder želi da obavesti korisnika, on kreira sadržaj obaveštenja (payload) i šalje ga zajedno sa tokenom uređaja ka APNs-u putem HTTP/2 protokola. APNs zatim pronalazi aktivnu vezu za taj uređaj i isporučuje payload push obaveštenja direktno operativnom sistemu iOS, koji prikazuje obaveštenje ili pokreće aplikaciju u skladu sa konfiguracijom.
11Kako biste refaktorisali obiman UIViewController koji upravlja ažuriranjem korisničkog interfejsa (UI - User Interface), validacijom, mrežnom komunikacijom i navigacijom?
Refaktorisanje preobimnog kontrolera prikaza (Massive View Controller – MVC) treba raditi inkrementalno kako bi se smanjio rizik, uz razdvajanje zasebnih odgovornosti u namenske slojeve:
1. Izdvajanje mrežnog sloja i podataka: Izmestite API pozive, trajno skladištenje podataka i parsiranje JSON formata iz kontrolera prikaza u namenske servisne ili repozitorijumske klase (Service / Repository) sa apstrakcijama u vidu protokola.
2. Izdvajanje stanja prezentacije i validacije: Uvedite ViewModel (ili Presenter). Premestite validaciju unosa, formatiranje stringova i upravljanje stanjem korisničkog interfejsa u ViewModel, čineći ovu logiku pogodnom za izolovano jedinično testiranje.
3. Izdvajanje navigacije: Primenite Coordinator obrazac (ili Router / Flow Controller) kako biste uklonili pozive za push/present i logiku navigacionog toka iz kontrolera prikaza.
4. Održavanje kontrolera prikaza sažetim: Zadržite samo raspored elemenata korisničkog interfejsa, konfiguraciju podređenih prikaza (subviews), metode životnog veka i povezivanje UI kontrola sa ViewModel-om.
5. Inkrementalno izvođenje uz testove: Dodajte jedinične testove za novoizdvojene servise i ViewModel-ove tokom refaktorisanja kako biste garantovali da postojeće ponašanje ostaje nepromenjeno.
// 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() }
}
12Kako biste dijagnostikovali i otklonili neuspešno otpremanje iOS arhive uzrokovano greškama pri potpisivanju koda ili profilima za obezbeđivanje (provisioning)?
Dijagnostikovanje neuspešnog otpremanja iOS arhive uzrokovanog problemima sa potpisivanjem ili profilima obuhvata proveru tri glavne oblasti: distributivne sertifikate, profile za obezbeđivanje (provisioning profiles) i prava pristupa cilja (target entitlements).
1. Pregled dijagnostičkih dnevnika: Proverite Xcode Organizer dnevnike distribucije, detaljne greške pri validaciji otpremanja ili dnevnike CI (Continuous Integration) konzole (`altool`/`notarytool`) kako biste utvrdili tačan razlog odbijanja.
2. Verifikacija sertifikata i identiteta: Proverite da li se u Keychain-u nalazi važeći Apple Distribution sertifikat sa odgovarajućim privatnim ključem i da nije istekao, opozvan ili da mu ne nedostaje Apple WWDR posrednički sertifikat.
3. Usklađenost profila za obezbeđivanje: Uverite se da je profil korišćen za izvoz App Store Distribution profil koji tačno odgovara identifikatoru paketa (Bundle Identifier). Ako se koriste mogućnosti poput Push Notifications ili Associated Domains, proverite da li je konfigurisan eksplicitni App ID umesto nekompatibilnog zamenskog znaka (wildcard).
4. Usklađenost prava pristupa (Entitlements): Proverite neslaganja između `.entitlements` datoteke cilja i mogućnosti omogućenih za App ID na Apple Developer portalu. Ako lokalna prava zahtevaju dozvole koje nisu registrovane na profilu na portalu, otpremanje neće uspeti.
5. Konfiguracija potpisivanja: Ako koristite automatsko potpisivanje, proverite izabrani tim i Apple ID akreditive u Xcode podešavanjima. Ako koristite ručno potpisivanje (ili CI opcije za izvoz), osigurajte da `ExportOptions.plist` mapira svaki ID paketa na ispravan distributivni sertifikat 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
13Kako biste izabrali između BGAppRefreshTask, BGProcessingTask i pozadinskog URLSession za funkcionalnost pozadinske sinhronizacije u produkciji?
Izbor pravog pozadinskog API-ja zavisi od trajanja zadatka, zahteva u pogledu napajanja/mrežnih uslova i prirode mrežnog prenosa podataka: 1. **BGAppRefreshTask**: Najbolji za kratka, lagana ažuriranja sadržaja (poput osvežavanja vesti, fidova na društvenim mrežama ili kontrolnih tabli korisnika) koja traju otprilike 15–30 sekundi. Sistem ih raspoređuje na osnovu obrazaca korišćenja korisnika, tako da svež sadržaj bude spreman neposredno pre nego što korisnik uobičajeno otvori aplikaciju. 2. **BGProcessingTask**: Namenjen za dugotrajne, nehitne, zahtevne operacije kao što su indeksiranje podataka, čišćenje/migracije baze podataka, treniranje ML modela ili sinhronizacija velikih količina podataka. Može trajati nekoliko minuta i može zahtevati da se uređaj puni (`requiresExternalPower = true`) i da je povezan na Wi-Fi/mrežu (`requiresNetworkConnectivity = true`), pri čemu se obično izvršava tokom noći. 3. **Pozadinski URLSession (`URLSessionConfiguration.background`)**: Pravi izbor pri prenosu velikih datoteka (otpremanje fotografija/video-zapisa ili preuzimanje velikih paketa resursa) koji se mora nastaviti van procesa čak i ako operativni sistem suspenduje ili ugasi aplikaciju. On prepušta mrežni prenos pozadinskom servisu `nsurlsessiond` umesto da drži kôd aplikacije aktivnim u memoriji.
14Kako biste izgradili Combine pipeline za tekstualno polje za pretragu koji vrši debounce unosa, ignoriše duplikate, izvršava mrežni zahtev i bezbedno ažurira UI (User Interface)?
Da biste izgradili bezbedan pipeline za pretragu u radnom okviru Combine:
1. **Debounce i uklanjanje duplikata:** Uzmite izdavača (publisher) upita za pretragu (npr. `$queryText`), primenite `.debounce(for: .milliseconds(300), scheduler: RunLoop.main)` kako biste sačekali pauzu u kucanju, kao i `.removeDuplicates()` radi ignorisanja nepromenjenih tekstualnih upita.
2. **Mrežni zahtev i otkazivanje:** Preslikajte svaki upit u izdavača mrežnog zahteva i poravnajte tok pomoću `.switchToLatest()`. Ovo automatski otkazuje aktivne zahteve koji su u toku kada stigne novi upit za pretragu, čime se izbegavaju uslovi trke (race conditions) i zastareli rezultati.
3. **Izolacija grešaka:** Uhvatite mrežne greške unutar unutrašnjeg izdavača (npr. `.catch { _ in Just([]) }`) kako neuspeh na mreži ne bi prekinuo spoljašnji tok teksta za pretragu.
4. **Isporuka na glavnoj niti:** Primenite `.receive(on: DispatchQueue.main)` pre ažuriranja stanja ili dodele vrednosti elementima korisničkog interfejsa.
15Kako biste iskoristili Swift async/await i otkazivanje zadataka (Task cancellation) da implementirate ekran koji učitava podatke sa mreže i izbegava ažuriranje korisničkog interfejsa (UI - User Interface) nakon što korisnik napusti ekran?
Definisao bih mrežno učitavanje kao `async` funkciju na servisu ili view model-u i pokrenuo je iz `Task`-a čiji je životni vek vezan za ekran. U UIKit-u to obično podrazumeva čuvanje nečega poput `var loadTask: Task<Void, Never>?` na view controller-u ili view model-u, pokretanje kada ekran treba da se učita i otkazivanje u `viewWillDisappear`, `deinit` ili pre pokretanja novijeg zahteva, u zavisnosti od željenog životnog veka. U SwiftUI-ju bih dao prednost `.task` ili `.task(id:)` modifikatorima kada je to moguće, jer ih SwiftUI automatski otkazuje kada pogled nestane ili se promeni id. Unutar zadatka pozvao bih asinhroni mrežni API pomoću `try await`, obradio otkazivanje odvojeno od stvarnih grešaka i proverio status otkazivanja pre primene rezultata ukoliko postoji više `await` poziva ili koraka obrade. Promene stanja UI-ja moraju se izvršavati na glavnom akteru (`main actor`), bilo postavljanjem `@MainActor` na view model ili korišćenjem `await MainActor.run`. Da bih izbegao zastareo UI, otkazivao bih prethodne zadatke, izbegavao nepovezane/globalne zadatke (detached tasks) za posao vezan za ekran i opciono upoređivao id zahteva ili trenutni id stavke pre primene rezultata.
@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)
}
}
}
}
16Kako biste birali između rešenja UserDefaults, Keychain, datoteka, SQLite i Core Data za različite tipove podataka u aplikaciji?
Izbor odgovarajućeg mehanizma za skladištenje na iOS platformi zavisi od osetljivosti podataka, njihove veličine, relacionih složenosti, zahteva za pretragom i životnog ciklusa pravljenja rezervnih kopija: 1. **Keychain**: Za osetljive podatke (tokeni za autentifikaciju, lozinke, ključevi za enkripciju, biometrijske tajne). Pruža hardverski podržanu enkripciju, opstaje nakon ponovne instalacije aplikacije i ostaje bezbedan čak i u kompromitovanim sandbox okruženjima. 2. **UserDefaults**: Za lagana, neosetljiva podešavanja i zastavice stanja (npr. `hasCompletedOnboarding`, izbor teme). Pošto se učitava u celosti u memoriju kao lista svojstava (property list), ne treba ga koristiti za velike skupove podataka, medijske datoteke ili osetljive tokene. 3. **Fajl sistem (Documents / Caches / Application Support)**: Za zasebne datoteke i velike binarne objekte (preuzete slike, audio zapisi, PDF dokumenti). `Documents` je dostupan korisniku i pravi se rezervna kopija na iCloud; `Caches` služi za sadržaj koji se može obrisati i ponovo preuzeti; `Application Support` je za perzistentne datoteke koje nisu direktno izložene korisniku. 4. **SQLite**: Za strukturirane, tabelarne podatke koji zahtevaju brze indeksirane upite, masovne operacije ili deljenje baze podataka na više platformi bez opterećenja grafa objekata. 5. **Core Data / SwiftData**: Za složene grafove objekata sa relacijama, praćenjem promena, lenjim učitavanjem (faulting), upravljanjem opozivom akcija (undo) i direktnom integracijom sa UIKit (`NSFetchedResultsController`) ili SwiftUI (`@Query`).
17Na produkcionom UIKit ekranu sa povratnim pozivima iz view model-a, kako biste odlučili da li da uhvatite self kao weak, unowned ili strong u closure rukovaocima?
U UIKit arhitekturi gde view controller komunicira sa view model-om putem povratnih poziva, strategija hvatanja je određena vlasništvom i životnim vekom: 1. `[weak self]`: Standardni i najbezbedniji pristup za sačuvana ili 'escaping' zatvorenja (kao što su povratni pozivi za događaje view model-a ili asinhroni završni rukovaoci za mrežu i podatke). Pošto view controller poseduje view model, snažno hvatanje `self` u povratnom pozivu koji čuva view model stvara ciklus snažnih referenci (`VC -> VM -> closure -> VC`). `[weak self]` pretvara `self` u opcioni tip (`UIViewController?`), omogućavajući da se `self` uredno dealocira i sprečava curenje memorije. 2. `[unowned self]`: Pretpostavlja da `self` nikada neće biti nil kada se zatvorenje izvršava. Treba ga koristiti sa izuzetnim oprezom na UI ekranima. Ako se asinhroni zadatak ili povratni poziv završi nakon što je view controller zatvoren i dealociran, pristupanje `unowned self` izaziva fatalno rušenje programa tokom izvršavanja. Iz tog razloga, `[weak self]` se u produkcionim UIKit UI povratnim pozivima uopšteno preferira u odnosu na `unowned`. 3. Snažno hvatanje (podrazumevano, bez liste hvatanja): Prikladno je kada zatvorenje nije 'escaping' (npr. standardne operacije nad kolekcijama kao što su `map`/`filter`) ili kada je zatvorenje kratkog veka i ne stvara ciklus vlasništva ka `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 */ }
}
18Pridružujete se zreloj iOS aplikaciji sa stotinama ekrana, mnogo preopterećenih kontrolera prikaza (Massive View Controllers) i sporim ciklusima objavljivanja. Kako biste isplanirali strategiju inkrementalnog unapređenja arhitekture bez zaustavljanja isporuke novih funkcionalnosti?
Efikasna inkrementalna strategija migracije izbegava potpuno ponovno pisanje koda i direktno povezuje refaktorisanje sa tekućom isporukom funkcionalnosti (prateći Strangler Fig obrazac). Započinjete definisanjem dogovorenog plana ciljne arhitekture (kao što je MVVM ili VIPER sa ubacivanjem zavisnosti i modularnim koordinatorima) i uspostavljanjem početne osnove za testiranje (karakterizacioni testovi, testovi snimaka ekrana i jedinični testovi) oko postojećeg koda pre bilo kakvih izmena. Određivanje prioriteta treba da bude vođeno učestalošću promena u funkcionalnostima i nivoom rizika: refaktorišite ekrane koje timovi aktivno menjaju ili delove sa visokom stopom grešaka, umesto stabilnog nasleđenog koda. Prilikom refaktorisanja preopterećenih kontrolera prikaza, razdvojte poslovnu i prezentacionu logiku u namenske klase ViewModel/Presenter i uvedite adaptere ili koordinatore zasnovane na protokolima kako biste izolovali nasleđeni UI od novog koda. Na kraju, održavajte brzinu razvoja i kvalitet kroz arhitektonsko upravljanje: obezbedite referentne implementacije zlatnog standarda, nametnite granice putem alata za statičku analizu koda u CI okruženju ili smernica za pregled koda, i kontinualno izdvajajte kapacitet za tehnički dug (npr. 15–20% kapaciteta ili uparivanje refaktorisanja sa povezanim radom na funkcionalnostima), dok istovremeno pratite metrike kao što su vreme kompajliranja, stope rušenja i pokrivenost testovima.
19Objavljujete visokoopterećenu iOS aplikaciju koja uključuje migraciju baze podataka i izmenu pozadinskog API (Application Programming Interface) servisa; kako biste osmislili plan puštanja u rad kako biste minimizovali uticaj na korisnike?
Planiranje puštanja u rad visokoopterećene iOS aplikacije koja uključuje migracije baze podataka i izmene pozadinskog API-ja zahteva razdvajanje postavljanja koda od aktivacije funkcionalnosti zbog specifičnih ograničenja iOS klijenta (asinhrona ažuriranja korisnika i nemogućnost prisilnog vraćanja klijentske binarne verzije na prethodno stanje). 1. Kompatibilnost na backend-u: Primenite strategiju proširenja i sužavanja (expand-and-contract). Postavite API v2 uporedo sa API v1 tako da i stare i ažurirane verzije klijenta funkcionišu istovremeno bez prekida rada. 2. Otporna migracija baze podataka: Osigurajte da su lokalne migracije šeme (npr. SQLite, Core Data, SwiftData) idempotentne, nedestruktivne i da bezbedno podnose prelaze preko više verzija (npr. nadogradnju sa N-3 na N) bez blokiranja glavne niti ili izazivanja petlji rušenja pri pokretanju. 3. Dinamička kontrola pristupa: Isporuka novih funkcionalnosti klijenta vrši se u skrivenom stanju iza zastavica funkcionalnosti (feature flags) ili daljinske konfiguracije sa servera. Držite zastavicu isključenom tokom početne distribucije. 4. Fazno puštanje u rad i praćenje: Distribuirajte aplikaciju putem funkcije App Store Phased Release (postepeno puštanje tokom 7 dana). Kontinuirano pratite telemetriju, stopu rušenja, metriku uspešnosti migracije baze i stopu grešaka API-ja. Ako se pojave anomalije, pauzirajte fazno puštanje u App Store Connect konzoli i isključite zastavice funkcionalnosti bez potrebe za hitnim povlačenjem i ponovnim slanjem binarne verzije aplikacije.
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.
20Projektujete sinhronizaciju koja funkcioniše primarno van mreže (offline-first) za aplikaciju za beleške gde se izmene moraju vremenom sinhronizovati na svim uređajima, ali iOS može odložiti ili otkazati rad u pozadini. Koju arhitekturu za izvršavanje u pozadini biste izabrali?
Arhitektura sinhronizacije prilagođena radu van mreže (offline-first) tretira lokalnu bazu podataka (kao što su SQLite, Core Data ili SwiftData) kao neposredni jedini izvor istine za stanje korisničkog interfejsa, dok se mutacije dodaju u perzistentni red za slanje (outbox) na disku, tako da neposlate izmene preživljavaju prekid procesa. Da bi se upravljalo oportunističkim i nedeterminističkim izvršavanjem u pozadini na iOS-u, pozadinski rad je struktuiran kroz više nivoa: 1. Pražnjenje outbox reda pri prelasku u pozadinu: Pokreće se preko `UIApplication.shared.beginBackgroundTask(expirationHandler:)` da bi se završile mutacije u toku ili brze izmene na čekanju. 2. Zakazana periodična sinhronizacija: Registrovana putem `BGTaskScheduler` koristeći `BGAppRefreshTask` za laganu sinhronizaciju metapodataka/razlika i `BGProcessingTask` za zahtevnije operacije sinhronizacije. 3. Sinhronizacija velikih resursa: Obavlja se van procesa preko pozadinskog `URLSessionConfiguration` koristeći otpremanje i preuzimanje datoteka. 4. Sinhronizacija inicirana sa servera: Oportunistička buđenja putem tihih push notifikacija (`content-available: 1`). Pošto pozadinski zadaci mogu biti prekinuti ili ponovo raspoređeni u bilo kom trenutku, operacije moraju koristiti ključeve idempotentnosti generisane na klijentu (npr. UUID) kako bi se omogućili bezbedni ponovni pokušaji bez dupliranja podataka. Ako se pozove `expirationHandler` nekog zadatka, rad u toku mora se odmah otkazati, nepotvrđene operacije vratiti u status čekanja i pozvati `setTaskCompleted(success: false)`. Eventualna konzistentnost se održava mehanizmima za razrešavanje konflikata kao što su CRDT (Conflict-free Replicated Data Types), verzioni vektori ili pravilo poslednjeg upisa (last-write-wins) sa oznakama za brisanje (tombstones), osiguravajući da udaljene promene ne prepišu nepotvrđene lokalne izmene iz outbox reda.
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()
}
}
}