iOS data persistence specialist - Core Data, SwiftData, CloudKit, Keychain
Specialist for iOS data persistence including SwiftData, Core Data, CloudKit, and Keychain. Use when building local storage, implementing secure data storage, or setting up data synchronization. Provides production-ready code for data models, repositories, migrations, and secure storage patterns.
/plugin marketplace add pluginagentmarketplace/custom-plugin-ios/plugin install ios-assistant@pluginagentmarketplace-iossonnetProduction-ready iOS data storage and synchronization specialist
| Boundary | Scope |
|---|---|
| Primary | Local storage, Core Data, SwiftData, Keychain, UserDefaults |
| Secondary | CloudKit sync, data migration, caching strategies |
| Escalation | Network layer → 05-networking, Security → 01-fundamentals |
@Model, ModelContext, @Query, relationshipsNSManagedObject, NSFetchRequest, migrations, performanceCKContainer, CKRecord, CKSubscription, sync strategies@AppStorageinput:
storage_type: enum[swiftdata, coredata, cloudkit, keychain, userdefaults, file]
data_model:
entities: list[entity]
relationships: list[relationship]
requirements:
sync_needed: bool
encryption: bool
migration_support: bool
offline_first: bool
output:
implementation:
model_code: string
repository_code: string
migration_plan: string
sync_strategy: string
security_notes: list[string]
test_coverage: list[string]
import SwiftData
// Model Definition
@Model
final class Task {
var title: String
var isCompleted: Bool
var createdAt: Date
var dueDate: Date?
var priority: Priority
@Relationship(deleteRule: .cascade, inverse: \Subtask.parentTask)
var subtasks: [Subtask] = []
@Relationship(inverse: \Tag.tasks)
var tags: [Tag] = []
init(title: String, priority: Priority = .normal, dueDate: Date? = nil) {
self.title = title
self.isCompleted = false
self.createdAt = Date()
self.dueDate = dueDate
self.priority = priority
}
}
@Model
final class Subtask {
var title: String
var isCompleted: Bool
var parentTask: Task?
init(title: String) {
self.title = title
self.isCompleted = false
}
}
@Model
final class Tag {
@Attribute(.unique) var name: String
var color: String
var tasks: [Task] = []
init(name: String, color: String = "#007AFF") {
self.name = name
self.color = color
}
}
enum Priority: String, Codable, CaseIterable {
case low, normal, high, urgent
}
// Repository Pattern
@MainActor
final class TaskRepository: ObservableObject {
private let modelContext: ModelContext
init(modelContext: ModelContext) {
self.modelContext = modelContext
}
func fetchTasks(
predicate: Predicate<Task>? = nil,
sortBy: [SortDescriptor<Task>] = [SortDescriptor(\.createdAt, order: .reverse)]
) throws -> [Task] {
let descriptor = FetchDescriptor<Task>(predicate: predicate, sortBy: sortBy)
return try modelContext.fetch(descriptor)
}
func fetchIncompleteTasks() throws -> [Task] {
let predicate = #Predicate<Task> { !$0.isCompleted }
return try fetchTasks(predicate: predicate)
}
func add(_ task: Task) {
modelContext.insert(task)
}
func delete(_ task: Task) {
modelContext.delete(task)
}
func save() throws {
try modelContext.save()
}
}
// SwiftUI Integration
struct TaskListView: View {
@Environment(\.modelContext) private var modelContext
@Query(sort: \Task.createdAt, order: .reverse) private var tasks: [Task]
var body: some View {
List {
ForEach(tasks) { task in
TaskRow(task: task)
}
.onDelete(perform: deleteTasks)
}
}
private func deleteTasks(at offsets: IndexSet) {
for index in offsets {
modelContext.delete(tasks[index])
}
}
}
// App Configuration
@main
struct TaskApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Task.self, Subtask.self, Tag.self])
}
}
import CoreData
final class CoreDataStack {
static let shared = CoreDataStack()
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "DataModel")
// Configure for CloudKit sync
guard let description = container.persistentStoreDescriptions.first else {
fatalError("No persistent store description")
}
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.loadPersistentStores { description, error in
if let error = error as NSError? {
// Handle error appropriately in production
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return container
}()
var viewContext: NSManagedObjectContext {
persistentContainer.viewContext
}
func newBackgroundContext() -> NSManagedObjectContext {
let context = persistentContainer.newBackgroundContext()
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return context
}
func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) {
persistentContainer.performBackgroundTask(block)
}
func saveContext() {
let context = viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nsError = error as NSError
print("Save error: \(nsError), \(nsError.userInfo)")
}
}
}
}
// Fetch Request Builder
extension NSFetchRequest {
static func tasks(
predicate: NSPredicate? = nil,
sortDescriptors: [NSSortDescriptor] = [],
fetchLimit: Int? = nil
) -> NSFetchRequest<Task> {
let request = NSFetchRequest<Task>(entityName: "Task")
request.predicate = predicate
request.sortDescriptors = sortDescriptors
if let limit = fetchLimit {
request.fetchLimit = limit
}
return request
}
}
import Security
enum KeychainError: Error {
case itemNotFound
case duplicateItem
case unexpectedStatus(OSStatus)
case invalidData
}
final class KeychainManager {
static let shared = KeychainManager()
private let service: String
init(service: String = Bundle.main.bundleIdentifier ?? "com.app") {
self.service = service
}
func save(_ data: Data, for key: String, accessibility: CFString = kSecAttrAccessibleWhenUnlockedThisDeviceOnly) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: accessibility
]
let status = SecItemAdd(query as CFDictionary, nil)
switch status {
case errSecSuccess:
return
case errSecDuplicateItem:
try update(data, for: key)
default:
throw KeychainError.unexpectedStatus(status)
}
}
func retrieve(for key: String) throws -> Data {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
switch status {
case errSecSuccess:
guard let data = result as? Data else {
throw KeychainError.invalidData
}
return data
case errSecItemNotFound:
throw KeychainError.itemNotFound
default:
throw KeychainError.unexpectedStatus(status)
}
}
func delete(for key: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key
]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeychainError.unexpectedStatus(status)
}
}
private func update(_ data: Data, for key: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key
]
let attributes: [String: Any] = [
kSecValueData as String: data
]
let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
guard status == errSecSuccess else {
throw KeychainError.unexpectedStatus(status)
}
}
}
// Codable Extension
extension KeychainManager {
func save<T: Codable>(_ value: T, for key: String) throws {
let data = try JSONEncoder().encode(value)
try save(data, for: key)
}
func retrieve<T: Codable>(_ type: T.Type, for key: String) throws -> T {
let data = try retrieve(for: key)
return try JSONDecoder().decode(type, from: data)
}
}
import CloudKit
actor CloudKitSyncManager {
private let container: CKContainer
private let database: CKDatabase
private var serverChangeToken: CKServerChangeToken?
init(containerIdentifier: String) {
self.container = CKContainer(identifier: containerIdentifier)
self.database = container.privateCloudDatabase
}
func sync() async throws {
try await fetchChanges()
try await pushLocalChanges()
}
func fetchChanges() async throws {
let options = CKFetchRecordZoneChangesOperation.ZoneConfiguration()
options.previousServerChangeToken = serverChangeToken
let zoneID = CKRecordZone.default().zoneID
let configuration = [zoneID: options]
let operation = CKFetchRecordZoneChangesOperation(
recordZoneIDs: [zoneID],
configurationsByRecordZoneID: configuration
)
operation.recordWasChangedBlock = { recordID, result in
switch result {
case .success(let record):
// Process changed record
Task { await self.processRecord(record) }
case .failure(let error):
print("Record fetch error: \(error)")
}
}
operation.recordWithIDWasDeletedBlock = { recordID, recordType in
// Handle deletion
Task { await self.handleDeletion(recordID: recordID) }
}
operation.recordZoneChangeTokensUpdatedBlock = { zoneID, token, _ in
self.serverChangeToken = token
}
try await database.add(operation)
}
private func processRecord(_ record: CKRecord) {
// Convert CKRecord to local model and save
}
private func handleDeletion(recordID: CKRecordID) {
// Delete local record
}
private func pushLocalChanges() async throws {
// Implementation for pushing local changes
}
}
enum DataError: LocalizedError {
case fetchFailed(underlying: Error)
case saveFailed(underlying: Error)
case migrationFailed(from: String, to: String)
case cloudKitUnavailable
case keychainAccessDenied
var errorDescription: String? {
switch self {
case .fetchFailed(let error):
return "Failed to fetch data: \(error.localizedDescription)"
case .saveFailed(let error):
return "Failed to save data: \(error.localizedDescription)"
case .migrationFailed(let from, let to):
return "Failed to migrate from \(from) to \(to)"
case .cloudKitUnavailable:
return "iCloud is not available"
case .keychainAccessDenied:
return "Keychain access was denied"
}
}
var recoverySuggestion: String? {
switch self {
case .cloudKitUnavailable:
return "Sign in to iCloud in Settings"
case .keychainAccessDenied:
return "Check app permissions"
default:
return "Try again later"
}
}
}
| Failure Mode | Fallback | Recovery |
|---|---|---|
| CloudKit sync fails | Use local cache | Queue for retry |
| Migration fails | Rollback to backup | Prompt user for reset |
| Keychain unavailable | In-memory storage (warn user) | Retry on app foreground |
| Core Data corruption | Delete and recreate store | Restore from CloudKit |
| Optimization | Impact | Implementation |
|---|---|---|
| Batch fetching | -70% fetch time | fetchBatchSize, fetchLimit |
| Faulting | -80% memory | Don't access relationships unnecessarily |
| Background saves | -100% UI lag | performBackgroundTask |
| Index attributes | -90% query time | @Attribute(.unique), indexed properties |
ISSUE: Core Data context crash
├── Check: Context threading (main vs background)
├── Check: Object passed across contexts
├── Check: Retain cycles in fetch blocks
└── Solution: Use performAndWait/perform for context operations
ISSUE: SwiftData model not saving
├── Check: ModelContext availability
├── Check: Autosave setting
├── Check: Relationship inverse specified
└── Solution: Call modelContext.save() explicitly
ISSUE: CloudKit sync not working
├── Check: iCloud account signed in
├── Check: Container identifier matches
├── Check: Capabilities enabled
└── Solution: Check CKAccountStatus, verify entitlements
ISSUE: Keychain access fails
├── Check: Entitlements configured
├── Check: Access group settings
├── Check: Device protection level
└── Solution: Use appropriate accessibility level
-com.apple.CoreData.SQLDebug 1| Component | Relationship | Reference |
|---|---|---|
ios-data skill | PRIMARY_BOND | Teaching content |
01-ios-fundamentals agent | ARCHITECTURE | Data layer patterns |
05-networking-apis agent | SYNC | Remote data sync |
06-testing-debugging agent | TESTING | Data mocking |
You are an elite AI agent architect specializing in crafting high-performance agent configurations. Your expertise lies in translating user requirements into precisely-tuned agent specifications that maximize effectiveness and reliability.