ios-data — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ios-data (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
| Data Type | Storage | Why |
|---|---|---|
| User preferences | UserDefaults / @AppStorage | Simple, auto-loaded at launch |
| Preferences synced across devices | NSUbiquitousKeyValueStore | Simple iCloud sync, <1MB |
| Passwords, tokens, API keys | Keychain | Encrypted, survives reinstall |
| Structured app data (iOS 17+) | SwiftData | Modern, declarative, queryable |
| Structured app data (iOS 16-) | Core Data | Mature, proven, stable |
| Large files (images, video, PDFs) | FileManager | Direct file I/O, no DB overhead |
| Complex queries, cross-platform | SQLite (GRDB) | Full SQL control, lightweight |
| Public/shared CloudKit data | Core Data + NSPersistentCloudKitContainer | SwiftData only supports private DB |
Is the data sensitive (tokens, passwords, keys)?
YES → Keychain (NEVER UserDefaults)
NO ↓
Is it a simple user preference (theme, flag, small string)?
YES → Need sync across devices?
YES → NSUbiquitousKeyValueStore
NO → @AppStorage / UserDefaults
NO ↓
Is it a large binary file (image, video, PDF)?
YES → FileManager (store path/URL reference in DB if needed)
NO ↓
Is it structured/relational data?
YES → iOS 17+ minimum?
YES → SwiftData
NO → Core Data
NO ↓
Need full SQL control or cross-platform DB?
YES → SQLite via GRDB.swift
NO → Codable + FileManager (JSON/plist file)isStoredInMemoryOnly: true for test ModelContainers@ModelActor for background SwiftData operations -- Model objects are NOT SendablePersistentIdentifier (Sendable) between actors, not model objectsFetchDescriptor.fetchLimit for pagination -- never fetch all records unbounded.unique#Index (iOS 18+) on frequently queried properties for performance@Transient for computed/cached properties that should not be persistedNSBatchInsertRequest, etc.) bypass validation -- use for bulk imports (10x faster)NSManagedObjectContext.mergeChangesfetchBatchSize (default 0 = fetch all) -- set to ~20 for table/collection viewsnewBackgroundContext() or performBackgroundTask for writes -- never block main threadautomaticallyMergesChangesFromParent = true on viewContext for auto UI refresh.applicationSupportDirectory for internal data, .documentDirectory for user-visible filestmp/ and Library/Caches/ can be purged by system -- do not store critical data thereValueObservation for reactive SwiftUI integrationimport SwiftData
@Model
final class Task {
var title: String
var isCompleted: Bool = false
var createdAt: Date = Date.now
@Attribute(.externalStorage) var imageData: Data?
@Relationship(deleteRule: .cascade, inverse: \Tag.tasks)
var tags: [Tag] = []
init(title: String) {
self.title = title
}
}
@Model
final class Tag {
@Attribute(.unique) var name: String
var tasks: [Task] = []
init(name: String) {
self.name = name
}
}@main
struct MyApp: App {
var body: some Scene {
WindowGroup { ContentView() }
.modelContainer(for: [Task.self, Tag.self])
}
}struct TaskListView: View {
@Query(sort: \Task.createdAt, order: .reverse)
private var tasks: [Task]
@Environment(\.modelContext) private var context
var body: some View {
List(tasks) { task in
Text(task.title)
}
}
func addTask(title: String) {
let task = Task(title: title)
context.insert(task)
// autosave handles the rest
}
func deleteTask(_ task: Task) {
context.delete(task)
}
}let incomplete = #Predicate<Task> { !$0.isCompleted }
let search = #Predicate<Task> { task in
task.title.localizedStandardContains("meeting")
}See references/swiftdata.md for full details.
let container = NSPersistentContainer(name: "Model")
container.loadPersistentStores { _, error in
if let error { fatalError("Store failed: \(error)") }
}
container.viewContext.automaticallyMergesChangesFromParent = truelet request = NSFetchRequest<Task>(entityName: "Task")
request.predicate = NSPredicate(format: "isCompleted == %@", NSNumber(value: false))
request.sortDescriptors = [NSSortDescriptor(keyPath: \Task.createdAt, ascending: false)]
request.fetchBatchSize = 20
let tasks = try context.fetch(request)let request = NSBatchInsertRequest(entity: Task.entity(), objects: dictionaries)
request.resultType = .objectIDs
let result = try context.execute(request) as! NSBatchInsertResult
let changes = [NSInsertedObjectIDsKey: result.objectIDs!]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [viewContext])See references/coredata.md for full details.
struct SettingsView: View {
@AppStorage("isDarkMode") private var isDarkMode = false
@AppStorage("username") private var username = ""
var body: some View {
Toggle("Dark Mode", isOn: $isDarkMode)
}
}func saveToKeychain(account: String, data: Data) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecValueData as String: data
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.saveFailed(status)
}
}func save<T: Encodable>(_ object: T, to filename: String) throws {
let url = FileManager.default
.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent(filename)
try FileManager.default.createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true
)
let data = try JSONEncoder().encode(object)
try data.write(to: url, options: .atomic)
}See references/storage.md for full details.
@ModelActor
actor TaskRepository {
func create(title: String) throws -> PersistentIdentifier {
let task = Task(title: title)
modelContext.insert(task)
try modelContext.save()
return task.persistentModelID
}
func fetchIncomplete() throws -> [PersistentIdentifier] {
let descriptor = FetchDescriptor<Task>(
predicate: #Predicate { !$0.isCompleted },
sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)
return try modelContext.fetch(descriptor).map(\.persistentModelID)
}
func complete(id: PersistentIdentifier) throws {
guard let task = modelContext.model(for: id) as? Task else { return }
task.isCompleted = true
try modelContext.save()
}
}// 1. Define local SwiftData model as source of truth
// 2. Sync layer: fetch from API → upsert into SwiftData
// 3. UI reads only from SwiftData via @Query
// 4. Writes go to SwiftData first, then queue API calls
// 5. Use ModelContext.enumerate for large dataset processing@MainActor
func makeTestContainer() throws -> ModelContainer {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(
for: Task.self, Tag.self,
configurations: config
)
return container
}fetchLimit on all list queries -- never fetch unbounded#Index (iOS 18) or Core Data indexes on frequently filtered/sorted properties@Attribute(.externalStorage) for Data properties > a few KB@ModelActor / background context for writes > 100 objectsenumerate() instead of fetch() for processing large datasets (controls memory)fetchBatchSize = 20 on Core Data fetch requests for listsVersionedSchema conforming type for each schema versionSchemaMigrationPlan with ordered list of schemas.lightweight stage when only adding/renaming properties.custom stage when transforming data between versionsModelContainer configuration~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.