Irbisa · Шпаргалка

Хранилище iOS

UserDefaults, Keychain, Core Data, SwiftData, FileManager

Middle-разработчик · 25 вопросов

  1. 01

    Какие способы хранения данных есть в iOS и как выбрать между ними?

    Берите самое лёгкое хранилище, которое подходит данным: мелкие настройки кладите в UserDefaults, секреты в Keychain, документы и бинарные данные в файлы, структурированные связанные данные в SwiftData или Core Data.

  2. 02

    Для чего нужен UserDefaults и что в нём хранить нельзя?

    UserDefaults представляет собой небольшое хранилище «ключ-значение» для пользовательских настроек; оно сохраняется как property list в контейнере приложения и читается в память при запуске.

    let defaults = UserDefaults.standarddefaults.set(true, forKey: "hasSeenOnboarding")defaults.set(["swift", "ios"], forKey: "recentTags")   // array of plist types: fine struct Filter: Codable {  var sort: String…
  3. 03

    Переключатель должен быть включён по умолчанию, но после чистой установки звук не играет. Почему и как это исправить?

    struct SettingsView: View {  @AppStorage("soundEnabled") private var soundEnabled = true   var body: some View {    Toggle("Sound", isOn: $soundEnabled)  }} final class SoundPlayer {  func play(_ name: String) {    guard UserDefaults.standard.bool(forKey: "soundEnabled") else { return }    // ... plays the sound  }}

    По этому ключу никто ничего не записал: true в @AppStorage служит лишь запасным значением внутри property wrapper, а bool(forKey:) на отсутствующий ключ отвечает false.

  4. 04

    Где хранить токен авторизации в iOS и почему не в UserDefaults?

    В Keychain: система шифрует его ключами, привязанными к устройству и его код-паролю, а UserDefaults пишет в обычный plist, который читается из незашифрованного бэкапа или на джейлбрейкнутом телефоне.

    enum TokenStore {  private static var base: [String: Any] {    [kSecClass as String: kSecClassGenericPassword,     kSecAttrService as String: "com.example.auth",     kSecAttrAccount as String: "accessToken"]  }…
  5. 05

    Переживают ли данные в Keychain удаление приложения и за что отвечают классы доступности и группы доступа?

    Да: элементы Keychain лежат вне контейнера приложения, поэтому переживают удаление, и после переустановки они снова на месте.

  6. 06

    Для чего нужны Documents, Application Support, Caches и tmp и что из них попадает в бэкап или может быть удалено системой?

    У каждого приложения свой контейнер-песочница: в Documents лежит созданное пользователем, в Library/Application Support хранятся собственные данные приложения, и обе папки попадают в бэкап; Library/Caches в бэкап не попадает и может быть очищена; tmp служит временным местом для черновой работы.

    // Re-downloadable media: caches, purgeable, not backed uplet cachesURL = URL.cachesDirectory.appending(path: "videos", directoryHint: .isDirectory)try FileManager.default.createDirectory(at: cachesURL, withIntermediateDirectories: true)let videoURL = cachesURL.appending(path: "\(assetID).mp4") // Moving a finished download into place beats copying the bytes…
  7. 07

    NSCoding/NSKeyedArchiver и Codable: в чём разница и что использовать сегодня?

    Codable состоит из пары Swift-протоколов, реализацию которых компилятор синтезирует для структур, enum и классов; NSCoding является старым протоколом из Objective-C для классов-наследников NSObject, архивируемым через NSKeyedArchiver.

    // Codable: synthesised by the compiler, works for a structstruct Settings: Codable {  var theme: String  var fontSize: Int}let json = try JSONEncoder().encode(Settings(theme: "dark", fontSize: 16))…
  8. 08

    Приложение убили посреди записи, и после следующего запуска файл настроек оказался мусором. Как сделать запись файла устойчивой к падению?

    Никогда не пишите поверх живого файла: положите новые байты рядом и подставьте их переименованием, которое файловая система делает атомарно.

    // wrong: truncating the live file, a crash here leaves half a documentlet handle = try FileHandle(forWritingTo: url)try handle.truncate(atOffset: 0)try handle.write(contentsOf: JSONEncoder().encode(settings))try handle.close()…
  9. 09

    После восстановления из бэкапа на новом телефоне пропали все сохранённые фотографии, хотя файлы лежат в Documents, а пути записаны в базе. Что сломалось?

    В пути к контейнеру есть UUID, который может измениться (восстановление из бэкапа, перенос на новое устройство, переустановка), поэтому ранее сохранённый абсолютный путь указывает на несуществующую директорию.

    // wrong: the container UUID can change after a restore or migrationlet fileURL = URL.documentsDirectory.appending(path: "avatar.jpg")photo.storedPath = fileURL.path()   // /var/mobile/Containers/Data/Application/<UUID>/… // right: persist a name relative to a directory you can rebuildphoto.fileName = "avatar.jpg"…
  10. 10

    Из каких частей состоит стек Core Data и за что отвечает каждая?

    Четыре части: NSManagedObjectModel описывает схему, NSPersistentStoreCoordinator связывает эту модель с файлами хранилища, NSManagedObjectContext служит черновиком, в котором вы получаете и меняете объекты, а NSPersistentContainer создаёт их все и владеет ими.

    final class Persistence {  let container: NSPersistentContainer   init(inMemory: Bool = false) {    container = NSPersistentContainer(name: "Model")      // loads Model.momd    if inMemory {…
  11. 11

    SwiftData или Core Data: как они связаны и что выбрать для нового приложения?

    SwiftData представляет собой Swift-нативный слой над движком Core Data, где схема описывается классами с @Model, а не файлом .xcdatamodeld.

  12. 12

    Как список остаётся синхронным с хранилищем: NSFetchedResultsController в UIKit, @FetchRequest и @Query в SwiftUI?

    Каждый из них оборачивает запрос (сущность, предикат, порядок сортировки) и следит за сохранёнными изменениями в своём контексте, поэтому список обновляется сам, и вам не нужно перезапрашивать данные после каждой записи.

    // SwiftData: the filter comes in through init, because a @Query is fixed once builtstruct NotesList: View {  @Query private var notes: [Note]   init(search: String) {    let filter = #Predicate<Note> { search.isEmpty || $0.title.localizedStandardContains(search) }…
  13. 13

    Найдите баг: у части пользователей поиск клиентов падает, пока они вводят имя.

    func searchCustomers(matching text: String,                     in context: NSManagedObjectContext) throws -> [Customer] {  let request = Customer.fetchRequest()  request.predicate = NSPredicate(format: "name CONTAINS[cd] '\(text)'")  request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]  request.fetchLimit = 50  do {    return try context.fetch(request)  } catch {    print("Search failed: \(error)")    return []  }}

    Текст поиска интерполируется прямо в строку формата предиката, поэтому имя с апострофом вроде O'Brien делает её неразбираемой, и NSPredicate(format:) бросает исключение, которое роняет приложение.

  14. 14

    После удаления плейлиста остаются осиротевшие треки. Как работают правила удаления и обратные связи?

    Для связи нужны обратная связь (inverse) и явное правило удаления, и именно правило решает, удалятся ли вместе с родителем дети, просто отвяжутся или удаление будет запрещено.

    @Modelfinal class Playlist {  var name: String   // right: cascade deletes the tracks; the default (nullify) leaves them with playlist == nil  @Relationship(deleteRule: .cascade, inverse: \Track.playlist)…
  15. 15

    Этот код время от времени падает с ошибкой многопоточности Core Data. Что не так и как передать объект в другой контекст?

    func markAsRead(_ article: Article) {  // article was fetched from container.viewContext on the main thread  let background = container.newBackgroundContext()  background.perform {    article.isRead = true    article.readAt = .now    try? background.save()  }}

    article принадлежит viewContext, а меняется на очереди фонового контекста, причём в background он вообще не зарегистрирован, так что этот save() ничего не записывает.

  16. 16

    Импорт двадцати тысяч записей вешает интерфейс. Как писать в хранилище не на главном потоке?

    Пишите в контексте, из которого UI не читает, а результаты передавайте через идентификаторы, а не через объекты.

    @ModelActoractor ImportActor {  func importPosts(_ dtos: [PostDTO]) throws {    modelContext.autosaveEnabled = false    for start in stride(from: 0, to: dtos.count, by: 500) {      for dto in dtos[start..<min(start + 500, dtos.count)] {…
  17. 17

    После каждой фоновой синхронизации количество строк в таблице удваивается. Как сделать импорт идемпотентным?

    Сделайте идентификатор с сервера идентичностью модели через ограничение уникальности: тогда импорт уже существующей записи обновит имеющуюся строку, а не добавит вторую.

    // SwiftData: the server id is the identity, and insert becomes an upsert@Attribute(.unique) var serverID: String     // inside the @Model class// a known serverID is updated in place on insert and save, not duplicated // CloudKit-synced model: no .unique allowed, so dedupe by hand in ONE fetch@Model final class SyncedArticle {…
  18. 18

    Как мигрировать хранилище Core Data или SwiftData, когда схема меняется между релизами?

    Для аддитивных изменений пусть фреймворк выполнит lightweight-миграцию, кастомную стадию пишите только там, где нужно преобразовать существующие данные, и тестируйте на реальных старых хранилищах: неудачная миграция означает падение при запуске.

    enum SchemaV1: VersionedSchema {  static let versionIdentifier = Schema.Version(1, 0, 0)  static var models: [any PersistentModel.Type] { [Note.self] }   @Model final class Note {    var title: String…
  19. 19

    Как писать юнит-тесты на код, который сохраняет данные через SwiftData или Core Data?

    Дайте каждому тесту собственное хранилище, живущее только в памяти, чтобы тест начинался с пустой базы и ничего не доживало до следующего.

    @MainActorprivate func makeContainer() throws -> ModelContainer {  let configuration = ModelConfiguration(isStoredInMemoryOnly: true)  return try ModelContainer(for: Note.self, Folder.self, configurations: configuration)}…
  20. 20

    Виджету нужны данные пользователя. Как разделить хранилище между приложением и его расширениями?

    Расширение работает в отдельном процессе со своим контейнером, поэтому всё общее должно лежать в каталоге App Group, который разрешено открывать обоим таргетам.

    let groupID = "group.com.example.notes"let containerURL = FileManager.default  .containerURL(forSecurityApplicationGroupIdentifier: groupID)!let storeURL = containerURL.appending(path: "Notes.store") // Shared SwiftData container: identical code in app and widget target…
  21. 21

    После выхода из аккаунта на устройстве не должно остаться ничего от пользователя. Что на самом деле входит в удаление его данных?

    Удалить строки не значит удалить данные: нужно уничтожить файл хранилища вместе с его спутниками, а затем вычистить всё, что живёт вне его (Keychain, настройки, кэши и контейнер App Group).

  22. 22

    Когда выбирать SQLite через GRDB или Realm вместо Core Data или SwiftData?

    Выбирайте GRDB или голый SQLite, когда нужен настоящий SQL (FTS5, CTE, оконные функции), большие данные с интенсивной записью или простые value-типы без фолтов; Realm сегодня остаётся в основном унаследованным выбором.

  23. 23

    Список на @Query тормозит всё сильнее по мере роста таблицы. Что проверять в первую очередь?

    Проверьте, что фильтрация действительно происходит в SQLite: транслируемый предикат отсекает строки в базе, а замыкание поверх уже полученного массива сперва поднимает в память всю таблицу.

    // Slow: everything is fetched, then filtered and sorted in Swiftlet all = try context.fetch(FetchDescriptor<Message>())let recent = all.filter { $0.isUnread && $0.folderName == folderName }                .sorted { $0.sentAt > $1.sentAt } // Fast: predicate translated to SQL, sorted on an indexed column, paged…
  24. 24

    Два экрана редактируют одну и ту же запись, оба сохраняются без ошибки, а одна правка тихо исчезает. Куда она делась?

    Каждый контекст сохраняется относительно снимка, который он получил при выборке, и когда строка в хранилище перестаёт совпадать с этим снимком, какие значения выживут, решает merge policy, а не ваш код.

    // default policy: the save throws instead of guessingviewContext.mergePolicy = NSMergePolicy.error do {  try viewContext.save()} catch let error as NSError where error.code == NSManagedObjectMergeError {…
  25. 25

    Команда хочет синхронизацию через iCloud для уже выпущенных моделей. Что меняется и где будут проблемы?

    Переключите хранилище на конфигурацию с CloudKit, но сначала приведите уже выпущенную схему к правилам CloudKit (каждый атрибут опциональный или со значением по умолчанию, каждая связь опциональная и с обратной связью, без ограничений уникальности), иначе хранилище не загрузится.

    @Modelfinal class Note {  // Every attribute optional or defaulted: CloudKit has no required columns  var title: String = ""  var body: String = ""  var updatedAt: Date = Date()…