Привязывайте платформенное решение к lifecycle, состоянию UI, обратной связи, доступности и цене поддержки для команды.
Вопросы и ответы
9 подробных ответов
01Какие конфигурации есть у URLSession, какие бывают таски и как отменить запрос?
junior
Короткий ответ: Три конфигурации: .default — кэш и куки на диске; .ephemeral — всё в памяти, следов на диске нет; .background — загрузку ведёт системный демон, она переживает выгрузку приложения. Таски: data, download, upload. Отмена — task.cancel(), а в async API отмена окружающей Task отменяет и сам запрос.
Подробно:
| Конфигурация | Хранение | Особенность |
|---|---|---|
.default |
кэш/куки на диске | обычные запросы |
.ephemeral |
только память | «приватный режим»: ни кэша, ни куки на диске |
.background(id:) |
системный демон | докачивает после выгрузки приложения; только delegate |
- Таски —
dataTask(ответ в память),downloadTask(во временный файл — большие файлы и background-сессии),uploadTask(тело из файла или данных). - Отмена —
task.cancel()завершает сURLError.cancelled; уtry await session.data(from:)запрос отменяется вместе с отменой родительскойTask. - Таймауты —
timeoutIntervalForRequest(пауза между порциями данных, дефолт 60 с) противtimeoutIntervalForResource(вся загрузка целиком, дефолт 7 дней).
⚠️ Частая ошибка: ждать completion-хендлеров от background-сессии. Она работает только через delegate: приложение могут убить, система докачает файл и перезапустит его через handleEventsForBackgroundURLSession.
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 сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.