Привязывайте платформенное решение к lifecycle, состоянию UI, обратной связи, доступности и цене поддержки для команды.
Вопросы и ответы
9 подробных ответов
Проверка по первоисточникам опубликована для 1 из 9 ответов. Дата и область проверки указаны у каждого проверенного ответа.
01Какие конфигурации есть у URLSession, какие бывают таски и как отменить запрос?
junior
Короткий ответ: .default использует постоянные хранилища кэша, cookies и credentials; .ephemeral не сохраняет эти данные сессии на диск. .background(withIdentifier:) передаёт загрузки системе и может продолжать их при приостановке или системном завершении приложения. Принудительное закрытие пользователем — исключение: фоновые передачи отменяются, автоматический перезапуск для них не происходит до следующего запуска пользователем.
Подробно:
| Конфигурация | Поведение |
|---|---|
.default |
обычные запросы с настроенными постоянными хранилищами |
.ephemeral |
кэш, cookies и credentials сессии не сохраняются постоянно; это не запрет на любую запись файлов |
.background(withIdentifier:) |
системные upload/download-задачи; события через delegate |
- Типы задач —
dataTaskполучает данные в память,downloadTaskпишет временный файл,uploadTaskотправляет тело. Для загрузок в фоне, переживающих выход из приложения, тело upload должно быть файлом. - Отмена —
task.cancel()запрашивает отмену; завершение обычно приходит сURLError.cancelled. У async-методовURLSessionотмена вызывающейTaskраспространяется на запрос. - Возобновление — после системного завершения воссоздайте background-сессию с тем же идентификатором и обработайте delegate-события; учитывайте системные условия запуска.
- Таймауты —
timeoutIntervalForRequestограничивает ожидание очередных данных,timeoutIntervalForResource— длительность ресурса целиком; задавайте значения под сценарий.
⚠️ Частая ошибка: обещать, что background-запрос переживёт любое закрытие, или что .ephemeral не оставит файлов. Пользовательский force-quit меняет поведение, а download-задача и код приложения могут писать на диск и в ephemeral-сессии.
02Что такое SSL pinning, от чего он защищает и как его внедрить?
middle
Короткий ответ: Пиннинг — проверка, что сервер предъявил именно наш сертификат или ключ, а не любой, которому доверяет система. Защищает от MITM с подставным или скомпрометированным CA. Реализация — URLSessionDelegate с проверкой challenge или ATS-пиннинг в Info.plist (iOS 14+). Пинить лучше публичный ключ (SPKI), а не сертификат.
Подробно:
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge) async
-> (URLSession.AuthChallengeDisposition, URLCredential?) {
guard let trust = challenge.protectionSpace.serverTrust,
SecTrustEvaluateWithError(trust, nil),
let key = publicKeyHash(trust) else {
return (.cancelAuthenticationChallenge, nil) // цепочка не прошла — рвём соединение
}
return pinnedHashes.contains(key)
? (.useCredential, URLCredential(trust: trust))
: (.cancelAuthenticationChallenge, nil) // ключ не наш — MITM?
}
- Сертификат vs ключ — пин сертификата ломается при каждом продлении; пин SPKI-ключа переживает ротацию сертификата, пока пара ключей та же.
- ATS-пиннинг —
NSPinnedDomainsв Info.plist: декларативно, без кода (iOS 14+). - Финтех-стандарт — банковские приложения пинят обязательно (Т-Банк спросит) и всегда держат резервный ключ на случай компрометации.
⚠️ Частая ошибка: запинить leaf-сертификат без плана ротации: сертификат продлили — и все установленные приложения перестали ходить в сеть. Пинь ключ и держи backup-пин.
03Codable: как декодировать ответ API со snake_case, вложенной структурой и опциональным легаси-полем?
junior
Короткий ответ: Snake_case — keyDecodingStrategy = .convertFromSnakeCase или явные CodingKeys; вложенный объект без промежуточной модели — nestedContainer(keyedBy:) в кастомном init(from:); легаси-поле — decodeIfPresent с дефолтом.
Подробно:
// {"user_name": "Алия", "stats": {"total_orders": 5}, "legacy_flag": null}
struct User: Decodable {
let userName: String
let totalOrders: Int
let legacyFlag: Bool
enum CodingKeys: String, CodingKey {
case userName = "user_name", stats, legacyFlag = "legacy_flag"
}
enum StatsKeys: String, CodingKey { case totalOrders = "total_orders" }
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
userName = try c.decode(String.self, forKey: .userName)
let stats = try c.nestedContainer(keyedBy: StatsKeys.self, forKey: .stats)
totalOrders = try stats.decode(Int.self, forKey: .totalOrders)
legacyFlag = try c.decodeIfPresent(Bool.self, forKey: .legacyFlag) ?? false
}
}
- Стратегия vs CodingKeys —
.convertFromSnakeCaseкороче, но конвертирует каждый ключ в рантайме и прячет соответствие имён; явные ключи быстрее и честнее. - Даты —
dateDecodingStrategy:.iso8601или кастомный форматтер, не парсить руками. decodeIfPresent— покрывает и отсутствующий ключ, иnull.
⚠️ Частая ошибка: один битый элемент валит декодирование всего массива. Для лент заводят lossy-обёртку: декодировать элементы поштучно и молча пропускать сломанные.
04Как устроен стек Core Data: viewContext, фоновые контексты и слияние изменений?
middle
Короткий ответ: NSPersistentContainer поднимает весь стек: модель, координатор, хранилище и viewContext — контекст главного потока для чтения и UI. Запись идёт в фоновых контекстах (performBackgroundTask), а изменения доезжают до UI через automaticallyMergesChangesFromParent и merge policy.
Подробно:
NSPersistentContainer
│
┌─────────────┴─────────────┐
│ viewContext (main) │ ← чтение, FRC, UI
│ background contexts │ ← запись, импорт
└─────────────┬─────────────┘
NSPersistentStoreCoordinator
│
SQLite-хранилище
- viewContext — только главный поток: чтение,
NSFetchedResultsController, биндинг в UI. - Фоновая запись —
container.performBackgroundTask { ctx in … try ctx.save() }: тяжёлый импорт не трогает главный поток. - Слияние —
viewContext.automaticallyMergesChangesFromParent = trueплюсNSMergeByPropertyObjectTrumpMergePolicy: сохранённое в фоне само появляется в UI. - Границы потоков — контексты и managed-объекты не потокобезопасны: между контекстами передают
NSManagedObjectID, а не сам объект.
⚠️ Частая ошибка: писать большие батчи в viewContext — UI замирает; или трогать managed-объект из чужого потока — редкий и злой краш. Правило: контекст живёт в своём потоке, наружу выходит только objectID.
05Миграции Core Data: что умеет lightweight-миграция и когда нужна тяжёлая?
middle
Короткий ответ: Lightweight-миграцию Core Data выводит сам: добавление и удаление атрибутов, переименования (через renaming ID), смена optionality. Ломается на всём, что требует семантики — разделение и слияние сущностей, преобразование данных: тогда mapping model или поэтапная (staged) миграция.
Подробно:
| Изменение | Миграция |
|---|---|
| Новый атрибут / сущность | lightweight |
| Переименование | lightweight + renaming ID в модели |
| Optional → required (с дефолтом) | lightweight |
| Разделить сущность на две | mapping model (heavyweight) |
| Преобразовать данные (строка → enum) | mapping model / миграция кодом |
| Скачок через несколько версий | цепочка staged-миграций |
- Включение —
shouldMigrateStoreAutomatically+shouldInferMappingModelAutomatically; уNSPersistentContainerвключены по умолчанию. - Heavyweight —
NSMappingModel+ entity migration policy; ест много памяти, поэтому большие базы мигрируют поэтапно, версия за версией. - iOS 17+ —
NSStagedMigrationManagerформализует поэтапную миграцию кодом вместо зоопарка mapping-моделей.
⚠️ Частая ошибка: тестировать миграцию только на свежей установке. Держи в тестах реальные старые .sqlite-файлы: упавшая миграция — это краш на старте у всех, кто обновился.
06Что такое SwiftData и стали бы вы брать его в прод сегодня?
concept
Короткий ответ: SwiftData — макро-обёртка над концепциями хранилища Core Data: @Model вместо .xcdatamodeld, @Query для SwiftUI, типизированные предикаты через #Predicate. Честный ответ: greenfield на iOS 17+ со SwiftUI — да; зрелая схема, сложные миграции и тонкая настройка — Core Data.
Подробно:
- Что даёт — модель прямо в коде через
@Model,@Queryсам обновляет вью, типобезопасные предикаты вместо строковыхNSPredicate. - Что под капотом — то же SQLite-хранилище и те же концепции (контекст →
ModelContext); это новый слой API, а не новый движок. - Ограничения — минимум iOS 17; продвинутые сценарии (сложные merge policy, derived attributes, undo, тонкий тюнинг fetch) пока слабее Core Data; в CIS-проде встречается редко — команды консервативны.
| Проект | Выбор |
|---|---|
| Greenfield, iOS 17+, SwiftUI | SwiftData |
| Зрелая схема, годы миграций | Core Data |
| Нужен контроль: batch, tuning | Core Data |
⚠️ Частая ошибка: подать SwiftData как «замену Core Data». Это надстройка над той же базой; полнота API и зрелость у Core Data пока выше — собеседующий ждёт именно этой оговорки, а не рекламного энтузиазма.
07Где хранить токены: Keychain или UserDefaults — и почему?
junior
Короткий ответ: Токены, пароли, ключи — только Keychain: он шифруется системой, переживает переустановку и умеет классы доступа вплоть до биометрии. UserDefaults — нешифрованный plist: попадает в бэкапы и тривиально читается на джейлбрейкнутом устройстве.
Подробно:
| UserDefaults | Keychain | |
|---|---|---|
| Формат | plist в песочнице, открытым текстом | зашифрованное системное хранилище |
| Переустановка | стирается | значение может пережить |
| Бэкап | уходит как есть | зависит от класса доступа |
| Доступность | всегда | afterFirstUnlock, whenUnlockedThisDeviceOnly… |
| Биометрия | ✗ | ✓ (SecAccessControl) |
| Назначение | настройки, флаги | секреты |
- Классы доступа —
kSecAttrAccessibleAfterFirstUnlockдля фоновых запросов после перезагрузки; варианты…ThisDeviceOnlyне уезжают в бэкап и на другие устройства. - Биометрия —
SecAccessControlс.biometryCurrentSet: элемент отдаётся только после Face ID/Touch ID. - API — сырой
SecItemAdd/SecItemCopyMatchingмногословен; в проде берут тонкую обёртку или проверенную библиотеку.
⚠️ Частая ошибка: «UserDefaults защищён песочницей, значит токену там нормально». Песочница — не шифрование: бэкап, джейлбрейк или доступ к файловой системе отдают plist как есть.
08Как спроектировать кэш картинок для ленты: память, диск и переиспользование ячеек?
middle
Короткий ответ: Два слоя: NSCache для декодированных картинок (сам чистится под memory pressure) плюс диск для сырых данных. Ключевое — даунсэмплить до размера показа до кэширования, дедуплицировать запросы и отменять загрузку при reuse ячейки. Так устроены Kingfisher и Nuke.
Подробно:
запрос картинки
│ 1. память (NSCache) ── hit → отдать мгновенно
│ 2. диск ── hit → decode + downsample → в память
│ 3. сеть ── download → downsample → диск + память
└─ in-flight: один URL = одна загрузка (дедупликация)
- NSCache — авто-эвикция под давлением памяти,
totalCostLimitв байтах; хранить уже декодированныйUIImage, чтобы не декодировать на скролле. - Даунсэмплинг до кэша — декодированный битмап весит w×h×4 байта: фото 4000×3000 ≈ 48 МБ, а ячейке нужно 200×200.
CGImageSourceCreateThumbnailAtIndexдекодирует сразу в нужный размер. - Reuse ячеек — в
prepareForReuseотменять таску загрузки, иначе быстрый скролл рисует чужие картинки в ячейках. - Дедупликация — десять ячеек с одним URL = одна сетевая загрузка и веер подписчиков.
⚠️ Частая ошибка: кэшировать полноразмерные декодированные картинки — память вылетает на первом же быстром скролле ленты.
09Приложение должно работать без сети и синхронизироваться потом. Как построить offline-first?
senior
Короткий ответ: Локальная база — источник истины: UI всегда читает и пишет локально, а персистентная очередь мутаций доставляет изменения на сервер с ретраями. Ключевые решения: политика конфликтов, «сеть как подсказка, а не как условие» и очередь, которая не теряет операции ни при каком краше.
Подробно:
UI ──► локальная база (истина) ──► очередь мутаций (персистентная)
▲ │ retry + backoff
│ merge ▼
└──────────── sync engine ◄──────────── сервер
- Источник истины — Core Data/SQLite; UI никогда не ждёт сеть, изменения применяются локально сразу (optimistic).
- Очередь мутаций — каждая операция персистится до подтверждения сервером, ретраи с экспоненциальным backoff. Финтех-правило: поставленный в очередь платёж не теряется никогда.
- Конфликты — last-write-wins (просто, но теряет данные) vs merge по полям vs server-authoritative с версией/ETag: клиент шлёт базовую версию, сервер решает.
- Reachability — только хинт — пробуй запрос и обрабатывай ошибку; не «gate» по флагу сети: он врёт (captive portal, полурабочий Wi-Fi).
- Фон —
BGAppRefreshTaskнегарантирован: бюджет выдаёт система, поэтому синк обязан догонять и при обычном запуске.
⚠️ Частая ошибка: держать очередь мутаций в памяти. Убили приложение — потеряли операции пользователя. Очередь живёт в той же локальной базе, транзакционно с самим изменением данных.
Источники
Источники и редакционная политика
Ссылки на источники помогают изучить тему глубже. Если для ответа опубликована техническая проверка, рядом указаны её дата и область. Мы не связаны с упомянутыми работодателями и не продаём места в подборках. Источники, функции сервисов и формат найма могут меняться.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.