Подготовка к собеседованию по iOS

Вопросы для собеседования по iOS

20 часто задаваемых вопросов на интервью по iOS. iOS-разработка - востребованное направление, где создают приложения на Swift, SwiftUI и UIKit, решают задачи lifecycle, конкурентности, сети, хранения данных, производительности, тестирования и публикации в App Store. Вопросы охватывают разные уровни, и вы можете потренироваться отвечать на них устно в нашем тренажере.

Начать AI-собеседование по iOSКредитная карта не требуется. Доступна 1 бесплатная сессия.
Практика технических собеседований на английскомРежим, в котором не носители языка могут потренироваться проходить интервью.

Вопросы для начинающих

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
    }
}
Попробовать ответить на вопрос AI-тренеру

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)
Попробовать ответить на вопрос AI-тренеру

3Что означает фоновая работа iOS-приложения и какие основные ограничения накладывает система?

Это выполнение кода, когда приложение не активно на экране или не видно пользователю. При выходе оно переходит из активного переднего плана в фон, а вскоре — в приостановленное состояние: выполнение остановлено, его данные остаются в оперативной памяти (RAM). Для батареи, отзывчивости и ресурсов iOS строго управляет фоновым исполнением. По умолчанию доступен лишь короткий период, определяемый системой; запрос на ограниченное по времени фоновое выполнение (background-task assertion) либо подходящий фоновый API могут дать дополнительное время без гарантированной фиксированной длительности. Система ограничивает приоритет использования процессора и сети и может завершить приостановленные приложения при нехватке памяти.

import UIKit

NotificationCenter.default.addObserver(
    forName: UIApplication.didEnterBackgroundNotification,
    object: nil,
    queue: .main
) { _ in
    print("App entered background. Code execution will pause soon unless extended.")
}
Попробовать ответить на вопрос AI-тренеру

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)")
    }
)
Попробовать ответить на вопрос AI-тренеру

5Что представляет собой async/await в Swift и как использовать эту конструкцию для вызова асинхронного API в приложении под iOS?

`async/await` — это встроенный в Swift синтаксис для написания асинхронного кода в линейном, читаемом и последовательном стиле вместо использования вложенных замыканий и обработчиков завершения (completion handlers). Добавление ключевого слова `async` к функции сообщает компилятору, что ее выполнение может быть приостановлено на время ожидания завершения длительной операции (такой как сетевой запрос или ввод-вывод файлов). Ключевое слово `await` указывает точку приостановки (suspension point): выполнение текущей функции приостанавливается, освобождая базовый поток для выполнения других задач, и возобновляется, когда ожидаемый результат или ошибка готовы. Чтобы вызвать асинхронный API в iOS-приложении, вы вызываете метод с `async`, добавляя перед ним ключевое слово `await` (или `try await`, если функция может выбросить ошибку), находясь внутри асинхронного контекста, например внутри другой функции `async` или блока `Task`.

func fetchUserData(from url: URL) async throws -> User {
    let (data, response) = try await URLSession.shared.data(from: url)
    
    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
        throw URLError(.badServerResponse)
    }
    
    return try JSONDecoder().decode(User.self, from: data)
}
Попробовать ответить на вопрос AI-тренеру

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"
Попробовать ответить на вопрос AI-тренеру

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
Попробовать ответить на вопрос AI-тренеру

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()
Попробовать ответить на вопрос AI-тренеру

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.
Попробовать ответить на вопрос AI-тренеру

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, которая отображает оповещение или активирует приложение в соответствии с заданной конфигурацией.

[Provider Server] ---> (HTTP/2 Request + Device Token + Payload) ---> [APNs]
                                                                           |
                                                               (Persistent APNs Connection)
                                                                           v
                                                                    [iOS Device / App]
Попробовать ответить на вопрос AI-тренеру

Вопросы для продолжающих

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() }
}
Попробовать ответить на вопрос AI-тренеру

12Как устранить ошибку загрузки iOS-архива из-за подписи или профиля подготовки приложения (provisioning)?

Проверяйте сертификаты распространения, профили подготовки приложения и заявленные права (entitlements). 1. Логи: изучите журналы распространения Xcode Organizer, подробные ошибки валидации или журналы непрерывной интеграции (CI) инструмента загрузки в App Store — не notarytool, предназначенного для нотарификации macOS, — для точной причины. 2. Сертификат и удостоверение подписи: действующий Apple Distribution должен присутствовать в Keychain с соответствующим закрытым ключом, не быть просроченным или отозванным и иметь промежуточный Apple WWDR. 3. Профиль: при экспорте нужен профиль распространения через App Store с точным Bundle Identifier. Push Notifications и Associated Domains требуют подходящего явного App ID вместо несовместимого идентификатора с подстановочным знаком. 4. Entitlements: сравните файл `.entitlements` целевой сборки (target) с возможностями App ID в Developer Portal. Права, не зарегистрированные в профиле портала, вызывают отказ. 5. Настройка подписи: при автоматической проверьте Team и учётные данные Apple ID в Xcode Settings. При ручной или 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
Попробовать ответить на вопрос AI-тренеру

13Как выбрать BGAppRefreshTask, BGProcessingTask или фоновую URLSession для фоновой синхронизации в приложении, работающем у пользователей?

Выбор зависит от длительности, требований питания и сети и характера передаваемых данных. 1. BGAppRefreshTask — короткие лёгкие обновления, например новости, лента и панели, примерно на 15–30 секунд. Система планирует их по привычкам пользователя, чтобы обновить содержимое перед типичным открытием. 2. BGProcessingTask — длительные несрочные тяжёлые операции: индексация, очистка и миграция базы, обучение моделей машинного обучения (ML), большая синхронизация. Может работать минуты и требовать зарядки через `requiresExternalPower = true` и подключения через `requiresNetworkConnectivity = true`, что не означает только Wi-Fi. Время запуска выбирает система; ночь или фиксированная длительность не гарантированы. 3. Фоновая URLSession — `URLSessionConfiguration.background` — для больших файлов, фото, видео и ресурсов, которые должны передаваться вне процесса при приостановке или завершении приложения системой. Передача делегируется `nsurlsessiond`, не удерживая код приложения активным.

import BackgroundTasks

func scheduleDatabaseMaintenance() {
    let request = BGProcessingTaskRequest(identifier: "com.app.db_cleanup")
    request.requiresNetworkConnectivity = false
    request.requiresExternalPower = true // Run overnight on charger
    request.earliestBeginDate = Date(timeIntervalSinceNow: 6 * 3600)
    
    try? BGTaskScheduler.shared.submit(request)
}

func scheduleTimelineRefresh() {
    let request = BGAppRefreshTaskRequest(identifier: "com.app.feed_refresh")
    request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
    
    try? BGTaskScheduler.shared.submit(request)
}
Попробовать ответить на вопрос AI-тренеру

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)` перед обновлением состояния или присвоением значений свойствам пользовательского интерфейса.

import Combine
import Foundation

class SearchViewModel: ObservableObject {
    @Published var query: String = ""
    @Published var searchResults: [String] = []
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        $query
            .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
            .removeDuplicates()
            .map { query -> AnyPublisher<[String], Never> in
                guard !query.trimmingCharacters(in: .whitespaces).isEmpty else {
                    return Just([]).eraseToAnyPublisher()
                }
                return self.performSearch(query)
                    .catch { _ in Just([]) }
                    .eraseToAnyPublisher()
            }
            .switchToLatest()
            .receive(on: DispatchQueue.main)
            .assign(to: &$searchResults)
    }
    
    private func performSearch(_ term: String) -> AnyPublisher<[String], Error> {
        let url = URL(string: "https://api.example.com/search?q=\(term)")!
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: [String].self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
}
Попробовать ответить на вопрос AI-тренеру

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)
            }
        }
    }
}
Попробовать ответить на вопрос AI-тренеру

16Как выбирать UserDefaults, Keychain, файлы, SQLite и Core Data для разных данных приложения?

Выбор зависит от чувствительности, размера, связей, запросов и резервного копирования. 1. Keychain — токены, пароли, ключи шифрования. Поддерживает шифрование и контроль доступа; биометрические шаблоны управляются системой и недоступны приложениям. Keychain не скрывает данные от кода с разрешённым доступом, а сохранение после переустановки нельзя считать контрактом хранения. 2. UserDefaults — небольшие нечувствительные настройки вроде hasCompletedOnboarding или темы; не большие наборы, медиа и секретные токены. 3. Documents/Caches/Application Support — отдельные файлы и крупные двоичные данные: картинки, аудио, PDF. Documents предназначен пользователю и копируется в iCloud; Caches — удаляемое повторно загружаемое содержимое; Application Support — постоянные внутренние файлы. 4. SQLite — структурированные таблицы с быстрыми индексными запросами, массовыми операциями или межплатформенной общей базой без накладных расходов графа объектов. 5. Core Data/SwiftData — сложные графы со связями, отслеживанием изменений, отложенной загрузкой данных объектов (faulting), отменой действий и прямой интеграцией с NSFetchedResultsController UIKit или @Query SwiftUI.

- Auth tokens / API keys          -> Keychain (Hardware-backed encrypted store)
- App settings / UI flags         -> UserDefaults (Lightweight key-value, non-sensitive)
- Downloaded videos / PDFs        -> File System (Documents / Caches directory)
- 50k items with complex queries   -> SQLite / Core Data / SwiftData (Indexed, relationship-aware)
Попробовать ответить на вопрос AI-тренеру

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 */ }
}
Попробовать ответить на вопрос AI-тренеру

Вопросы для опытных

18Вы приходите в зрелый iOS-проект с сотнями экранов, множеством Massive View Controller и медленными циклами релизов. Как вы спланируете стратегию поэтапного улучшения архитектуры без остановки поставки новой функциональности?

Эффективная стратегия поэтапной миграции исключает полное переписывание системы с нуля и совмещает рефакторинг непосредственно с текущей разработкой фич (по паттерну Strangler Fig). Сначала согласуется целевая архитектурная модель (например, MVVM или VIPER с внедрением зависимостей и модульными координаторами) и формируется базовое тестовое покрытие (характеризационные, снапшот- и модульные тесты) вокруг легаси-кода перед его изменением. Приоритизация должна опираться на частоту изменений и риски: рефакторингу подвергаются экраны, которые команды активно модифицируют, или участки с высоким уровнем багов, а не стабильный легаси-код. При рефакторинге Massive View Controller бизнес- и презентационная логика выносятся в выделенные ViewModels/Presenters, а для изоляции старого UI от нового кода внедряются адаптеры на основе протоколов или координаторы. Наконец, темп и качество поддерживаются через архитектурный контроль (governance): создаются эталонные примеры реализации (golden sample), соблюдение границ архитектуры проверяется линтерами в CI или правилами код-ревью, а также выделяется постоянный ресурс на работу с техническим долгом (например, 15–20% емкости спринта или совмещение рефакторинга с релевантными продуктовыми задачами) с отслеживанием метрик: времени сборки, частоты сбоев и покрытия тестами.

protocol ProfileDisplaying: AnyObject {
    func updateProfile(name: String, avatarUrl: URL?)
}

extension LegacyProfileViewController: ProfileDisplaying {
    func updateProfile(name: String, avatarUrl: URL?) {
        self.nameLabel.text = name
    }
}

final class ProfilePresenter {
    private weak var view: ProfileDisplaying?
    private let userUseCase: FetchUserUseCaseProtocol
    
    init(view: ProfileDisplaying, userUseCase: FetchUserUseCaseProtocol) {
        self.view = view
        self.userUseCase = userUseCase
    }
    
    func onViewLoaded() async {
        guard let user = try? await userUseCase.execute() else { return }
        await MainActor.run {
            view?.updateProfile(name: user.name, avatarUrl: user.avatarURL)
        }
    }
}
Попробовать ответить на вопрос AI-тренеру

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.
Попробовать ответить на вопрос AI-тренеру

20Вы проектируете синхронизацию с приоритетом автономной работы заметок: правки должны со временем сходиться между устройствами, но 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 немедленно отменяйте текущую работу, возвращайте незафиксированные операции в состояние ожидания (pending) и вызывайте setTaskCompleted(success: false). Также синхронизируйте на переднем плане и после локальных правок через одного согласованного владельца процесса. Фоновый запуск не гарантирован, поэтому фиксированный срок схождения обещать нельзя. Для согласованности используйте реплицируемые типы данных без конфликтов (CRDT), векторы версий с политикой слияния либо правило приоритета последней записи с метками удаления (tombstone), не позволяя удалённым дельтам затирать незафиксированные локальные изменения 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()
        }
    }
}
Попробовать ответить на вопрос AI-тренеру