20 usein kysyttyä iOS-haastattelukysymystä. iOS-kehitys on kysytty ala, jossa rakennetaan Swift-, SwiftUI- ja UIKit-sovelluksia ja ratkaistaan lifecycle-, rinnakkaisuus-, verkko-, tallennus-, suorituskyky-, testaus- ja App Store -julkaisutehtäviä. Kysymykset kattavat eri tasot, ja voit harjoitella niihin vastaamista ääneen haastatteluharjoittajassamme.
1Mitä vastuualueita UIViewController-oliolla tulisi olla tyypillisessä iOS:n MVC (Model-View-Controller) -arkkitehtuurissa?
Perinteisessä iOS:n MVC-arkkitehtuurissa (Model-View-Controller) `UIViewController` toimii ohjaimena (Controller), joka välittää tietoa näkymäkomponenttien (View) ja mallidatan (Model) välillä. Sen päävastuualueisiin kuuluvat näkymän elinkaaren hallinta (kuten `viewDidLoad`, `viewWillAppear`), käyttöliittymäelementtien alustaminen ja päivittäminen, suorien käyttäjäinteraktioiden käsittely (kuten painikkeiden painallukset, delegaatit ja target-actionit) sekä näyttösiirtymien ja esitysten koordinointi. Käyttöliittymään liittymättömät vastuut – kuten suorat verkkopyynnöt, tiedon pysyväistallennus ja raskas liiketoimintalogiikka – tulisi delegoida erillisille palvelu- tai malliolioille, jotta vältetään näkymäohjaimen paisuminen niin kutsutuksi Massive View Controlleriksi.
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
}
}
2Mikä on provisiointiprofiilin (provisioning profile) rooli iOS-sovelluksen julkaisussa?
iOS-julkaisussa provisiointiprofiili toimii kryptografisesti allekirjoitettuna pakettina, joka yhdistää Applen tietoturvavaatimukset sovellusbinaariin ja kohdelaitteisiin. Sen ensisijainen tehtävä on kertoa iOS-käyttöjärjestelmälle ja App Store Connectille, että sovelluksella on lupa toimia määritettyjen jakelusääntöjen mukaisesti.
Provisiointiprofiili yhdistää useita kriittisiä osia: App ID:n (Bundle Identifier), valtuutetut allekirjoitussertifikaatit (jotka vahvistavat kuka sovelluksen käänsi), sovellukselle myönnetyt oikeudet ja ominaisuudet (entitlements, kuten Push Notifications, Sign in with Apple tai iCloud) sekä – muissa kuin App Store -profiileissa, kuten Development- tai Ad-Hoc-profiileissa – sallittujen laitteiden UDID-tunnisteet (Unique Device Identifier). App Storen julkaisuprofiileista yksittäisten laitteiden UDID-tunnisteet jätetään pois, koska Apple sallii jakelun mille tahansa kuluttajalaitteelle App Storen kautta.
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)
3Mitä iOS-sovelluksen tausta-ajolla tarkoitetaan, ja mitä perusrajoituksia järjestelmä asettaa taustalla tehtävälle työlle?
iOS-sovelluksen tausta-ajo tarkoittaa koodin suorittamista silloin, kun sovellus ei ole aktiivisena näytöllä tai käyttäjän nähtävillä. Kun käyttäjä poistuu sovelluksesta, se siirtyy aktiivisesta etualatilasta taustalle ja pian sen jälkeen keskeytettyyn tilaan (suspended), jossa sen suoritus pysäytetään, vaikka sen muisti säilyy RAM-muistissa. Akun keston, järjestelmän reagoivuuden ja laiteresurssien säästämiseksi iOS valvoo taustasuoritusta tiukasti. Järjestelmä rajoittaa sitä, kuinka kauan sovellus voi suorittaa koodia taustalle siirtymisen jälkeen (yleensä lyhyt määräaikainen, noin 30 sekunnin ikkuna, ellei käytetä erityisiä taustatiloja tai ajastettuja tehtäviä), rajoittaa suorittimen ja verkon prioriteettia sekä voi sulkea keskeytettyjä sovelluksia, jos laitteen muisti käy vähiin.
4Mikä on Combine-kehyksen Publisher, ja miten se eroaa Subscriber-tilaajasta tyypillisessä iOS-tietovirrassa?
Combine-kirjastossa Publisher lähettää arvojen virran ajan kuluessa ja voi päättyä valmistumistapahtumaan (joko onnistuneesti tai virheeseen). Se määrittelee kaksi liittyvää tyyppiä: `Output` (tuotetun datan tyyppi) ja `Failure` (virheen tyyppi, jonka se voi tuottaa). Subscriber vastaanottaa arvot ja elinkaaritapahtumat Publisherilta. Tyypillisessä iOS-tietovirrassa Publisherit toimivat asynkronisten tapahtumien lähteenä tai tuottajana (kuten verkkopyynnön vastaukset, ilmoitukset tai käyttäjän syötteet), kun taas Subscriberit toimivat kuluttajina, jotka reagoivat lähetettyihin arvoihin, käsittelevät virheet ja reagoivat valmistumiseen (kuten päivittävät käyttöliittymän tilaa tai kirjoittavat tietokantaan). Subscriber kiinnittyy Publisheriin, vastaanottaa tilausviitteen (subscription) ja pyytää elementtejä tarpeen mukaan.
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)")
}
)
5Mitä async/await tarkoittaa Swiftissä, ja miten sitä käytetään asynkronisen API (Application Programming Interface) -rajapinnan kutsumiseen iOS-sovelluksesta?
`async/await` on Swiftin sisäänrakennettu syntaksi asynkronisen koodin kirjoittamiseen lineaarisesti, luettavasti ja peräkkäisesti sisäkkäisten sulkeumien (closures) ja valmistumiskäsittelijöiden (completion handlers) sijaan. Funktion merkitseminen avainsanalla `async` kertoo kääntäjälle, että funktio voi keskeyttää suorituksensa odottaessaan pitkäkestoisen työn (kuten verkkopyynnön tai tiedosto-I/O:n) valmistumista. Avainsana `await` osoittaa keskeytyskohdan (suspension point): nykyisen funktion suoritus keskeytyy vapauttaen taustalla olevan säikeen muihin töihin, ja jatkuu, kun odotettu tulos tai virhe on valmis. Asynkronista rajapintaa kutsutaan iOS-sovelluksessa kutsumalla `async`-metodia `await`-etuliitteellä (tai `try await` -rakenteella, jos funktio voi heittää virheen) asynkronisesta kontekstista, kuten toisen `async`-funktion tai `Task`-lohkon sisältä.
6Milloin UserDefaults-luokkaa tulisi käyttää iOS-sovelluksessa, ja millaista dataa sinne ei pitäisi tallentaa?
UserDefaults on suunniteltu pienten ja keveiden käyttäjäasetusten, valintojen ja lippujen tallentamiseen sovelluksen käynnistysten välillä (kuten teema-asetukset, äänenvoimakkuus tai `hasSeenOnboarding`-boolean-lippu). Se tukee property list -tyyppejä, kuten String, Int, Double, Bool, Date, Data, Array ja Dictionary. Sinne EI pitäisi tallentaa: 1. Arkaluonteisia tietoja (kuten käyttäjän salasanoja, yksityisiä avaimia tai autentikointitokeneita), koska UserDefaults tallentaa tiedot salaamattomina selkokieliseen plist-tiedostoon. 2. Suuria tietoaineistoja, dokumentteja tai mediatiedostoja (kuten kuvia, ääntä tai suuria JSON-hyötykuormia), koska koko plist ladataan muistiin sitä luettaessa, mikä aiheuttaa suurta muistinkulutusta ja hidastaa sovelluksen käynnistymistä.
// 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"
7Mitä tarkoittaa ARC (Automatic Reference Counting) Swift-kielessä, ja miten se hallitsee luokkainstanssien elinkaarta iOS-sovelluksessa?
ARC (Automatic Reference Counting) on Swiftin käännösaikainen muistinhallintamekanismi, jolla seurataan ja hallitaan luokkainstanssien (viittaustyyppien) elinkaarta. Aina kun luokasta luodaan uusi instanssi ja se sijoitetaan vahvaan viittaukseen (strong reference), ARC ylläpitää kyseisen instanssin sisäistä viittauslaskuria. Niin kauan kuin instanssiin on olemassa vähintään yksi vahva viittaus, ARC pitää instanssin muistissa. Kun kaikki vahvat viittaukset poistetaan ja viittauslaskuri putoaa nollaan, ARC vapauttaa instanssin välittömästi muistista ja kutsuu automaattisesti instanssin `deinit`-metodia juuri ennen vapauttamista.
Toisin kuin Java- tai .NET-ympäristöjen suorituksenaikainen roskienkeruu (Garbage Collector), ARC ei suorita säännöllisiä keruusyklejä; Swift-kääntäjä lisää tarvittavat retain- ja release-kutsut binaariin jo käännösaikana.
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
8Miten suoritetaan yksinkertainen HTTP (Hypertext Transfer Protocol) GET -pyyntö URLSession-luokalla iOS-sovelluksessa?
Yksinkertaisen HTTP GET -pyynnön tekemiseksi iOS-sovelluksessa luodaan URL-olio ja se välitetään `URLSession`-instanssille, kuten `URLSession.shared`. Pyyntö voidaan suorittaa käyttämällä datatehtävän takaisinkutsukäsittelijää (`URLSession.shared.dataTask(with: url) { data, response, error in ... }`) tai modernia Swiftin asynkronisuutta (`let (data, response) = try await URLSession.shared.data(from: url)`). Perinteistä `dataTask`-lähestymistapaa käytettäessä tehtävä on käynnistettävä kutsumalla sille metodia `.resume()`, käsiteltävä mahdolliset siirtovirheet, tarkistettava `HTTPURLResponse`-tilakoodi ja varmistettava, että käyttöliittymäpäivitykset ohjataan pääsäikeelle.
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()
9Mikä on Xcode-kehitysympäristön Instruments-työkalu, ja miten sitä käytetään yksinkertaisen suorituskykyongelman selvittämiseen iOS-sovelluksessa?
Instruments on Applen suorituskyvyn profilointi- ja analysointityökalu, joka toimitetaan Xcoden mukana. Sen avulla kehittäjät voivat seurata sovelluksen ajonaikaista toimintaa, suorittimen (CPU) käyttöä, muistinvarauksia, muistivuotoja sekä käyttöliittymän renderöintisuorituskykyä. Yksinkertaisen suorituskykyongelman (kuten käyttöliittymän nykimisen tai korkean suorittimen käytön) selvittämiseksi Instruments käynnistetään Xcodesta (valitsemalla Product -> Profile), valitaan sopiva mallipohja (kuten Time Profiler suorittimen pullonkauloille tai Allocations/Leaks muistiongelmille) ja tallennetaan suoritusjälki samalla kun ongelmallinen käyttäjäpolku toistetaan laitteella. Tallennuksen jälkeen analysoidaan kutsupuuta (call tree) ja raskaimpia kutsupinoja sellaisten metodien paikantamiseksi, jotka aiheuttavat viiveitä tai liiallista resurssinkulutusta. Tämän ansiosta pullonkaula voidaan mitata ja paikantaa ennen koodimuutosten tekemistä.
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.
10Mikä on APNs (Apple Push Notification service) ja mikä on sen rooli push-ilmoitusten toimittamisessa iOS-sovellukseen?
APNs (Apple Push Notification service) on Applen pilvipohjainen välityspalvelu, joka reitittää push-ilmoitukset turvallisesti taustapalvelimiltasi (provider servers) Apple-laitteille. Koska mobiililaitteet eivät voi ylläpitää jatkuvia, avoimia socket-yhteyksiä jokaisen yksittäisen sovelluksen taustajärjestelmään kuluttamatta akkua ja liikaa verkkokaistaa, Apple ylläpitää yhtä jatkuvaa, vähän virtaa kuluttavaa yhteyttä kunkin laitteen ja APNs-palvelun välillä. Kun sovelluksesi rekisteröityy etäilmoituksia varten, APNs luo yksilöllisen, peitetyn laitetunnisteen (device token), joka yksilöi kyseisen sovellusasennuksen kyseisellä laitteella. Sovellus lähettää tämän tunnisteen taustapalvelimellesi. Kun palvelin haluaa ilmoittaa käyttäjälle, se muodostaa hyötykuorman ja lähettää sen yhdessä laitetunnisteen kanssa APNs-palvelulle HTTP/2-protokollan ylitse. APNs etsii sitten laitteen aktiivisen yhteyden ja toimittaa push-ilmoituksen suoraan iOS:lle, joka näyttää ilmoituksen tai herättää sovelluksen asetusten mukaisesti.
11Miten refaktorisoisit suuren UIViewController-luokan, joka hoitaa käyttöliittymän päivitykset, validoinnin, verkkoliikenteen ja navigoinnin?
Liian suuren näkymäohjaimen (Massive View Controller) refaktorointi tulisi tehdä vaiheittain riskien minimoimiseksi ja eri vastuualueiden eriyttämiseksi omiin kerroksiinsa:
1. **Eristä verkkoliikenne ja data:** Siirrä API-kutsut (Application Programming Interface), tiedon pysyväistallennus ja JSON-jäsentäminen pois näkymäohjaimesta erillisiin Service- tai Repository-luokkiin protokolla-abstraktioita hyödyntäen.
2. **Eristä esitystila ja validointi:** Ota käyttöön ViewModel (tai Presenter). Siirrä syötteiden validointi, merkkijonojen muotoilu ja käyttöliittymän tilanhallinta ViewModeliin, jolloin tämä logiikka voidaan yksikkötestata erillään käyttöliittymästä.
3. **Eristä navigointi:** Ota käyttöön Coordinator-malli (tai Router / Flow Controller), jotta push/present-kutsut ja navigointilogiikka poistuvat näkymäohjaimesta.
4. **Pidä näkymäohjain kevyenä:** Jätä näkymäohjaimeen vain käyttöliittymän asettelu, alinäkymien määrittely, elinkaarikutsut ja käyttöliittymäkomponenttien sidonta ViewModeliin.
5. **Vaiheittainen toteutus testien avulla:** Kirjoita yksikkötestejä uusille palveluille ja ViewModel-luokille refaktoroinnin aikana varmistaaksesi, että sovelluksen aiempi toiminnallisuus säilyy muuttumattomana.
// 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() }
}
12Miten selvittäisit iOS-arkiston lähetysvirheen, joka johtuu allekirjoitus- tai provisiointivirheistä?
Allekirjoitus- tai provisiointiongelmista johtuvan iOS-arkiston lähetysvirheen vianmäärityksessä tarkastellaan kolmea pääaluetta: jakelusertifikaatteja (distribution certificates), provisiointiprofiileja (provisioning profiles) ja kohteen oikeuksia (entitlements).
1. **Tarkista diagnostiikkalokit:** Käy läpi Xcode Organizerin jakelulokit, yksityiskohtaiset validoinnin virheilmoitukset tai CI-konsolilokit (Continuous Integration, esim. `altool` / `notarytool`) tarkan hylkäyssyyn selvittämiseksi.
2. **Sertifikaatin ja identiteetin varmistus:** Varmista, että Avainnipussa (Keychain) on voimassa oleva Apple Distribution -varmenne vastaavan yksityisen avaimen kanssa, eikä varmenne ole vanhentunut, kumottu tai siltä puutu Apple WWDR -välivarmenne (Worldwide Developer Relations).
3. **Provisiointiprofiilin vastaavuus:** Varmista, että vientiin käytettävä profiili on App Store Distribution -profiili, joka vastaa tarkalleen sovelluksen Bundle Identifieria. Jos käytössä on ominaisuuksia kuten Push Notifications tai Associated Domains, varmista, että käytössä on eksplisiittinen App ID eikä yhteensopimaton yleismerkkitunnus (wildcard).
4. **Oikeuksien (entitlements) täsmäävyys:** Tarkista erot kohteen `.entitlements`-tiedoston ja Apple Developer Portalissa App ID:lle sallittujen kyvykkyyksien välillä. Jos paikalliset oikeudet vaativat lupia, joita ei ole rekisteröity portaalin profiiliin, lähetys epäonnistuu.
5. **Allekirjoituksen konfiguraatio:** Jos käytät automaattista allekirjoitusta, tarkista valittu tiimi ja Apple ID -tunnukset Xcoden asetuksista. Jos käytät manuaalista allekirjoitusta (tai CI-vientiasetuksia), varmista, että `ExportOptions.plist` yhdistää kunkin bundle ID:n oikeaan jakelusertifikaattiin ja -profiiliin.
# 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
13Miten valitsisit BGAppRefreshTaskin, BGProcessingTaskin ja taustalla toimivan URLSession-rajapinnan välillä tuotantotason taustasynkronointiominaisuudessa?
Oikean tausta-API:n valinta riippuu tehtävän kestosta, virta- ja verkkovaatimuksista sekä siirrettävän datan luonteesta:
1. **BGAppRefreshTask**: Paras lyhyille ja kevyille sisällönpäivityksille (kuten uutissyötteiden, sosiaalisen median aikajanojen tai koontinäyttöjen päivittämiseen), jotka kestävät noin 15–30 sekuntia. Järjestelmä ajoittaa nämä käyttötapojen perusteella siten, että tuore sisältö on valmiina juuri ennen kuin käyttäjä tyypillisesti avaa sovelluksen.
2. **BGProcessingTask**: Suunniteltu pitkäkestoisiin, ei-kiireellisiin ja raskaisiin toimenpiteisiin, kuten tietojen indeksointiin, tietokannan siivoukseen tai migraatioihin, koneoppimismallien koulutukseen tai laajojen datamäärien synkronointiin. Se voi suorittua useiden minuuttien ajan ja vaatia laitteen kytkemistä laturiin (`requiresExternalPower = true`) sekä verkkoon (`requiresNetworkConnectivity = true`), suorittuen tyypillisesti yön aikana.
3. **Taustalla toimiva URLSession (`URLSessionConfiguration.background`)**: Oikea valinta suurten tiedostojen siirtoon (kuvien/videoiden lataamiseen tai suurten resurssipakettien noutamiseen), joiden on jatkuttava erillisessä prosessissa, vaikka käyttöjärjestelmä keskeyttäisi tai sulkisi sovelluksen. Se siirtää tiedonsiirron `nsurlsessiond`-taustaprosessille sen sijaan, että sovelluskoodia pidettäisiin aktiivisena muistissa.
14Miten rakentaisit Combine-putken hakukentälle siten, että se viivästää syötteitä (debounce), jättää huomiotta kaksoiskappaleet, tekee verkkopyynnön ja päivittää käyttöliittymän turvallisesti?
Turvallisen hakuputken rakentaminen Combine-kehyksellä:
1. **Viivästys ja kaksoiskappaleiden poisto:** Otetaan hakukyselyn julkaisija (esim. `$queryText`), käytetään operaattoria `.debounce(for: .milliseconds(300), scheduler: RunLoop.main)` odottamaan kirjoitustaukoja ja operaattoria `.removeDuplicates()` ohittamaan muuttumattomat hakumerkkijonot.
2. **Verkkopyyntö ja peruminen:** Muunnetaan jokainen hakukysely verkkopyynnön julkaisijaksi ja litistetään suoritus operaattorilla `.switchToLatest()`. Tämä peruu automaattisesti kesken olevat pyynnöt uuden hakukyselyn saapuessa, mikä estää kilpailutilanteet ja vanhentuneet tulokset.
3. **Virheiden eristäminen:** Otetaan verkkoverheet kiinni sisemmässä julkaisijassa (esim. `.catch { _ in Just([]) }`), jotta verkkovirheet eivät päätä ulompaa hakutekstin tietovirtaa.
4. **Toimitus pääsäikeelle:** Käytetään operaattoria `.receive(on: DispatchQueue.main)` ennen tilan päivittämistä tai arvojen asettamista käyttöliittymäkomponenteille.
15Miten käyttäisit Swiftin async/await-ominaisuutta ja Task-peruutusta (Task cancellation) toteuttaaksesi näkymän, joka lataa dataa verkosta ja välttää käyttöliittymän (UI, User Interface) päivittämisen käyttäjän siirryttyä pois näkymästä?
Toteuttaisin verkkolatauksen `async`-funktiona palvelu- tai näkymämallissa (view model) ja suorittaisin sen `Task`-tehtävästä, jonka elinkaari on sidottu näkymään. UIKitissä tämä tarkoittaa yleensä muuttujan, kuten `var loadTask: Task<Void, Never>?`, tallentamista näkymänohjaimeen (view controller) tai näkymämalliin, sen käynnistämistä silloin kun näkymän data pitää ladata, ja sen peruuttamista `viewWillDisappear`-metodissa, `deinit`-vaiheessa tai ennen uuden pyynnön käynnistämistä halutusta elinkaaresta riippuen. SwiftUIssa käyttäisin mieluiten `.task`- tai `.task(id:)` -määrettä aina kun mahdollista, koska SwiftUI peruuttaa sen automaattisesti näkymän poistuessa tai tunnisteen muuttuessa. Tehtävän sisällä kutsuisin asynkronista verkkorajapintaa lausekkeella `try await`, käsittelisin peruutuksen erillään varsinaisista virheistä ja tarkistaisin peruutustilan ennen tulosten soveltamista, jos koodissa on useita `await`-vaiheita tai käsittelyvaiheita. Käyttöliittymän tilanmuutosten täytyy tapahtua päätoimijalla (main actor), joko merkitsemällä näkymämalli `@MainActor`-annotaatiolla tai käyttämällä kutsua `await MainActor.run`. Vanhentuneen käyttöliittymätilan välttämiseksi peruuttaisin aiemmat tehtävät, välttäisin erillisiä tai globaaleja tehtäviä (detached/global tasks) näkymään sidotussa työssä ja vertaisin valinnaisesti pyyntötunnistetta tai nykyisen kohteen id:tä ennen tuloksen asettamista.
@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)
}
}
}
}
16Miten valitsisit ratkaisun UserDefaults-, Keychain-, tiedostojärjestelmä-, SQLite- ja Core Data -vaihtoehtojen välillä erilaisille sovellusdatan tyypeille?
Sopivan iOS-tallennusmekanismin valinta riippuu datan sensitiivisyydestä, koosta, relaatiorakenteen monimutkaisuudesta, kyselytarpeista ja varmuuskopioinnin elinkaaresta: 1. **Keychain**: Sensitiiviselle datalle (todennustokenit, salasanat, salausavaimet, biometriset salaisuudet). Se tarjoaa laitteistotason suojauksen (hardware-backed encryption), säilyy sovelluksen uudelleenasennusten yli ja pysyy turvassa myös hiekkalaatikkoympäristön vaarantuessa. 2. **UserDefaults**: Kevyille, ei-sensitiivisille asetuksille ja lipuille (esim. `hasCompletedOnboarding`, teemavalinta). Koska se ladataan kokonaisuudessaan muistiin property list -muodossa, sitä ei tule käyttää suurille aineistoille, medialle tai sensitiivisille tokeneille. 3. **Tiedostojärjestelmä (Documents / Caches / Application Support)**: Erillisille tiedostoille ja suurille binaariobjekteille (ladatut kuvat, ääni, PDF-tiedostot). `Documents` on käyttäjälle näkyvä ja varmuuskopioidaan iCloudiin; `Caches` on tarkoitettu poistettavissa olevalle, uudelleen ladattavalle sisällölle; `Application Support` on tarkoitettu käyttäjältä piilotetuille pysyville tiedostoille. 4. **SQLite**: Rakenteiselle, taulukkomuotoiselle datalle, joka vaatii nopeita indeksoituja kyselyitä, massatoimintoja tai alustariippumatonta tietokannan jakamista ilman oliograafin tuomaa lisäkuormaa. 5. **Core Data / SwiftData**: Monimutkaisille oliograafeille relaatioineen, muutosten seurantoineen, laiskalatauksineen (faulting), peruutustoimintoineen sekä suorine integraatioineen UIKitin (`NSFetchedResultsController`) tai SwiftUIn (`@Query`) kanssa.
17Miten tuotantotason UIKit-näkymässä, jossa käytetään näkymämallin takaisinkutsuja, päätät, kaapataanko self sulkeumakäsittelijöissä määritteellä weak, unowned vai strong?
UIKit-arkkitehtuurissa, jossa näkymäohjain (view controller) viestii näkymämallin (view model) kanssa takaisinkutsujen kautta, kaappaustavan määräävät omistajuus ja elinkaari:
1. `[weak self]`: Standardi ja turvallisin lähestymistapa tallennetuille tai karkaaville (escaping) sulkeumille (kuten näkymämallin tapahtumakutsut tai asynkroniset verkko- ja datakutsut). Koska näkymäohjain omistaa näkymämallin, `self`-viitteen vahva kaappaaminen näkymämallin tallentamassa sulkeumassa aiheuttaa vahvan viittauskehän (`VC -> VM -> sulkeuma -> VC`). `[weak self]` muuntaa viitteen valinnaiseksi (`UIViewController?`), mikä mahdollistaa olion vapauttamisen muistista ja estää muistivuodot.
2. `[unowned self]`: Olettaa, että `self` ei ole koskaan nil sulkeumaa suoritettaessa. Tätä tulisi käyttää käyttöliittymänäkymissä äärimmäisen varoen. Jos asynkroninen tehtävä tai takaisinkutsu valmistuu sen jälkeen, kun näkymäohjain on jo poistettu näytöltä ja purettu muistista, viittaus `unowned self` -olioon aiheuttaa sovelluksen kaatumisen ajonaikana. Tästä syystä `[weak self]` on tuotantotason UIKit-koodissa yleensä huomattavasti suositeltavampi kuin `unowned`.
3. Vahva kaappaus (oletus, ei kaappauslistaa): Soveltuu tilanteisiin, joissa sulkeuma ei ole karkaava (esim. standardit kokoelmaoperaatiot kuten `map` ja `filter`) tai kun sulkeuma on lyhytikäinen eikä muodosta omistussilmukkaa takaisin `self`-olioon.
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 */ }
}
18Olet aloittamassa työt kypsässä iOS-sovellusprojektissa, jossa on satoja näkymiä, useita massiivisia näkymäohjaimia (Massive View Controller) ja hitaat julkaisusyklit. Miten suunnittelisit asteittaisen arkkitehtuurin parantamisstrategian pysäyttämättä uusien ominaisuuksien toimittamista?
Tehokas asteittainen migraatiostrategia välttää kokonaan uudelleenkirjoittamista ja yhdistää refaktoroinnin suoraan jatkuvaan ominaisuuskehitykseen (noudattaen Strangler Fig -mallia). Työ aloitetaan määrittelemällä sovittu tavoitearkkitehtuuri (kuten MVVM tai VIPER riippuvuuksien injektoinnilla ja modulaarisilla koordinaattoreilla) sekä luomalla testipohja (luonnehdinta-, snapshot- ja yksikkötestit) vanhalle koodille ennen sen muuttamista. Priorisoinnin tulisi perustua koodin muutosnopeuteen ja riskeihin: refaktoroidaan ne näkymät, joita tiimit muokkaavat aktiivisesti tai joissa esiintyy runsaasti virheitä, vakaan perintökoodin sijaan. Massiivisia näkymäohjaimia (Massive View Controller) refaktoroitaessa liiketoiminta- ja esityskerroslogiikka irrotetaan erillisiin ViewModel- tai Presenter-luokkiin, ja uusi koodi eristetään vanhasta käyttöliittymästä protokollapohjaisten adapterien tai koordinaattorien avulla. Lopuksi nopeutta ja laatua ylläpidetään arkkitehtuurin hallintamallilla: tarjotaan esimerkillisiä referenssitoteutuksia, valvotaan arkkitehtuurirajoja CI-lintereillä tai katselmointikäytännöillä ja varataan jatkuvaa kapasiteettia tekniselle velalle (esim. 15–20 % kapasiteetista tai yhdistämällä refaktorointi vastaaviin ominaisuustöihin) samalla seuraten mittareita, kuten käännösaikoja, kaatumisasteita ja testikattavuutta.
19Olet julkaisemassa suuren liikenteen iOS-sovellusta, johon liittyy tietokantamigraatio sekä taustajärjestelmän API (Application Programming Interface) -muutos; miten suunnittelisit julkaisusuunnitelman käyttäjävaikutusten minimoimiseksi?
Suuren liikenteen iOS-sovelluksen julkaisusuunnitelman laatiminen tietokantamigraation ja taustajärjestelmän API-muutosten yhteydessä edellyttää käyttöönoton (deployment) erottamista ominaisuuden aktivoinnista (activation) iOS-asiakasohjelmien rajoitusten vuoksi (epätahtiset käyttäjäpäivitykset ja mahdottomuus pakottaa asiakaspuolen binäärien takaisinrullauksia).
1. Taustajärjestelmän yhteensopivuus: Hyödynnä laajenna ja supista -strategiaa (expand-and-contract). Ota käyttöön API v2 rinnakkain API v1:n kanssa, jotta sekä vanhat että päivitetyt asiakasversiot toimivat samanaikaisesti ilman rikkoutuvia muutoksia.
2. Vikasietoinen tietokantamigraatio: Varmista, että paikalliset skeemamigraatiot (kuten SQLite, Core Data, SwiftData) ovat idempotentteja, tuhoamattomia ja käsittelevät turvallisesti useiden versioiden yli hyppäävät päivitykset (esimerkiksi siirtymisen versiosta N-3 versioon N) estämättä pääsäiettä tai aiheuttamatta käynnistyksen kaatumissilmukoita.
3. Dynaaminen hallinta (gating): Julkaise uudet asiakasominaisuudet piilotettuina palvelinohjattujen ominaisuuslippujen (feature flags) tai etäkonfiguraation taakse. Pidä ominaisuuslippu pois päältä alustavan jakelun aikana.
4. Vaiheittainen julkaisu ja valvonta: Jakele sovellus App Storen vaiheittaisella julkaisulla (App Store Phased Release, 7 päivän porrastettu julkaisu). Seuraa jatkuvasti telemetriaa, kaatumismääriä, tietokantamigraatioiden onnistumismittareita ja API-virhemääriä. Jos poikkeamia ilmenee, keskeytä vaiheittainen julkaisu App Store Connectissa ja kytke ominaisuusliput pois päältä ilman hätätilanteen binääripäivityksen tarvetta.
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.
20Suunnittelet offline-first-synkronointia muistiinpanosovellukselle, jossa muokkausten on lopulta synkronoiduttava laitteiden välillä, mutta iOS saattaa lykätä tai peruuttaa taustatyöt. Minkä tausta-ajon arkkitehtuurin valitsisit?
Offline-first-synkronointiarkkitehtuuri käsittelee paikallista tietokantaa (kuten SQLite, Core Data tai SwiftData) käyttöliittymätilan välittömänä totuuden lähteenä (single source of truth), kun taas muokkaukset lisätään levylle pysyvään outbox-jonoon, jotta lähettämättömät muokkaukset säilyvät prosessin päättymisen yli. iOS:n opportunistisen ja ei-deterministisen tausta-ajon hallitsemiseksi taustatyö jaetaan useaan tasoon:
1. Outboxin tyhjennys taustalle siirryttäessä: Käynnistetään kutsulla `UIApplication.shared.beginBackgroundTask(expirationHandler:)` käynnissä olevien tai nopeiden odottavien muokkausten viimeistelemiseksi.
2. Ajastettu jaksottainen synkronointi: Rekisteröidään `BGTaskScheduler`-komponenttiin käyttäen `BGAppRefreshTask`-tehtävää kevyelle metatiedon tai deltan synkronoinnille ja `BGProcessingTask`-tehtävää raskaammille synkronointioperaatioille.
3. Suurten aineistojen synkronointi: Käsitellään prosessin ulkopuolella tausta-ajon `URLSessionConfiguration`-konfiguraatiolla hyödyntäen tiedostojen latauksia.
4. Palvelinlähtöinen synkronointi: Opportunistiset herätykset hiljaisten push-ilmoitusten kautta (`content-available: 1`).
Koska taustatehtävät voivat keskeytyä tai tulla uudelleenajastetuiksi missä vaiheessa tahansa, operaatioiden on käytettävä asiakkaan generoimia idempotenssiavaimia (kuten UUID-tunnisteita) turvallisten uudelleenyritysten mahdollistamiseksi ilman datan kahdentumista. Jos tehtävän `expirationHandler` laukeaa, käynnissä oleva työ on peruutettava välittömästi, vahvistamattomat operaatiot merkittävästi takaisin odottaviksi ja kutsuttava `setTaskCompleted(success: false)`. Lopullinen yhdenmukaisuus (eventual consistency) ylläpidetään konfliktinratkaisumekanismeilla, kuten CRDT-rakenteilla, versiovektoreilla tai last-write-wins-periaatteella hyödyntäen poistomerkintöjä (tombstones), mikä varmistaa, etteivät etädeltat ylikirjoita vahvistamattomia paikallisia outbox-muutoksia.
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()
}
}
}