Хранение данных
Выбор между UserDefaults, файлами, Keychain, Core Data и SwiftData; стек Core Data, его правила многопоточности и миграции, и защита данных на диске.
14 вопросов
JuniorТеорияОчень частоИз каких основных частей состоит стек Core Data и за что отвечает каждая?
Из каких основных частей состоит стек Core Data и за что отвечает каждая?
NSManagedObjectModel описывает схему; NSPersistentStoreCoordinator связывает её с файлом хранилища (обычно SQLite); NSManagedObjectContext — блокнот для создания и правки объектов; NSPersistentContainer соединяет их и выдаёт главный контекст.
Типичные ошибки
- ✗Думают, что
NSManagedObjectContextпишет на диск при каждом изменении свойства - ✗Путают роль store coordinator с ролью контекста
- ✗Считают, что схему задаёт
NSPersistentContainer, а не модель
Уточняющие вопросы
- →Какой объект стека вы сохраняете, чтобы записать изменения на диск?
- →Почему контейнер выдаёт контекст, привязанный к главной очереди?
JuniorТеорияОчень частоUserDefaults, Keychain, файлы и Core Data/SwiftData — что где хранить и что нельзя класть в UserDefaults?
UserDefaults, Keychain, файлы и Core Data/SwiftData — что где хранить и что нельзя класть в UserDefaults?
Мелкие настройки и флаги — в UserDefaults; секреты — в Keychain; крупные блобы — в файловой системе; графы объектов — в Core Data или SwiftData. Никогда не держите секреты или большие данные в UserDefaults — это незашифрованный plist.
Типичные ошибки
- ✗Хранят токены авторизации или пароли в
UserDefaults - ✗Считают
UserDefaultsзашифрованным или безопасным для секретов - ✗Используют
UserDefaultsдля больших данных вместо файлов или базы
Уточняющие вопросы
- →Почему
UserDefaults— плохой выбор для токена авторизации? - →Когда вы выберете Core Data вместо собственных файлов?
JuniorТеорияЧастоЧто такое NSFetchRequest и что NSFetchedResultsController даёт таблице?
Что такое NSFetchRequest и что NSFetchedResultsController даёт таблице?
NSFetchRequest описывает, что выбрать — сущность, предикат и дескрипторы сортировки. NSFetchedResultsController оборачивает его для таблицы — даёт секции и строки и через делегата присылает точечные insert/delete/move/update.
Типичные ошибки
- ✗Думают, что FRC перезагружает всю таблицу вместо точечных изменений
- ✗Путают fetch request (запрос) с контроллером (наблюдателем)
- ✗Считают, что FRC тянет данные из сети, а не из локального хранилища
Уточняющие вопросы
- →Как FRC доставляет таблице перемещение в отличие от обычного обновления?
- →Какую роль играет
sectionNameKeyPathв FRC?
JuniorТеорияЧастоDocuments, Caches и tmp — что бэкапится, что iOS может очистить и куда положить скачанное видео?
Documents, Caches и tmp — что бэкапится, что iOS может очистить и куда положить скачанное видео?
Documents/ хранит пользовательские данные и бэкапится в iCloud. Library/Caches/ — воспроизводимые данные: не бэкапится, iOS может очистить при нехватке места. tmp/ — временный каталог, стираемый в любой момент. Видео кладут в Caches.
Типичные ошибки
- ✗Кладут крупные повторно загружаемые медиа в Documents, раздувая бэкап iCloud
- ✗Считают, что система никогда не чистит Caches
- ✗Относятся к tmp как к долговечному хранилищу
Уточняющие вопросы
- →Почему хранение повторно загружаемых медиа в Documents раздувает бэкапы или грозит реджектом?
- →Что даёт установка
isExcludedFromBackupна файле в Documents?
JuniorТеорияЧастоЧто хранит Keychain, что задаёт kSecAttrAccessible и почему элемент переживает переустановку?
Что хранит Keychain, что задаёт kSecAttrAccessible и почему элемент переживает переустановку?
Keychain — небольшое зашифрованное, аппаратно защищённое хранилище для секретов — токенов, паролей, ключей — а не для крупных данных. kSecAttrAccessible… задаёт, когда элемент читаем и привязан ли только к устройству. Элементы живут вне песочницы приложения.
Типичные ошибки
- ✗Хранят крупные данные или кэши в Keychain вместо мелких секретов
- ✗Думают, что элементы Keychain удаляются при деинсталляции приложения
- ✗Игнорируют
kSecAttrAccessible, оставляя элементы читаемыми при блокировке
Уточняющие вопросы
- →Чем отличается
WhenUnlockedотAfterFirstUnlock? - →Как значение доступности
ThisDeviceOnlyвлияет на резервные копии?
MiddleТеорияЧастоЧто делает context.save() и что происходит с несохранёнными изменениями, если приложение убито?
Что делает context.save() и что происходит с несохранёнными изменениями, если приложение убито?
save() проталкивает изменения контекста на уровень вниз — в родителя, если он есть, иначе через координатор в хранилище. save() дочернего доходит только до родителя, который тоже должен сохраниться. Несохранённые изменения теряются при завершении.
Типичные ошибки
- ✗Думают, что
save()дочернего контекста доходит до диска без сохранения родителя - ✗Считают, что несохранённые изменения переживут завершение приложения
- ✗Верят, что
save()одного контекста сбрасывает все остальные
Уточняющие вопросы
- →Почему сохранение дочернего контекста не пишет сразу в файл хранилища?
- →Как фоновый дочерний контекст не блокирует контекст главной очереди?
MiddleДебаггингЧастоCore Data падает при установке связи между объектами из разных контекстов
Core Data падает при установке связи между объектами из разных контекстов
Связь Core Data соединяет только два NSManagedObject из одного контекста. Здесь note в главном контексте, а category получен в фоновом, поэтому note.category = category падает. Исправление — заново получить category в главном контексте по его objectID и там установить связь.
Типичные ошибки
- ✗Устанавливают связь между двумя разными
NSManagedObjectContext - ✗Думают, что это проблема потоков, решаемая одним переходом на очередь
- ✗Используют KVC в обход проверки контекста вместо переноса через
objectID
Уточняющие вопросы
- →Почему
objectIDбезопасно передавать между контекстами, а сам объект нет? - →Когда здесь стоит взять
object(with:)вместоexistingObject(with:)?
MiddleТеорияЧастоПочему NSManagedObject не потокобезопасен и как передать его в другой контекст?
Почему NSManagedObject не потокобезопасен и как передать его в другой контекст?
NSManagedObject привязан к своему контексту и его очереди, поэтому обращение из другого потока портит контекст. Не передавайте сам объект — передайте objectID и заново получите объект в целевом контексте внутри его блока perform.
Типичные ошибки
- ✗Передают сам
NSManagedObjectмежду потоками или контекстами - ✗Думают, что лок делает managed object потокобезопасным
- ✗Забывают заново получить объект по
objectIDвнутриperformцелевого контекста
Уточняющие вопросы
- →Чем отличается
object(with:)отexistingObject(withID:)? - →Почему повторный fetch должен идти внутри блока
performцелевого контекста?
MiddleДизайнЧастоВы начинаете новое iOS-приложение в 2026 году, которому нужно локальное структурированное запрашиваемое хранилище. Команда выбирает между SwiftData — с @Model и ModelContainer — и Core Data. Минимальная поддерживаемая версия обсуждаема. Один инженер утверждает, что SwiftData полностью заменяет Core Data и к Core Data больше не надо прикасаться; другой хочет остаться на Core Data ради зрелости. Решите, что вы возьмёте и почему, и чётко скажите, на чём SwiftData на самом деле построен, где ему пока не хватает паритета, какую минимальную версию он навязывает и — если у приложения уже было хранилище Core Data из раннего прототипа — что вы сделаете с этими данными.
Вы начинаете новое iOS-приложение в 2026 году, которому нужно локальное структурированное запрашиваемое хранилище. Команда выбирает между SwiftData — с @Model и ModelContainer — и Core Data. Минимальная поддерживаемая версия обсуждаема. Один инженер утверждает, что SwiftData полностью заменяет Core Data и к Core Data больше не надо прикасаться; другой хочет остаться на Core Data ради зрелости. Решите, что вы возьмёте и почему, и чётко скажите, на чём SwiftData на самом деле построен, где ему пока не хватает паритета, какую минимальную версию он навязывает и — если у приложения уже было хранилище Core Data из раннего прототипа — что вы сделаете с этими данными.
Берите SwiftData для нового приложения, если можете требовать iOS 17+; к Core Data — там, где нет паритета. Это не замена, а Swift-нативный слой поверх стека Core Data — @Model использует тот же движок. Данные Core Data не выбрасывают — оба делят одно хранилище при миграции.
Типичные ошибки
- ✗Считают SwiftData движком с нуля, заменяющим Core Data
- ✗Забывают о требовании SwiftData к iOS 17+
- ✗Думают, что для перехода на SwiftData надо стереть данные Core Data
Уточняющие вопросы
- →Каких возможностей Core Data всё ещё нет эквивалента в SwiftData?
- →Как
ModelContainerиз SwiftData и стек Core Data делят одно хранилище?
MiddleТеорияИногдаЧто задают классы Data Protection и как полная защита ломает фоновую задачу на заблокированном устройстве?
Что задают классы Data Protection и как полная защита ломает фоновую задачу на заблокированном устройстве?
Data Protection шифрует файлы на диске; класс задаёт, когда файл читаем относительно блокировки. Complete нечитаем при блокировке; дефолтный CompleteUntilFirstUserAuthentication читаем после первой разблокировки. Задача в 3 ночи с Complete-файлом падает после автоблокировки телефона.
Типичные ошибки
- ✗Оставляют читаемый в фоне файл на
Complete, и он нечитаем при блокировке - ✗Думают, что Data Protection задаёт, какие приложения имеют доступ к файлу
- ✗Считают, что зашифрованный файл читаем независимо от блокировки устройства
Уточняющие вопросы
- →Какой класс защиты позволяет фоновой задаче читать файл после первой разблокировки?
- →Почему ключ расшифровки недоступен, пока устройство заблокировано?
MiddleДизайнИногдаВаше опубликованное приложение хранит данные в Core Data. В следующем релизе к существующей сущности добавляются два атрибута, один атрибут переименовывается и добавляется новая сущность со связью со старой. Часть пользователей на три версии позади. Нужно выпустить изменение схемы так, чтобы никто не потерял локальные данные при обновлении, и миграция должна проходить автоматически при запуске без заметной задержки для подавляющего большинства. Объясните, когда lightweight-миграция Core Data справится с таким изменением сама, для чего нужна кастомная mapping model и когда вы обязаны её применить, и как вы версионируете модель и проверяете путь обновления, чтобы пользователь на три версии назад безопасно пришёл к новой схеме.
Ваше опубликованное приложение хранит данные в Core Data. В следующем релизе к существующей сущности добавляются два атрибута, один атрибут переименовывается и добавляется новая сущность со связью со старой. Часть пользователей на три версии позади. Нужно выпустить изменение схемы так, чтобы никто не потерял локальные данные при обновлении, и миграция должна проходить автоматически при запуске без заметной задержки для подавляющего большинства. Объясните, когда lightweight-миграция Core Data справится с таким изменением сама, для чего нужна кастомная mapping model и когда вы обязаны её применить, и как вы версионируете модель и проверяете путь обновления, чтобы пользователь на три версии назад безопасно пришёл к новой схеме.
Lightweight-миграция справляется, когда Core Data может вывести отображение — добавление/удаление атрибутов и сущностей, превращение в optional, переименование через renaming identifier. Преобразования данных требуют mapping model и, возможно, policy. Версионируйте модель, проверяйте каждый шаг.
Типичные ошибки
- ✗Ждут, что lightweight-миграция потянет преобразования значений или разделение сущностей
- ✗Забывают renaming identifier при переименовании атрибута
- ✗Не тестируют путь обновления через несколько версий на реальных данных
Уточняющие вопросы
- →Что позволяет
NSEntityMigrationPolicy, чего не может вывод отображения? - →Как ступенчатые миграции помогают пользователю на несколько версий назад?
MiddleПроизводительностьИногдаПриложение пишет тысячи мелких файлов; запуск замедляется и диск растёт — как измерить и исправить?
Приложение пишет тысячи мелких файлов; запуск замедляется и диск растёт — как измерить и исправить?
Сначала измерьте — шаблон File Activity в Instruments плюс du и замер времени обхода каталога, чтобы увидеть число записей и горячий путь. Тысячи мелких файлов стоят метаданных на файл, замедляют обход и раздувают бэкапы. Решение — объединить записи в одно хранилище SQLite или Core Data.
Типичные ошибки
- ✗Хранят один файл на запись вместо единого объединённого хранилища
- ✗Игнорируют стоимость метаданных на файл и обхода каталога на объёме
- ✗Не профилируют файловый ввод-вывод в Instruments перед сменой раскладки
Уточняющие вопросы
- →Почему обход каталога из тысяч файлов замедляет запуск приложения?
- →Как объединение в SQLite снижает накладные расходы на бэкап и диск?
SeniorДизайнРедкоСпроектируйте слой локального хранения для приложения, которое должно быть полностью работоспособно офлайн — на чтение и на запись — а сервер выступает лишь как отложенное зеркало, а не источник истины прямо сейчас. Требования: локальное хранилище — источник истины, из которого UI читает и в которое пишет мгновенно; офлайн-мутации пользователя попадают в устойчивую очередь ожидающих мутаций и проигрываются на сервер при появлении сети; и, поскольку ту же запись тем временем могли изменить на другом устройстве, нужна стратегия разрешения конфликтов, когда проигранная мутация сталкивается с версией сервера. Опишите хранилище и журнал мутаций, как вы безопасно осушаете очередь (порядок, повторы, идемпотентность) и как разрешаете конфликты — включая то, как схема локального хранилища и очереди развивается между версиями приложения, не теряя несинхронизированных мутаций.
Спроектируйте слой локального хранения для приложения, которое должно быть полностью работоспособно офлайн — на чтение и на запись — а сервер выступает лишь как отложенное зеркало, а не источник истины прямо сейчас. Требования: локальное хранилище — источник истины, из которого UI читает и в которое пишет мгновенно; офлайн-мутации пользователя попадают в устойчивую очередь ожидающих мутаций и проигрываются на сервер при появлении сети; и, поскольку ту же запись тем временем могли изменить на другом устройстве, нужна стратегия разрешения конфликтов, когда проигранная мутация сталкивается с версией сервера. Опишите хранилище и журнал мутаций, как вы безопасно осушаете очередь (порядок, повторы, идемпотентность) и как разрешаете конфликты — включая то, как схема локального хранилища и очереди развивается между версиями приложения, не теряя несинхронизированных мутаций.
Локальное хранилище — источник истины, из которого UI читает и пишет; каждая офлайн-мутация попадает в устойчивый упорядоченный журнал, а не только в саму запись. Воркер синхронизации осушает его при сети — по порядку, идемпотентно через client id, с backoff-повторами — и разрешает конфликты явной политикой вроде last-write-wins.
Типичные ошибки
- ✗Считают сервер источником истины прямо сейчас вместо локального хранилища
- ✗Держат ожидающие мутации в памяти, теряя их при завершении
- ✗Не имеют явной политики разрешения конфликтов для проигранных мутаций
Уточняющие вопросы
- →Почему журналу мутаций нужны ключи идемпотентности при осушении?
- →Когда last-write-wins неприемлем и отслеживающие конфликты version vectors стоят своей цены?