Irbisa · Шпаргалка
Хранилище iOS
UserDefaults, Keychain, Core Data, SwiftData, FileManager
Какие способы хранения данных есть в iOS и как выбрать между ними?
Для чего нужен
UserDefaultsи что в нём хранить нельзя?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…Переключатель должен быть включён по умолчанию, но после чистой установки звук не играет. Почему и как это исправить?
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 }}Где хранить токен авторизации в iOS и почему не в
UserDefaults?enum TokenStore { private static var base: [String: Any] { [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: "com.example.auth", kSecAttrAccount as String: "accessToken"] }…Переживают ли данные в Keychain удаление приложения и за что отвечают классы доступности и группы доступа?
Для чего нужны
Documents,Application Support,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…NSCoding/NSKeyedArchiverиCodable: в чём разница и что использовать сегодня?// 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))…Приложение убили посреди записи, и после следующего запуска файл настроек оказался мусором. Как сделать запись файла устойчивой к падению?
// 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()…После восстановления из бэкапа на новом телефоне пропали все сохранённые фотографии, хотя файлы лежат в
Documents, а пути записаны в базе. Что сломалось?// 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"…Из каких частей состоит стек Core Data и за что отвечает каждая?
final class Persistence { let container: NSPersistentContainer init(inMemory: Bool = false) { container = NSPersistentContainer(name: "Model") // loads Model.momd if inMemory {…SwiftData или Core Data: как они связаны и что выбрать для нового приложения?
Как список остаётся синхронным с хранилищем:
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) }…Найдите баг: у части пользователей поиск клиентов падает, пока они вводят имя.
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 [] }}После удаления плейлиста остаются осиротевшие треки. Как работают правила удаления и обратные связи?
@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)…Этот код время от времени падает с ошибкой многопоточности 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() }}Импорт двадцати тысяч записей вешает интерфейс. Как писать в хранилище не на главном потоке?
@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)] {…После каждой фоновой синхронизации количество строк в таблице удваивается. Как сделать импорт идемпотентным?
// 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 {…Как мигрировать хранилище Core Data или SwiftData, когда схема меняется между релизами?
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…Как писать юнит-тесты на код, который сохраняет данные через SwiftData или Core Data?
@MainActorprivate func makeContainer() throws -> ModelContainer { let configuration = ModelConfiguration(isStoredInMemoryOnly: true) return try ModelContainer(for: Note.self, Folder.self, configurations: configuration)}…Виджету нужны данные пользователя. Как разделить хранилище между приложением и его расширениями?
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…После выхода из аккаунта на устройстве не должно остаться ничего от пользователя. Что на самом деле входит в удаление его данных?
Когда выбирать SQLite через GRDB или Realm вместо Core Data или SwiftData?
Список на
@Queryтормозит всё сильнее по мере роста таблицы. Что проверять в первую очередь?// 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…Два экрана редактируют одну и ту же запись, оба сохраняются без ошибки, а одна правка тихо исчезает. Куда она делась?
// default policy: the save throws instead of guessingviewContext.mergePolicy = NSMergePolicy.error do { try viewContext.save()} catch let error as NSError where error.code == NSManagedObjectMergeError {…Команда хочет синхронизацию через iCloud для уже выпущенных моделей. Что меняется и где будут проблемы?
@Modelfinal class Note { // Every attribute optional or defaulted: CloudKit has no required columns var title: String = "" var body: String = "" var updatedAt: Date = Date()…