20 часто задаваемых вопросов на интервью по iOS. iOS-разработка - востребованное направление, где создают приложения на Swift, SwiftUI и UIKit, решают задачи lifecycle, конкурентности, сети, хранения данных, производительности, тестирования и публикации в App Store. Вопросы охватывают разные уровни, и вы можете потренироваться отвечать на них устно в нашем тренажере.
1Каковы основные зоны ответственности UIViewController в типичной архитектуре MVC (Model-View-Controller) в iOS?
В классическом 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, что приложение авторизовано для запуска в рамках заданных правил распространения. Provisioning profile объединяет несколько ключевых компонентов: App ID (Bundle Identifier), разрешенные сертификаты подписи, подтверждающие разработчика, права доступа и возможности приложения (entitlements и capabilities, такие как Push-уведомления, Sign in with Apple или iCloud), а также — для профилей вне App Store, таких как Development или Ad-Hoc — список идентификаторов разрешенных устройств (UDID, Unique Device Identifier). В релизных профилях для 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-приложения и какие базовые ограничения накладывает система на такую работу?
Выполнение задач в фоновом режиме в iOS означает исполнение кода, когда приложение не активно на экране и не отображается пользователю. Когда пользователь сворачивает или закрывает приложение, оно переходит из активного состояния переднего плана в фоновое, а вскоре после этого — в приостановленное состояние (suspended), в котором выполнение кода останавливается, но данные остаются в оперативной памяти. Для экономии заряда аккумулятора, поддержания отзывчивости системы и сохранения ресурсов устройства iOS жестко контролирует фоновую активность. Система ограничивает время выполнения кода после перехода в фон (обычно коротким интервалом около 30 секунд, если не используются специальные фоновые режимы или запланированные задачи), снижает приоритет использования процессора и сети, а также может принудительно завершить приостановленные приложения при нехватке памяти.
4Что такое Publisher во фреймворке Combine и чем он отличается от Subscriber в типичном потоке данных iOS?
Во фреймворке Combine `Publisher` (издатель) отправляет поток значений во времени и может завершиться событием завершения (успешным выполнением или ошибкой). Он объявляет два ассоциированных типа: `Output` (тип генерируемых данных) и `Failure` (тип возможной ошибки). `Subscriber` (подписчик) принимает значения и события жизненного цикла от издателя. В типичном потоке данных iOS издатели выступают источником асинхронных событий (таких как сетевые ответы, уведомления или действия пользователя), а подписчики — потребителями, которые реагируют на полученные значения, обрабатывают ошибки и завершение (например, обновляют интерфейс или сохраняют данные в базу). Подписчик подключается к издателю, получает объект подписки (`Subscription`) и запрашивает необходимое количество элементов (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 синтаксис для написания асинхронного кода в линейном, читаемом и последовательном стиле вместо использования вложенных замыканий и обработчиков завершения (completion handlers). Добавление ключевого слова `async` к функции сообщает компилятору, что ее выполнение может быть приостановлено на время ожидания завершения длительной операции (такой как сетевой запрос или ввод-вывод файлов). Ключевое слово `await` указывает точку приостановки (suspension point): выполнение текущей функции приостанавливается, освобождая базовый поток для выполнения других задач, и возобновляется, когда ожидаемый результат или ошибка готовы. Чтобы вызвать асинхронный API в iOS-приложении, вы вызываете метод с `async`, добавляя перед ним ключевое слово `await` (или `try await`, если функция может выбросить ошибку), находясь внутри асинхронного контекста, например внутри другой функции `async` или блока `Task`.
6В каких случаях следует использовать UserDefaults в приложении для iOS, и какие типы данных не стоит там хранить?
`UserDefaults` предназначен для сохранения небольших легковесных пользовательских настроек, конфигураций и флагов состояния между запусками приложения (например, выбранной темы оформления, уровня громкости или логического флага `hasSeenOnboarding`). Он поддерживает стандартные типы списка свойств (property list), такие как `String`, `Int`, `Double`, `Bool`, `Date`, `Data`, `Array` и `Dictionary`. Не следует хранить в `UserDefaults`: 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, предназначенный для отслеживания и управления временем жизни экземпляров классов (ссылочных типов). Каждый раз, когда создается новый экземпляр класса и присваивается сильной ссылке, ARC отслеживает внутренний счетчик ссылок для этого экземпляра. Пока существует хотя бы одна сильная ссылка, ARC удерживает объект в памяти. Когда все сильные ссылки удаляются и счетчик ссылок становится равным нулю, ARC немедленно освобождает память, автоматически вызывая метод экземпляра `deinit` непосредственно перед деаллокацией. В отличие от сборщика мусора во время выполнения в таких средах, как Java или .NET, ARC не выполняет периодических циклов очистки памяти; компилятор Swift внедряет соответствующие вызовы retain и release в бинарный код еще на этапе компиляции.
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. Он позволяет разработчикам отслеживать поведение во время выполнения, загрузку процессора, выделение памяти, утечки памяти и производительность отрисовки пользовательского интерфейса. Чтобы исследовать простую проблему с производительностью (например, подергивания интерфейса или высокую нагрузку на процессор), вы запускаете Instruments из Xcode (через Product -> Profile), выбираете подходящий шаблон (например, Time Profiler для поиска узких мест в нагрузке на процессор или Allocations/Leaks для проблем с памятью) и записываете трассировку при воспроизведении проблемного сценария на устройстве. После записи вы анализируете дерево вызовов и самые ресурсоемкие стеки вызовов, чтобы точно определить методы, вызывающие задержки или чрезмерное потребление ресурсов, что позволяет измерить и локализовать узкое место до внесения изменений в код.
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-уведомлений в приложение для iOS?
APNs (Apple Push Notification service) — это облачный промежуточный сервис Apple, который обеспечивает безопасную маршрутизацию push-уведомлений от серверов вашего бэкенда (серверов-провайдеров) на устройства Apple. Поскольку мобильные устройства не могут поддерживать непрерывные открытые сокет-соединения с бэкендом каждого отдельного приложения без быстрого разряда аккумулятора и избыточного расхода сетевого трафика, Apple поддерживает одно постоянное энергоэффективное соединение между каждым устройством и APNs. Когда приложение регистрируется для получения удаленных уведомлений, APNs генерирует уникальный непрозрачный токен устройства (device token), идентифицирующий конкретную установку приложения на конкретном устройстве. Приложение передает этот токен на сервер провайдера. Когда провайдеру требуется отправить уведомление пользователю, он формирует полезную нагрузку и отправляет ее вместе с токеном устройства в APNs по протоколу HTTP/2. Затем APNs находит активное соединение с данным устройством и доставляет полезную нагрузку push-уведомления непосредственно в iOS, которая отображает оповещение или активирует приложение в соответствии с заданной конфигурацией.
11Как выполнить рефакторинг перегруженного UIViewController, который отвечает за обновление UI (User Interface — пользовательский интерфейс), валидацию, работу с сетью и навигацию?
Рефакторинг перегруженного контроллера представлений (Massive View Controller) следует выполнять поэтапно, чтобы минимизировать риски и распределить зоны ответственности по отдельным слоям:
1. **Вынесение сетевого слоя и работы с данными:** Перенесите вызовы API, сохранение данных и парсинг JSON из контроллера в специализированные классы сервисов или репозиториев, скрытые за абстракциями протоколов.
2. **Вынесение состояния представления и валидации:** Внедрите ViewModel (или Presenter). Перенесите валидацию ввода, форматирование строк и управление состоянием интерфейса во ViewModel, что позволит изолированно покрыть эту логику модульными тестами.
3. **Вынесение навигации:** Примените паттерн Coordinator (или Router / Flow Controller), чтобы убрать логику переходов (push/present) и управление сценариями навигации из контроллера.
4. **Облегчение контроллера представлений:** Оставьте в UIViewController только верстку интерфейса, настройку дочерних представлений, методы жизненного цикла и связывание элементов управления с 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, вызванную проблемами с подписью кода (code signing) или профилями обеспечения (provisioning profiles)?
Диагностика и устранение ошибок загрузки архива iOS, связанных с подписью кода или профилями обеспечения, включают проверку трех основных компонентов: сертификатов распространения, профилей обеспечения и прав доступа (entitlements) таргета.
1. **Анализ логов диагностики:** Изучите логи распространения в Xcode Organizer, подробные ошибки валидации при загрузке или консольные логи CI (Continuous Integration — непрерывная интеграция) при использовании утилит `altool` / `notarytool`, чтобы выяснить точную причину отклонения.
2. **Проверка сертификата и идентификатора:** Убедитесь, что действующий сертификат Apple Distribution установлен в Связке ключей (Keychain) вместе с соответствующим закрытым ключом, не истек, не был отозван и содержит промежуточный сертификат Apple WWDR (Worldwide Developer Relations).
3. **Проверка профиля обеспечения:** Убедитесь, что для экспорта используется профиль типа App Store Distribution, строго соответствующий Bundle Identifier приложения. Если задействованы возможности (capabilities), такие как Push Notifications или Associated Domains, проверьте, что настроен явный App ID, а не несовместимый wildcard-идентификатор.
4. **Согласование Entitlements:** Проверьте отсутствие расхождений между файлом `.entitlements` таргета и возможностями, включенными для данного App ID в Apple Developer Portal. Если локальные entitlements запрашивают права, не зарегистрированные в профиле на портале, загрузка завершится ошибкой.
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 (Uniform Resource Locator Session) для реализации фоновой синхронизации в продакшене?
Выбор подходящего API для фоновой работы зависит от длительности задачи, требований к питанию и сети, а также от характера передаваемых данных: 1. **BGAppRefreshTask**: лучше всего подходит для коротких легковесных обновлений контента (например, обновление новостной ленты, таймлайна в соцсетях или дашборда пользователя), которые занимают примерно 15–30 секунд. Система планирует такие задачи на основе паттернов использования приложения пользователем, чтобы свежий контент был готов непосредственно перед тем, как пользователь обычно открывает приложение. 2. **BGProcessingTask**: предназначен для длительных, несрочных и ресурсоемких операций, таких как индексация данных, очистка/миграция базы данных, обучение моделей машинного обучения или синхронизация больших объемов данных. Задача может выполняться несколько минут и требовать подключения устройства к зарядному устройству (`requiresExternalPower = true`) и сети (`requiresNetworkConnectivity = true`), обычно запускается ночью. 3. **Фоновый URLSession (`URLSessionConfiguration.background`)**: правильный выбор для передачи больших файлов (загрузка фото/видео или скачивание объемных пакетов ресурсов), которая должна продолжаться вне процесса приложения, даже если приложение приостановлено или завершено операционной системой. Он делегирует передачу данных системному демону `nsurlsessiond`, не удерживая код приложения активным в оперативной памяти.
14Как построить конвейер Combine для текстового поля поиска с устранением дребезга (debounce), фильтрацией дубликатов, выполнением сетевого запроса и безопасным обновлением пользовательского интерфейса (UI, User Interface)?
Для построения надежного конвейера поиска в 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` или шагов обработки. Изменения состояния UI должны выполняться строго на главном акторе — либо путем аннотирования view model атрибутом `@MainActor`, либо через `await MainActor.run`. Чтобы избежать применения устаревших данных к UI, следует отменять предыдущие задачи, не использовать `Task.detached` и глобальные задачи для привязанных к экрану операций, а также при необходимости сверять ID запроса или текущего элемента перед обновлением состояния.
@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**: для структурированных табличных данных, требующих быстрых индексированных запросов, массовых операций или кроссплатформенного доступа к базе данных без накладных расходов объектного графа. 5. **Core Data / SwiftData**: для сложных графов объектов со связями, отслеживанием изменений, отложенной загрузкой (faulting), управлением отменой операций (undo) и прямой интеграцией с UIKit (`NSFetchedResultsController`) или SwiftUI (`@Query`).
17На продакшн-экране в UIKit с обратными вызовами из view model, как вы определяете, каким образом захватывать self в замыканиях: как weak, unowned или strong?
В архитектуре 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` корректно освобождаться из памяти и предотвращает утечки памяти.
2. `[unowned self]`: Предполагает, что `self` гарантированно не равен nil на момент выполнения замыкания. В коде UI-экранов его следует использовать с крайней осторожностью. Если асинхронная задача или обратный вызов завершится после того, как view controller был закрыт и деаллоцирован, обращение к `unowned self` приведет к фатальному сбою (runtime crash). По этой причине в продакшн-коде UIKit для UI-колбэков почти всегда предпочитают `[weak self]`.
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-проект с сотнями экранов, множеством Massive View Controller и медленными циклами релизов. Как вы спланируете стратегию поэтапного улучшения архитектуры без остановки поставки новой функциональности?
Эффективная стратегия поэтапной миграции исключает полное переписывание системы с нуля и совмещает рефакторинг непосредственно с текущей разработкой фич (по паттерну Strangler Fig). Сначала согласуется целевая архитектурная модель (например, MVVM или VIPER с внедрением зависимостей и модульными координаторами) и формируется базовое тестовое покрытие (характеризационные, снапшот- и модульные тесты) вокруг легаси-кода перед его изменением.
Приоритизация должна опираться на частоту изменений и риски: рефакторингу подвергаются экраны, которые команды активно модифицируют, или участки с высоким уровнем багов, а не стабильный легаси-код. При рефакторинге Massive View Controller бизнес- и презентационная логика выносятся в выделенные ViewModels/Presenters, а для изоляции старого UI от нового кода внедряются адаптеры на основе протоколов или координаторы.
Наконец, темп и качество поддерживаются через архитектурный контроль (governance): создаются эталонные примеры реализации (golden sample), соблюдение границ архитектуры проверяется линтерами в CI или правилами код-ревью, а также выделяется постоянный ресурс на работу с техническим долгом (например, 15–20% емкости спринта или совмещение рефакторинга с релевантными продуктовыми задачами) с отслеживанием метрик: времени сборки, частоты сбоев и покрытия тестами.
19Вы выпускаете высоконагруженное приложение для iOS с миграцией базы данных и изменением API (Application Programming Interface) бэкенда. Как вы спроектируете план поэтапного развертывания, чтобы минимизировать влияние на пользователей?
Проектирование плана развертывания высоконагруженного 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 Phased Release (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 для приложения заметок, где правки должны со временем синхронизироваться между устройствами, однако iOS может отложить или отменить фоновую работу. Какую архитектуру фонового выполнения вы выберете?
Архитектура синхронизации по принципу offline-first рассматривает локальную базу данных (например, SQLite, Core Data или SwiftData) как непосредственный единый источник истины для состояния пользовательского интерфейса, а изменения добавляются в сохраняемую на диске очередь исходящих операций (outbox queue), чтобы неотправленные правки сохранялись при завершении процесса. Для управления недетерминированным фоновым выполнением в iOS фоновая работа разделяется на несколько уровней: 1. Сброс очереди outbox при переходе в фоновый режим: инициируется через `UIApplication.shared.beginBackgroundTask(expirationHandler:)` для завершения выполняющихся или быстрых ожидающих изменений. 2. Плановая периодическая синхронизация: регистрируется в `BGTaskScheduler` с использованием `BGAppRefreshTask` для легковесной синхронизации метаданных/дельт и `BGProcessingTask` для более ресурсоемких операций синхронизации. 3. Синхронизация больших файлов: выполняется вне основного процесса через фоновую конфигурацию `URLSessionConfiguration` с загрузкой и скачиванием файлов. 4. Синхронизация по инициативе сервера: пробуждение приложения через «тихие» push-уведомления (`content-available: 1`). Поскольку фоновые задачи могут быть прерваны или перенесены в любой момент, операции должны использовать сгенерированные клиентом ключи идемпотентности (например, UUID), чтобы обеспечить безопасные повторные попытки без дублирования данных. При вызове `expirationHandler` выполняемая работа должна быть немедленно отменена, незафиксированные операции возвращены в статус ожидающих, и вызван метод `setTaskCompleted(success: false)`. Согласованность в конечном счете обеспечивается механизмами разрешения конфликтов, такими как CRDT, векторы версий или last-write-wins с фиктивными записями об удалении (deletion tombstones), гарантируя, что удаленные изменения не перезапишут незафиксированные локальные правки из очереди outbox.
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()
}
}
}