ios-data-persistence/SKILL.md
Implement data persistence using SwiftData, Core Data, UserDefaults, Keychain, and file storage. Use when storing data locally, designing data models, implementing caching, managing database migrations, or debugging persistence issues. Triggers on SwiftData, Core Data, UserDefaults, Keychain, storage, database, persist, save, cache, migration, model.
npx skillsauth add abanoub-ashraf/manus-skills-import ios-data-persistenceInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
4 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are a data persistence specialist for iOS apps. When this skill activates, help implement the right storage solution for each use case.
| Use Case | Solution | |----------|----------| | User preferences, settings | UserDefaults | | Sensitive data (tokens, passwords) | Keychain | | Structured app data | SwiftData (iOS 17+) or Core Data | | Large files (images, documents) | File System | | Temporary cache | NSCache + Disk cache | | Simple key-value store | UserDefaults or Property List |
import SwiftData
@Model
final class Task {
var title: String
var isCompleted: Bool
var createdAt: Date
var dueDate: Date?
@Relationship(deleteRule: .cascade)
var subtasks: [Subtask]?
@Relationship(inverse: \Category.tasks)
var category: Category?
init(title: String, isCompleted: Bool = false) {
self.title = title
self.isCompleted = isCompleted
self.createdAt = Date()
}
}
@Model
final class Category {
@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, Category.self])
}
}
// Custom configuration
let config = ModelConfiguration(
schema: Schema([Task.self]),
isStoredInMemoryOnly: false,
allowsSave: true
)
let container = try ModelContainer(for: Task.self, configurations: config)
struct TaskListView: View {
@Environment(\.modelContext) private var context
@Query(sort: \Task.createdAt, order: .reverse) private var tasks: [Task]
// Filtered query
@Query(filter: #Predicate<Task> { !$0.isCompleted },
sort: \Task.dueDate)
private var pendingTasks: [Task]
var body: some View {
List(tasks) { task in
TaskRow(task: task)
}
}
func addTask(title: String) {
let task = Task(title: title)
context.insert(task)
// Auto-saves
}
func deleteTask(_ task: Task) {
context.delete(task)
}
func updateTask(_ task: Task, title: String) {
task.title = title
// Auto-saves on change
}
}
struct SearchableTaskList: View {
@State private var searchText = ""
var body: some View {
TaskList(searchText: searchText)
.searchable(text: $searchText)
}
}
struct TaskList: View {
@Query private var tasks: [Task]
init(searchText: String) {
let predicate = #Predicate<Task> { task in
searchText.isEmpty || task.title.localizedStandardContains(searchText)
}
_tasks = Query(filter: predicate, sort: \Task.createdAt)
}
var body: some View {
List(tasks) { TaskRow(task: $0) }
}
}
class PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "Model")
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { _, error in
if let error = error {
fatalError("Core Data failed: \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
func save() {
let context = container.viewContext
if context.hasChanges {
try? context.save()
}
}
}
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Task.createdAt, ascending: false)],
predicate: NSPredicate(format: "isCompleted == %@", NSNumber(value: false)),
animation: .default
)
private var tasks: FetchedResults<Task>
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
let container: UserDefaults
init(_ key: String, defaultValue: T, container: UserDefaults = .standard) {
self.key = key
self.defaultValue = defaultValue
self.container = container
}
var wrappedValue: T {
get { container.object(forKey: key) as? T ?? defaultValue }
set { container.set(newValue, forKey: key) }
}
}
// App Storage for SwiftUI
struct SettingsView: View {
@AppStorage("isDarkMode") private var isDarkMode = false
@AppStorage("username") private var username = ""
@AppStorage("notificationsEnabled") private var notificationsEnabled = true
var body: some View {
Form {
Toggle("Dark Mode", isOn: $isDarkMode)
TextField("Username", text: $username)
Toggle("Notifications", isOn: $notificationsEnabled)
}
}
}
extension UserDefaults {
func setCodable<T: Codable>(_ value: T, forKey key: String) {
if let data = try? JSONEncoder().encode(value) {
set(data, forKey: key)
}
}
func codable<T: Codable>(forKey key: String) -> T? {
guard let data = data(forKey: key) else { return nil }
return try? JSONDecoder().decode(T.self, from: data)
}
}
// Usage
UserDefaults.standard.setCodable(user, forKey: "currentUser")
let user: User? = UserDefaults.standard.codable(forKey: "currentUser")
actor KeychainManager {
enum KeychainError: Error {
case duplicateItem
case itemNotFound
case unexpectedStatus(OSStatus)
}
func save(_ data: Data, for key: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
]
let status = SecItemAdd(query as CFDictionary, nil)
if status == errSecDuplicateItem {
try update(data, for: key)
} else if status != errSecSuccess {
throw KeychainError.unexpectedStatus(status)
}
}
func read(for key: String) throws -> Data {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess, let data = result as? Data else {
throw KeychainError.itemNotFound
}
return data
}
func delete(for key: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
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,
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)
}
}
}
// Convenience extension for Codable
extension KeychainManager {
func save<T: Codable>(_ item: T, for key: String) throws {
let data = try JSONEncoder().encode(item)
try save(data, for: key)
}
func read<T: Codable>(for key: String) throws -> T {
let data = try read(for: key)
return try JSONDecoder().decode(T.self, from: data)
}
}
actor FileStorageManager {
private let fileManager = FileManager.default
var documentsDirectory: URL {
fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
}
var cachesDirectory: URL {
fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
}
func save(_ data: Data, filename: String, in directory: URL? = nil) throws -> URL {
let targetDir = directory ?? documentsDirectory
let fileURL = targetDir.appendingPathComponent(filename)
try data.write(to: fileURL)
return fileURL
}
func load(filename: String, from directory: URL? = nil) throws -> Data {
let targetDir = directory ?? documentsDirectory
let fileURL = targetDir.appendingPathComponent(filename)
return try Data(contentsOf: fileURL)
}
func delete(filename: String, from directory: URL? = nil) throws {
let targetDir = directory ?? documentsDirectory
let fileURL = targetDir.appendingPathComponent(filename)
try fileManager.removeItem(at: fileURL)
}
func listFiles(in directory: URL? = nil) throws -> [URL] {
let targetDir = directory ?? documentsDirectory
return try fileManager.contentsOfDirectory(at: targetDir, includingPropertiesForKeys: nil)
}
}
actor ImageCache {
private let memoryCache = NSCache<NSString, UIImage>()
private let diskCacheURL: URL
init() {
let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
diskCacheURL = caches.appendingPathComponent("ImageCache")
try? FileManager.default.createDirectory(at: diskCacheURL, withIntermediateDirectories: true)
}
func image(for key: String) async -> UIImage? {
// Check memory
if let image = memoryCache.object(forKey: key as NSString) {
return image
}
// Check disk
let fileURL = diskCacheURL.appendingPathComponent(key.hash.description)
if let data = try? Data(contentsOf: fileURL),
let image = UIImage(data: data) {
memoryCache.setObject(image, forKey: key as NSString)
return image
}
return nil
}
func store(_ image: UIImage, for key: String) {
memoryCache.setObject(image, forKey: key as NSString)
let fileURL = diskCacheURL.appendingPathComponent(key.hash.description)
if let data = image.jpegData(compressionQuality: 0.8) {
try? data.write(to: fileURL)
}
}
func clear() {
memoryCache.removeAllObjects()
try? FileManager.default.removeItem(at: diskCacheURL)
try? FileManager.default.createDirectory(at: diskCacheURL, withIntermediateDirectories: true)
}
}
enum SchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] = [TaskV1.self]
@Model
final class TaskV1 {
var title: String
init(title: String) { self.title = title }
}
}
enum SchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] = [Task.self]
}
enum TaskMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] = [SchemaV1.self, SchemaV2.self]
static var stages: [MigrationStage] = [
.lightweight(fromVersion: SchemaV1.self, toVersion: SchemaV2.self)
]
}
development
Design principles for building polished, native-feeling SwiftUI apps and widgets. Use this skill when creating or modifying SwiftUI views, iOS widgets (WidgetKit), or any native Apple UI. Ensures proper spacing, typography, colors, and widget implementations that look and feel like quality apps rather than AI-generated slop.
data-ai
Design and implement SwiftUI views, components, and app architecture. Use when creating new SwiftUI views, implementing MVVM/TCA patterns, managing state with @Observable, @State, @Binding, or @Environment, designing navigation flows, or structuring iOS app architecture. Triggers on SwiftUI, view model, state management, navigation, coordinator pattern.
development
Implement, review, or improve SwiftUI animations and transitions. Use when adding implicit or explicit animations with withAnimation, configuring spring animations (.smooth, .snappy, .bouncy), building phase or keyframe animations with PhaseAnimator/KeyframeAnimator, creating hero transitions with matchedGeometryEffect or matchedTransitionSource, adding SF Symbol effects (bounce, pulse, variableColor, breathe, rotate, wiggle), implementing custom Transition or CustomAnimation types, or ensuring animations respect accessibilityReduceMotion.
testing
Audit SwiftUI views for accessibility (iOS + macOS) with patch-ready fixes