20 често задавани въпроса за интервю за iOS. iOS разработката е търсено направление за приложения със Swift, SwiftUI и UIKit, както и за lifecycle, конкурентност, мрежа, съхранение, производителност, тестове и публикуване в App Store. Въпросите покриват различни нива и можете да упражнявате устни отговори в нашия тренажор за интервюта.
1Какви отговорности трябва да има UIViewController в типична iOS MVC (Model-View-Controller) архитектура?
В класическата iOS MVC (Model-View-Controller) архитектура UIViewController действа като контролер (Controller), който посредничи между компонентите на изгледа (View) и данните в модела (Model). Основните му отговорности включват управление на жизнения цикъл на изгледа (например viewDidLoad, viewWillAppear), конфигуриране и обновяване на елементите от потребителския интерфейс, обработка на директните потребителски взаимодействия (като натискания на бутони, делегати и target-action шаблони) и организиране на преходите или представянето на екрани. Отговорности, които не са свързани с потребителския интерфейс — като директна мрежова комуникация, съхранение на данни и тежка бизнес логика — трябва да бъдат делегирани на специализирани услуги или моделни обекти, за да се предотврати превръщането на контролера в 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
}
}
2Каква е ролята на профила за осигуряване (provisioning profile) при публикуването на приложение за iOS?
При публикуването на iOS приложение профилът за осигуряване (provisioning profile) действа като криптографски подписан пакет, който свързва изискванията за сигурност на Apple с компилирания файл на приложението и целевите устройства. Основната му роля е да укаже на операционната система iOS и на App Store Connect, че приложението е упълномощено да се изпълнява при специфични правила за разпространение. Профилът за осигуряване обединява няколко критични компонента: App ID (Bundle Identifier), упълномощените сертификати за подписване, потвърждаващи кой е компилирал приложението, правата и възможностите (entitlements и capabilities), предоставени на приложението (като Push Notifications, Sign in with Apple или iCloud), и — при профили извън App Store като Development или Ad-Hoc — списък с разрешените идентификатори на устройства (UDID). При профилите за публикуване в App Store отделните идентификатори на устройства (UDID) се пропускат, тъй като Apple разрешава разпространението до всяко крайно потребителско устройство през 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)
3Какво означава едно iOS приложение да изпълнява работа във фонов режим (background) и какви основни ограничения налага операционната система върху тази работа?
Изпълнението на задачи във фонов режим в iOS означава стартиране на код, когато приложението не е активно на екрана или видимо за потребителя. Когато потребителят излезе от приложението, то преминава от активно състояние на преден план във фонов режим, а малко след това в състояние на временно спиране (suspended), при което изпълнението му е паузирано, въпреки че заделената памет остава в RAM. За да запази живота на батерията, бързодействието на системата и ресурсите на устройството, iOS стриктно контролира фоновото изпълнение. Системата ограничава времето, през което приложението може да изпълнява код след преминаване във фон (обикновено ограничен до кратък времеви прозорец от около 30 секунди, освен ако не се използват специфични фонови режими или планирани задачи), ограничава приоритета на процесора и мрежата и може да прекрати спрените приложения при недостиг на оперативна памет.
4Какво представлява Combine publisher и как се различава от subscriber в типичния поток от данни в iOS?
В рамката Combine един Publisher излъчва поток от стойности във времето и може да приключи със събитие за завършване (успешно завършване или грешка). Той дефинира два асоциирани типа: `Output` (типът данни, които произвежда) и `Failure` (типът грешка, която може да излъчи). Един Subscriber получава стойности и събития от жизнения цикъл от даден publisher. В типичния поток от данни в iOS издателите (publishers) действат като източник или производител на асинхронни събития (като мрежови отговори, известия или потребителски вход), докато абонатите (subscribers) действат като консуматори, които реагират на излъчените стойности, обработват грешки и отговарят на събитието за приключване (например актуализиране на състоянието на интерфейса или запис в база данни). Абонатът се прикачва към издателя, получава абонаментен токен и заявява търсене (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)")
}
)
5Какво означава async/await в Swift и как се използва за извикване на асинхронно API от приложение за iOS?
`async/await` е вграденият синтаксис на Swift за писане на асинхронен код по линеен, четим и последователен начин, вместо да се използват вложени затваряния (closures) и функции за обратно извикване (completion handlers). Маркирането на функция с `async` указва на компилатора, че тя може временно да прекъсне своето изпълнение, докато чака завършването на продължителна операция (като мрежови заявки или файлови входно-изходни операции). Ключовата дума `await` обозначава точка на прекъсване: изпълнението на текущата функция се поставя на пауза, освобождавайки използваната нишка за друга работа, и се възобновява след получаване на очаквания резултат или грешка. За да извикате асинхронно API в приложение за iOS, извиквате `async` метода, предшестван от `await` (и `try await`, ако функцията може да хвърли грешка), в рамките на асинхронен контекст, например в друга `async` функция или `Task` блок.
6Кога бихте използвали UserDefaults в iOS приложение и какви видове данни не трябва да се съхраняват там?
UserDefaults е предназначен за съхранение на малки, леки потребителски предпочитания, настройки и флагове между отделните стартирания на приложението (например предпочитания за тема, ниво на звука или булев флаг `hasSeenOnboarding`). Той поддържа типове от property list като String, Int, Double, Bool, Date, Data, Array и Dictionary. НЕ трябва да съхранявате: 1. Чувствителна информация (като потребителски пароли, частни ключове или токени за удостоверяване), тъй като UserDefaults съхранява данните некриптирани в обикновен текстов plist файл. 2. Големи набори от данни, документи или медийни файлове (като изображения, аудио или големи JSON пакети от данни), тъй като целият plist файл се зарежда в паметта при достъп, което води до висок разход на памет и забавя стартирането на приложението.
// 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Какво представлява ARC (Automatic Reference Counting) в Swift и как управлява времето на живот на инстанциите на класове в iOS приложение?
ARC (Automatic Reference Counting) е механизмът за управление на паметта по време на компилация в Swift, предназначен за проследяване и управление на времето на живот на инстанциите на класове (референтни типове). Всеки път, когато се създаде нова инстанция на клас и тя се присвои към силна референция (strong reference), ARC следи вътрешен брояч на референциите за тази инстанция. Докато съществува поне една силна референция към инстанцията, ARC я запазва в паметта. Когато всички силни референции бъдат премахнати и броячът спадне до нула, ARC незабавно освобождава инстанцията, за да освободи памет, като автоматично извиква метода `deinit` непосредствено преди освобождаването. За разлика от събирането на неизползвана памет по време на изпълнение в среди като Java или .NET, ARC не извършва периодични цикли на почистване; компилаторът на Swift вмъква съответните извиквания за запазване и освобождаване в двоичния код още при компилация.
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
8Как се извършва проста HTTP (Hypertext Transfer Protocol) GET заявка с URLSession в iOS приложение?
За да изпълните проста HTTP GET заявка в iOS приложение, създавате обект `URL` и го подавате на инстанция на `URLSession`, например `URLSession.shared`. Можете да изпълните заявката чрез обработчик за завършване на задача за данни (`URLSession.shared.dataTask(with: url) { data, response, error in ... }`) или чрез модерния синтаксис за конкурентност в Swift (`let (data, response) = try await URLSession.shared.data(from: url)`).
Когато използвате традиционния подход с `dataTask`, трябва да извикате `.resume()` върху задачата, за да я стартирате, да обработите евентуални грешки при преноса, да проверите кода за състояние на `HTTPURLResponse` и да се уверите, че всички актуализации на потребителския интерфейс се пренасочват към главната нишка.
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Какво представлява Instruments в Xcode и как бихте го използвали за изследване на прост проблем с производителността в iOS приложение?
Instruments е инструментът на Apple за профилиране и анализ на производителността, включен в пакета на Xcode. Той позволява на разработчиците да следят поведението по време на изпълнение, натоварването на процесора (CPU), заделянето на памет, изтичането на памет и производителността при изчертаване на потребителския интерфейс.
За да изследвате прост проблем с производителността (като насичане на интерфейса или високо натоварване на процесора), стартирате Instruments от Xcode (чрез Product -> Profile), избирате подходящ шаблон (като Time Profiler за тесни места в процесора или Allocations/Leaks за проблеми с паметта) и записвате трасировка (trace), докато възпроизвеждате проблемния потребителски сценарий на реално устройство. След записа анализирате дървото на извикванията (call tree) и най-натоварените стекове от извиквания, за да локализирате точните методи, причиняващи забавяне или прекомерна консумация на ресурси, което ви позволява да измерите и изолирате тясното място, преди да правите промени в кода.
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Какво представлява APNs (Apple Push Notification service) и каква роля играе при доставянето на насочени известия (push notifications) до iOS приложение?
APNs (Apple Push Notification service) е облачната посредническа услуга на Apple, която препраща сигурно насочени известия (push notifications) от вашите бекенд сървъри (provider сървъри) към устройства на Apple. Тъй като мобилните устройства не могат да поддържат постоянни, отворени сокет връзки към бекенда на всяко отделно приложение, без това да изтощава батерията и да консумира излишен мрежов трафик, Apple поддържа една-единствена постоянна връзка с ниска консумация на енергия между всяко устройство и APNs. Когато приложението ви се регистрира за отдалечени известия, APNs генерира уникален токен за устройството (device token), който идентифицира конкретната инсталация на приложението на съответното устройство. Приложението изпраща този токен към вашия бекенд сървър. Когато доставчикът иска да изпрати известие на потребителя, той конструира полезен товар (payload) и го изпраща заедно с токена на устройството към APNs през HTTP/2. След това APNs открива активната връзка за това устройство и доставя съдържанието на известието директно на iOS, която показва известието или активира приложението според конфигурацията.
11Как бихте рефакторирали голям UIViewController, който управлява обновяването на потребителския интерфейс (UI - User Interface), валидация, мрежова комуникация и навигация?
Рефакторирането на претоварен контролер (Massive View Controller / MVC) трябва да се извършва поетапно, за да се намали рискът, като същевременно отделните отговорности се разделят в специализирани слоеве: 1. Извличане на мрежовата комуникация и данните: Преместете API извикванията, персистентността на данните и парсването на JSON извън контролера в специализирани Service или Repository класове с протоколни абстракции. 2. Извличане на презентационното състояние и валидацията: Въведете ViewModel (или Presenter). Прехвърлете валидацията на входните данни, форматирането на низове и управлението на състоянието на потребителския интерфейс във ViewModel, правейки тази логика лесна за тестване с модулни тестове (unit tests) в пълна изолация. 3. Извличане на навигацията: Приложете шаблона Coordinator (или Router / Flow Controller), за да премахнете push/present извикванията и логиката на навигационния поток от контролера. 4. Запазване на контролера максимално изчистен: Оставете в него само подредбата на интерфейса (UI layout), конфигурацията на подгледките (subviews), методите от жизнения цикъл и свързването на контролите на потребителския интерфейс към ViewModel. 5. Поетапно изпълнение с тестове: Добавяйте модулни тестове за новоизвлечените услуги и ViewModel класове по време на рефакторирането, за да гарантирате, че съществуващото поведение остава непроменено.
// 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() }
}
12Как бихте диагностицирали и отстранили грешка при качване на iOS архив (archive upload failure), причинена от проблеми с подписването или профилите за осигуряване (provisioning errors)?
Отстраняването на грешка при качване на iOS архив, причинена от проблеми с подписването или профилите за осигуряване, изисква проверка на три основни области: сертификати за дистрибуция, профили за осигуряване (provisioning profiles) и права за достъп на таргета (target entitlements). 1. Преглед на диагностичните логове: Проверете логовете за дистрибуция в Xcode Organizer, детайлните грешки при валидация на качването или конзолните логове от CI системата (`altool`/`notarytool`), за да идентифицирате точната причина за отхвърлянето. 2. Проверка на сертификата и идентичността: Уверете се, че в Keychain присъства валиден сертификат Apple Distribution със съответстващия му частен ключ и че той не е изтекъл, не е анулиран и междинният сертификат Apple WWDR не липсва. 3. Съвпадение на профила за осигуряване: Уверете се, че профилът, използван за експортиране, е App Store Distribution профил, който съответства на точния Bundle Identifier. Ако се използват възможности като Push Notifications или Associated Domains, проверете дали е конфигуриран изричен App ID, а не несъвместим шаблон с маска (wildcard). 4. Съответствие на правата за достъп (Entitlements): Проверете за несъответствия между файла `.entitlements` на таргета и активираните възможности за съответния App ID в Apple Developer Portal. Ако локалните права изискват разрешения, които не са регистрирани в профила в портала, качването ще се провали. 5. Конфигурация на подписването: При използване на автоматично подписване проверете избрания екип (Team) и идентификационните данни за Apple ID в настройките на Xcode. При ръчно подписване (или опции за експорт в CI) се уверете, че `ExportOptions.plist` свързва всеки bundle ID с правилния сертификат и профил за дистрибуция.
# 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
13Как бихте избрали между BGAppRefreshTask, BGProcessingTask и фонова URLSession за функционалност за фонова синхронизация в продукционна среда?
Изборът на правилния фонов API зависи от продължителността на задачата, изискванията за захранване и мрежа, както и естеството на прехвърляните данни: 1. **BGAppRefreshTask**: Най-подходящ за кратки и леки обновявания на съдържание (като обновяване на новинарски емисии, социални мрежи или потребителски табла), които отнемат приблизително 15–30 секунди. Системата ги планира въз основа на навиците на потребителя, така че актуалното съдържание да е готово точно преди той обичайно да отвори приложението. 2. **BGProcessingTask**: Проектиран за продължителни, неотложни и тежки операции, като индексиране на данни, почистване/миграции на бази данни, обучение на ML модели или мащабна синхронизация на данни. Може да се изпълнява с минути и да изисква устройството да се зарежда (`requiresExternalPower = true`) и да е свързано към Wi-Fi/мрежа (`requiresNetworkConnectivity = true`), като обикновено се задейства през нощта. 3. **Фонова URLSession (`URLSessionConfiguration.background`)**: Правилният избор при прехвърляне на големи файлове (качване на снимки/видеоклипове или изтегляне на големи пакети с ресурси), които трябва да продължат извън процеса на приложението, дори ако то е временно спряно или терминирано от операционната система. Мрежовият трансфер се делегира на фоновата системна услуга `nsurlsessiond`, вместо кодът на приложението да се поддържа активен в паметта.
14Как бихте изградили конвейер с Combine за текстово поле за търсене, който забавя обработката (debounce), игнорира дубликати, изпълнява мрежова заявка и обновява безопасно потребителския интерфейс?
За изграждането на безопасен конвейер за търсене с Combine: 1. **Забавяне и премахване на дубликати (Debounce & Deduplicate):** Вземете издателя (publisher) на заявката за търсене (напр. `$queryText`), приложете `.debounce(for: .milliseconds(300), scheduler: RunLoop.main)`, за да изчакате паузите при въвеждане, и `.removeDuplicates()`, за да игнорирате непроменени низове. 2. **Мрежова заявка и отмяна:** Преобразувайте всяка заявка към издател на мрежова заявка и го разгърнете с `.switchToLatest()`. Това автоматично отменя активните заявки в ход при постъпване на нова заявка за търсене, предотвратявайки състезания за ресурси и остарели резултати. 3. **Изолиране на грешки:** Прихванете мрежовите грешки във вътрешния издател (напр. `.catch { _ in Just([]) }`), така че мрежовите сривове да не прекратяват външния поток от въвеждан текст. 4. **Предаване към главната нишка:** Приложете `.receive(on: DispatchQueue.main)` преди обновяване на състоянието или присвояване към елементи на потребителския интерфейс.
15Как бихте използвали Swift async/await и отмяна на задачи (`Task`), за да реализирате екран, който зарежда данни от мрежата и избягва обновяването на UI (User Interface), след като потребителят напусне екрана?
Бих дефинирал зареждането от мрежата като `async` функция в услуга или view model и бих я стартирал от `Task`, чийто жизнен цикъл е обвързан с екрана. В UIKit това обикновено означава запазване на променлива като `var loadTask: Task<Void, Never>?` във view controller или view model, стартирането ѝ, когато екранът трябва да зареди данни, и отмяната ѝ в `viewWillDisappear`, `deinit` или преди стартиране на нова заявка, в зависимост от желания жизнен цикъл. В SwiftUI бих предпочел `.task` или `.task(id:)`, когато е възможно, тъй като SwiftUI автоматично го отменя при скриване на изгледа или при промяна на идентификатора. В рамките на задачата бих извикал асинхронния мрежов API с `try await`, обработил отмяната отделно от реалните грешки и проверил за отмяна преди прилагане на резултатите, ако има множество последователни `await` извиквания или стъпки на обработка. Промените в състоянието на потребителския интерфейс трябва да се извършват на главния актьор – или чрез анотиране на view model с `@MainActor`, или чрез `await MainActor.run`. За да се избегне показването на остаряло състояние на потребителския интерфейс, бих отменял предходните задачи, избягвал несвързани глобални задачи за работа, привързана към екрана, и опционално сравнявал идентификатора на заявката или текущия елемент преди прилагане на резултата.
@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)
}
}
}
}
16Как бихте избрали между UserDefaults, Keychain, файловата система, SQLite и Core Data за различните типове данни в приложението?
Изборът на подходящ механизъм за съхранение в iOS зависи от чувствителността на данните, техния размер, релационната сложност, изискванията към заявките и жизнения цикъл на архивирането: 1. **Keychain**: За чувствителни данни (токени за автентикация, пароли, ключове за криптиране, биометрични данни). Осигурява хардуерно базирано криптиране, запазва се при преинсталиране на приложението и остава защитен дори при компрометирана изолирана среда (sandbox). 2. **UserDefaults**: За леки, нечувствителни потребителски предпочитания и флагове (напр. `hasCompletedOnboarding`, избрана цветова тема). Тъй като се зарежда изцяло в паметта като property list, не трябва да се използва за големи обеми от данни, мултимедия или чувствителни токени. 3. **Файлова система (Documents / Caches / Application Support)**: За отделни файлове и големи бинарни обекти (изтеглени изображения, аудио, PDF файлове). `Documents` е достъпна за потребителя директория и се архивира в iCloud; `Caches` е за съдържание, което може да бъде изчистено от системата и изтеглено отново; `Application Support` е за персистентни файлове, които не са предназначени за пряк достъп от потребителя. 4. **SQLite**: За структурирани таблични данни, изискващи бързи индексирани заявки, масови операции или споделяне на база данни между различни платформи без overhead от граф на обектите. 5. **Core Data / SwiftData**: За сложни графи от обекти с релации, проследяване на промените (change tracking), зареждане при поискване (faulting), поддръжка на отмяна на действия (undo management) и директна интеграция с UIKit (`NSFetchedResultsController`) или SwiftUI (`@Query`).
17В производствен UIKit екран с функции за обратно извикване (callbacks) от view model, как решавате дали да захванете self като weak, unowned или strong в обработчиците на затваряния (closures)?
В UIKit архитектура, където view controller взаимодейства с view model чрез функции за обратно извикване, стратегията за захващане се определя от управлението на собствеността и жизнения цикъл:
1. `[weak self]`: Стандартният и най-безопасен подход за съхранявани или излизащи от обхвата (escaping) затваряния (като събития от view model или асинхронни обработчици за завършване на мрежови заявки/данни). Тъй като view controller притежава view model, силното захващане на `self` в запазена от view model функция за обратно извикване създава цикъл от силни референции (`VC -> VM -> closure -> VC`). `[weak self]` превръща `self` в опционална стойност (`UIViewController?`), което позволява на `self` да се освободи чисто от паметта и предотвратява изтичане на памет (memory leaks).
2. `[unowned self]`: Предполага, че `self` никога няма да бъде nil при изпълнението на затварянето. Трябва да се използва с изключително внимание в екрани на потребителския интерфейс. Ако асинхронна задача или функция за обратно извикване завърши, след като view controller е бил затворен и освободен, достъпът до `unowned self` предизвиква фатален срив по време на изпълнение. Поради тази причина `[weak self]` е общоприетият избор пред `unowned` в производствени UIKit интерфейси.
3. **Силно захващане (по подразбиране, без списък за захващане):** Подходящо е, когато затварянето е non-escaping (например стандартни операции върху колекции като `map`/`filter`) или когато затварянето е краткотрайно и не създава цикъл на собственост обратно към `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 */ }
}
18Присъединявате се към зряло iOS приложение със стотици екрани, множество претоварени view controller-и (Massive View Controllers) и бавни цикли на пускане на версии. Как бихте планирали стратегия за поетапно подобряване на архитектурата, без да спирате предоставянето на нови функционалности?
Ефективната стратегия за поетапна миграция избягва пълното пренаписване на системата и съчетава рефакторирането директно с текущото предоставяне на функционалности (следвайки модела Strangler Fig). Започва се с установяване на съгласуван целеви архитектурен план (като MVVM или VIPER с инжектиране на зависимости и модулни координатори) и създаване на базова тестова основа (характеризиращи, snapshot и компонентни тестове) около съществуващия наследен код, преди той да бъде променян.
Приоритизирането трябва да се ръководи от честотата на промените и риска: рефакторирайте екрани, които екипите активно модифицират или области с висок процент на дефекти, вместо стабилен стар код. При рефакториране на претоварени view controller-и отделете бизнес и презентационната логика в специализирани ViewModels/Presenters и въведете адаптери или координатори, базирани на протоколи, за да изолирате стария потребителски интерфейс от новия код.
Накрая, поддържайте скоростта и качеството чрез архитектурно управление: осигурете еталонни примерни имплементации (golden samples), наложете архитектурни граници чрез автоматизирани проверки (linters) в CI или насоки при преглед на код (PR guidelines) и заделяйте постоянен капацитет за технически дълг (например 15–20% от времето или съчетаване на рефакториране със свързани нови задачи), като същевременно следите метрики като време за компилация, честота на сривове и тестово покритие.
19Издавате iOS приложение с голям трафик, което включва миграция на базата данни и промяна в бекенд API; как бихте проектирали плана за внедряване, за да минимизирате въздействието върху потребителите?
Проектирането на план за внедряване на iOS приложение с голям трафик, включващо миграции на базата данни и промени в бекенд API, изисква разделяне на внедряването от активирането на функционалностите поради ограниченията на iOS клиентите (асинхронни потребителски актуализации и невъзможност за принудително връщане на по-стара версия на бинарния файл от страна на клиента).
1. Съвместимост на бекенда: Приложете стратегия за плавен преход чрез паралелна поддръжка (expand-and-contract). Внедрете API v2 успоредно с API v1, така че и старите, и актуализираните версии на клиента да функционират едновременно без чупещи промени.
2. Устойчива миграция на базата данни: Уверете се, че локалните миграции на схемата (например SQLite, Core Data, SwiftData) са идемпотентни, недеструктивни и безопасно обработват прескачане на няколко версии (например обновяване от N-3 към N), без да блокират главната нишка или да предизвикват цикли от сривове при стартиране.
3. Динамично превключване на функционалности: Доставете новите клиентски възможности скрити зад управлявани от сървъра флагове (feature flags) / отдалечена конфигурация. Дръжте флага за функционалността изключен по време на първоначалното разпространение.
4. Поетапно пускане и мониторинг: Разпространете приложението чрез поетапно издаване в App Store (7-дневно поетапно внедряване). Непрекъснато наблюдавайте телеметрията, нивата на сривове, метриките за успеваемост на миграцията на базата данни и честотата на грешки в API. Ако възникнат аномалии, спрете на пауза поетапното внедряване в App Store Connect и изключете флаговете за функционалности, без да се налага аварийно връщане на нова бинарна версия.
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.
20Проектирате синхронизация с приоритет на работа офлайн (offline-first sync) за приложение за бележки, при което редакциите трябва в крайна сметка да се синхронизират между устройствата, но iOS може да отложи или прекрати фоновата работа. Каква архитектура за фоново изпълнение бихте избрали?
Архитектурата за синхронизация с приоритет на офлайн работа третира локалната база данни (като SQLite, Core Data или SwiftData) като непосредствен единствен източник на истина за състоянието на потребителския интерфейс, докато мутациите се добавят към устойчива опашка от изходящи заявки (outbox) на диска, така че неизпратените редакции да оцеляват при прекратяване на процеса. За да се управлява опортюнистичното и недетерминирано фоново изпълнение в iOS, фоновата работа е структурирана на няколко нива:
1. Изчистване на изходящата опашка при преминаване във фон: Стартира се чрез `UIApplication.shared.beginBackgroundTask(expirationHandler:)` за завършване на текущи или бързи чакащи мутации.
2. Планирана периодична синхронизация: Регистрира се с `BGTaskScheduler`, използвайки `BGAppRefreshTask` за лека синхронизация на метаданни/делти и `BGProcessingTask` за по-тежки операции по синхронизация.
3. Синхронизация на големи ресурси: Обработва се извън основния процес чрез фонова `URLSessionConfiguration` с използване на качвания/изтегляния на файлове.
4. Синхронизация, задействана от сървъра: Опортюнистично събуждане чрез тихи push известия (`content-available: 1`).
Тъй като фоновите задачи могат да бъдат прекъснати или пренасрочени във всеки момент, операциите трябва да използват генерирани от клиента ключове за идемпотентност (например UUID), за да позволят безопасни повторни опити без дублиране на данни. Ако се извика `expirationHandler` на дадена задача, текущата работа трябва незабавно да се прекрати, незаписаните операции да се върнат в състояние на изчакване и да се извика `setTaskCompleted(success: false)`. Крайната съгласуваност (eventual consistency) се поддържа с помощта на механизми за разрешаване на конфликти като CRDT (Conflict-free Replicated Data Types), вектори на версиите или подход „последен запис печели“ с маркери за изтриване (tombstones), гарантирайки, че отдалечените промени не презаписват неизпратени локални мутации от опашката.
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()
}
}
}