
Persistence Setup
- 2 installs
- 591 repo stars
- Updated July 24, 2026
- rshankras/claude-code-apple-skills
Generates a SwiftData or CoreData persistence layer with optional iCloud (CloudKit) sync for local storage and data models in iOS/macOS apps.
About
Generates a SwiftData or CoreData persistence layer with optional iCloud CloudKit sync and data models. A developer uses it to add local storage or cloud data sync to an Apple app.
- SwiftData or CoreData persistence layer
- Optional iCloud/CloudKit sync
Persistence Setup by the numbers
- 2 all-time installs (skills.sh)
- Ranked #741 of 911 Databases skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rshankras/claude-code-apple-skills --skill persistence-setupAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 591 |
| Last updated | July 24, 2026 |
| Repository | rshankras/claude-code-apple-skills ↗ |
What it does
Generates a SwiftData or CoreData persistence layer with optional iCloud (CloudKit) sync for local storage and data models in iOS/macOS apps.
Files
Persistence Setup Generator
Generates a production-ready persistence layer using SwiftData (iOS 17+) or CoreData with optional iCloud (CloudKit) sync.
When This Skill Activates
- User asks to "add persistence" or "set up data storage"
- User mentions "SwiftData", "CoreData", or "local storage"
- User wants to "sync data to iCloud" or "enable cloud sync"
- User asks about "offline storage" or "data models"
Pre-Generation Checks (CRITICAL)
1. Project Context Detection
Before generating, ALWAYS check:
# Check deployment target
cat Package.swift | grep -i "platform"
# Or check project.pbxproj
# Find existing persistence implementations
rg -l "ModelContainer|NSPersistentContainer|@Model|@Entity" --type swift
# Check for existing SwiftData models
rg "@Model" --type swift | head -5
# Check for CoreData stack
rg "NSManagedObjectContext|NSPersistentStore" --type swift | head -5
# Check existing entitlements for iCloud
cat *.entitlements 2>/dev/null | grep -i "icloud"2. Framework Selection
Use SwiftData if:
- Deployment target is iOS 17+ / macOS 14+
- User explicitly requests SwiftData
- No existing CoreData implementation
Use CoreData if:
- Deployment target < iOS 17
- Existing CoreData stack present
- User explicitly requests CoreData
3. Conflict Detection
If existing persistence found:
- Ask: Extend existing, migrate to SwiftData, or create separate?
Configuration Questions
Ask user via AskUserQuestion:
1. Framework choice?
- SwiftData (iOS 17+, recommended)
- CoreData (older targets)
2. Enable iCloud sync?
- Yes (requires CloudKit entitlement)
- No (local only)
3. Generate example model?
- Yes (with sample Item model)
- No (just infrastructure)
Generation Process
Step 1: Create Core Files
Always generate:
Sources/Persistence/
├── PersistenceController.swift # Container setup
├── Repository.swift # Repository protocol
└── SwiftDataRepository.swift # Concrete implementationIf example model requested:
Sources/Persistence/Models/
└── Item.swift # Sample @ModelIf iCloud enabled:
Sources/Persistence/CloudSync/
├── CloudKitConfiguration.swift # Container identifier
└── SyncStatus.swift # Sync monitoringStep 2: Read Templates
Read templates from this skill:
templates/PersistenceController.swifttemplates/Repository.swifttemplates/SwiftDataRepository.swifttemplates/ExampleModel.swifttemplates/CloudKitConfiguration.swift(if iCloud)templates/SyncStatus.swift(if iCloud)
Step 3: Customize for Project
Adapt templates to match:
- Project naming conventions
- Existing model patterns
- Bundle identifier for CloudKit container
Step 4: Integration
Basic Integration:
@main
struct MyApp: App {
let container = PersistenceController.shared.container
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(container)
}
}
}With iCloud sync:
@main
struct MyApp: App {
let container = PersistenceController.shared.container
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(container)
.environment(\.syncStatus, SyncStatus.shared)
}
}
}iCloud Sync Setup
Required Capabilities (Xcode)
1. iCloud capability:
- Check "CloudKit"
- Add container:
iCloud.com.yourcompany.yourapp
2. Background Modes (optional, for background sync):
- Check "Remote notifications"
Required Entitlements
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.com.yourcompany.yourapp</string>
</array>
<key>com.apple.developer.icloud-services</key>
<array>
<string>CloudKit</string>
</array>CloudKit Dashboard Setup
1. Go to CloudKit Dashboard 2. Select your container 3. Schema is auto-created from @Model classes 4. Deploy schema to production before release
Generated Code Patterns
Repository Protocol
protocol Repository<T>: Sendable {
associatedtype T: PersistentModel
func fetch(predicate: Predicate<T>?, sortBy: [SortDescriptor<T>]) async throws -> [T]
func insert(_ item: T) async throws
func delete(_ item: T) async throws
func save() async throws
}SwiftData Model
@Model
final class Item {
var title: String
var timestamp: Date
var isCompleted: Bool
init(title: String, timestamp: Date = .now, isCompleted: Bool = false) {
self.title = title
self.timestamp = timestamp
self.isCompleted = isCompleted
}
}Container with CloudKit
let container = try ModelContainer(
for: Item.self,
configurations: ModelConfiguration(
cloudKitDatabase: .private("iCloud.com.yourcompany.yourapp")
)
)Verification Checklist
After generation, verify:
- [ ] App launches without crashes
- [ ] Data persists between app launches
- [ ] Models compile without errors
- [ ] (If iCloud) CloudKit container exists in dashboard
- [ ] (If iCloud) Data syncs between devices
- [ ] Repository pattern allows easy testing
Common Customizations
Adding New Models
@Model
final class Project {
var name: String
@Relationship(deleteRule: .cascade) var items: [Item]
init(name: String, items: [Item] = []) {
self.name = name
self.items = items
}
}
// Update container
let container = try ModelContainer(for: Item.self, Project.self)Custom Fetch Descriptors
let descriptor = FetchDescriptor<Item>(
predicate: #Predicate { $0.isCompleted == false },
sortBy: [SortDescriptor(\.timestamp, order: .reverse)]
)
let items = try modelContext.fetch(descriptor)Migration (SwiftData)
// SwiftData handles lightweight migrations automatically
// For complex migrations, use VersionedSchema
enum ItemSchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] { [Item.self] }
}Troubleshooting
iCloud Sync Not Working
1. Check entitlements match CloudKit container 2. Verify CloudKit Dashboard shows your container 3. Check device is signed into iCloud 4. Deploy schema to production if testing on release build
Data Not Persisting
1. Verify modelContainer modifier is on root view 2. Check save() is called after modifications 3. Look for errors in Console.app
CloudKit Quota Exceeded
- Default quota is generous (free tier: 100MB asset storage)
- Consider pruning old data
- Use
cloudKitDatabase: .automaticfor shared containers
Related Skills
networking-layer- For remote API data alongside local cachesettings-screen- Often uses @AppStorage (simpler persistence)
References
Persistence Patterns
Best practices for implementing data persistence in iOS and macOS apps using SwiftData.
SwiftData Fundamentals
Model Definition
import SwiftData
@Model
final class Item {
// Stored properties
var title: String
var timestamp: Date
var isCompleted: Bool
// Transient (not persisted)
@Transient var isSelected: Bool = false
// Unique constraint
@Attribute(.unique) var identifier: UUID
// Relationships
@Relationship(deleteRule: .cascade) var tags: [Tag]
@Relationship(inverse: \Project.items) var project: Project?
init(title: String) {
self.identifier = UUID()
self.title = title
self.timestamp = .now
self.isCompleted = false
}
}Model Attributes
@Model
final class Document {
// Unique constraint
@Attribute(.unique) var id: UUID
// External storage for large data
@Attribute(.externalStorage) var imageData: Data?
// Encrypted (automatically uses Data Protection)
@Attribute(.allowsCloudEncryption) var sensitiveData: String?
// Spotlight indexing
@Attribute(.spotlight) var searchableTitle: String
// Preserve value on deletion (for soft delete)
@Attribute(.preserveValueOnDeletion) var deletedAt: Date?
}Relationships
@Model
final class Project {
var name: String
// One-to-many with cascade delete
@Relationship(deleteRule: .cascade)
var items: [Item] = []
// One-to-one (optional)
@Relationship
var owner: User?
// Many-to-many
@Relationship
var collaborators: [User] = []
}
@Model
final class Item {
var title: String
// Inverse relationship (required for bidirectional)
@Relationship(inverse: \Project.items)
var project: Project?
}Repository Pattern
Protocol Definition
protocol Repository<T>: Sendable {
associatedtype T: PersistentModel
func fetch(
predicate: Predicate<T>?,
sortBy: [SortDescriptor<T>]
) async throws -> [T]
func fetchOne(predicate: Predicate<T>) async throws -> T?
func insert(_ item: T) async throws
func delete(_ item: T) async throws
func save() async throws
}
extension Repository {
func fetch(sortBy: [SortDescriptor<T>] = []) async throws -> [T] {
try await fetch(predicate: nil, sortBy: sortBy)
}
}SwiftData Implementation
@MainActor
final class SwiftDataRepository<T: PersistentModel>: Repository {
private let modelContext: ModelContext
init(modelContext: ModelContext) {
self.modelContext = modelContext
}
func fetch(
predicate: Predicate<T>? = nil,
sortBy: [SortDescriptor<T>] = []
) async throws -> [T] {
let descriptor = FetchDescriptor<T>(
predicate: predicate,
sortBy: sortBy
)
return try modelContext.fetch(descriptor)
}
func fetchOne(predicate: Predicate<T>) async throws -> T? {
var descriptor = FetchDescriptor<T>(predicate: predicate)
descriptor.fetchLimit = 1
return try modelContext.fetch(descriptor).first
}
func insert(_ item: T) async throws {
modelContext.insert(item)
try save()
}
func delete(_ item: T) async throws {
modelContext.delete(item)
try save()
}
func save() async throws {
try modelContext.save()
}
}Usage in Views
struct ItemListView: View {
@Environment(\.modelContext) private var modelContext
@Query(sort: \Item.timestamp, order: .reverse) private var items: [Item]
var body: some View {
List(items) { item in
ItemRow(item: item)
}
}
private func deleteItem(_ item: Item) {
modelContext.delete(item)
}
}Container Configuration
Basic Setup
@MainActor
final class PersistenceController {
static let shared = PersistenceController()
let container: ModelContainer
private init() {
let schema = Schema([
Item.self,
Project.self,
])
let configuration = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: false
)
do {
container = try ModelContainer(
for: schema,
configurations: configuration
)
} catch {
fatalError("Failed to create ModelContainer: \(error)")
}
}
}With iCloud Sync
@MainActor
final class PersistenceController {
static let shared = PersistenceController()
let container: ModelContainer
private init() {
let schema = Schema([Item.self])
// CloudKit configuration
let cloudConfig = ModelConfiguration(
schema: schema,
cloudKitDatabase: .private("iCloud.com.yourcompany.yourapp")
)
do {
container = try ModelContainer(
for: schema,
configurations: cloudConfig
)
} catch {
fatalError("Failed to create ModelContainer: \(error)")
}
}
}Multiple Configurations
// Separate local and synced stores
let localConfig = ModelConfiguration(
"Local",
schema: Schema([CachedData.self]),
cloudKitDatabase: .none
)
let syncedConfig = ModelConfiguration(
"Synced",
schema: Schema([UserDocument.self]),
cloudKitDatabase: .private("iCloud.com.yourapp")
)
let container = try ModelContainer(
for: Schema([CachedData.self, UserDocument.self]),
configurations: localConfig, syncedConfig
)Querying Data
Using @Query
struct ContentView: View {
// Simple query
@Query private var items: [Item]
// With sorting
@Query(sort: \Item.timestamp, order: .reverse)
private var sortedItems: [Item]
// With predicate
@Query(filter: #Predicate<Item> { $0.isCompleted == false })
private var pendingItems: [Item]
// With both
@Query(
filter: #Predicate<Item> { $0.isCompleted == false },
sort: \Item.timestamp,
order: .reverse
)
private var pendingSortedItems: [Item]
// With animation
@Query(sort: \Item.timestamp, animation: .default)
private var animatedItems: [Item]
}Dynamic Queries
struct FilteredItemsView: View {
@Query private var items: [Item]
let showCompleted: Bool
init(showCompleted: Bool) {
self.showCompleted = showCompleted
let predicate: Predicate<Item>? = showCompleted
? nil
: #Predicate { $0.isCompleted == false }
_items = Query(
filter: predicate,
sort: \Item.timestamp,
order: .reverse
)
}
var body: some View {
List(items) { item in
ItemRow(item: item)
}
}
}Fetch Descriptors
func fetchRecentItems(context: ModelContext) throws -> [Item] {
let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: .now)!
let descriptor = FetchDescriptor<Item>(
predicate: #Predicate { $0.timestamp > oneWeekAgo },
sortBy: [SortDescriptor(\.timestamp, order: .reverse)]
)
return try context.fetch(descriptor)
}
// With pagination
func fetchItemsPage(page: Int, pageSize: Int, context: ModelContext) throws -> [Item] {
var descriptor = FetchDescriptor<Item>(
sortBy: [SortDescriptor(\.timestamp, order: .reverse)]
)
descriptor.fetchOffset = page * pageSize
descriptor.fetchLimit = pageSize
return try context.fetch(descriptor)
}iCloud Sync Patterns
Sync Status Monitoring
@Observable
final class SyncStatus {
static let shared = SyncStatus()
private(set) var isSyncing = false
private(set) var lastSyncDate: Date?
private(set) var error: Error?
private init() {
// Monitor CloudKit account status
NotificationCenter.default.addObserver(
forName: .CKAccountChanged,
object: nil,
queue: .main
) { [weak self] _ in
Task { @MainActor in
await self?.checkAccountStatus()
}
}
}
@MainActor
func checkAccountStatus() async {
do {
let status = try await CKContainer.default().accountStatus()
// Handle status changes
} catch {
self.error = error
}
}
}Conflict Resolution
SwiftData with CloudKit uses "last writer wins" by default. For custom resolution:
@Model
final class Document {
var content: String
var lastModified: Date
var deviceID: String // Track which device modified
// Version tracking for conflict detection
var version: Int
func merge(with other: Document) -> Document {
// Custom merge logic
if self.lastModified > other.lastModified {
return self
} else {
return other
}
}
}Handling Offline Mode
@Observable
final class DataManager {
private let modelContext: ModelContext
private let networkMonitor = NWPathMonitor()
var isOnline = true
init(modelContext: ModelContext) {
self.modelContext = modelContext
networkMonitor.pathUpdateHandler = { [weak self] path in
Task { @MainActor in
self?.isOnline = path.status == .satisfied
}
}
networkMonitor.start(queue: .global())
}
func saveItem(_ item: Item) throws {
modelContext.insert(item)
try modelContext.save()
// SwiftData automatically syncs when online
// No manual sync needed
}
}Migration Strategies
Lightweight Migration (Automatic)
SwiftData handles these automatically:
- Adding new properties (with defaults)
- Removing properties
- Renaming models/properties (with
@Attribute(originalName:))
@Model
final class Item {
var title: String
// Renamed from "done" to "isCompleted"
@Attribute(originalName: "done")
var isCompleted: Bool
// New property with default
var priority: Int = 0
}Versioned Schemas
enum ItemSchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[ItemV1.self]
}
@Model
final class ItemV1 {
var title: String
}
}
enum ItemSchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[Item.self] // Current model
}
}
// Migration plan
enum ItemMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[ItemSchemaV1.self, ItemSchemaV2.self]
}
static var stages: [MigrationStage] {
[migrateV1toV2]
}
static let migrateV1toV2 = MigrationStage.lightweight(
fromVersion: ItemSchemaV1.self,
toVersion: ItemSchemaV2.self
)
}Performance Best Practices
Batch Operations
func batchInsert(items: [Item], context: ModelContext) throws {
for item in items {
context.insert(item)
}
// Single save for all inserts
try context.save()
}
func batchDelete(predicate: Predicate<Item>, context: ModelContext) throws {
try context.delete(model: Item.self, where: predicate)
}Prefetching Relationships
var descriptor = FetchDescriptor<Project>()
descriptor.relationshipKeyPathsForPrefetching = [\Project.items]
let projects = try context.fetch(descriptor)
// items are already loaded, no additional fetches neededBackground Processing
actor BackgroundPersistence {
private let container: ModelContainer
init(container: ModelContainer) {
self.container = container
}
func importData(_ data: [ImportItem]) async throws {
let context = ModelContext(container)
for item in data {
let newItem = Item(title: item.title)
context.insert(newItem)
}
try context.save()
}
}Class Inheritance (iOS 17+)
SwiftData supports class inheritance for hierarchical models.
When to Use Inheritance
Good use cases:
- Clear "IS-A" relationship (e.g.,
BusinessTripIS-ATrip) - Models share fundamental properties but diverge for specialization
- Need both deep searches (all properties) and shallow searches (subclass-specific)
Avoid inheritance when:
- Subclasses share only a few properties
- A Boolean flag or enum would suffice
- Protocol conformance is more appropriate
Inheritance Example
@Model
class Trip {
var name: String
var destination: String
var startDate: Date
var endDate: Date
init(name: String, destination: String, startDate: Date, endDate: Date) {
self.name = name
self.destination = destination
self.startDate = startDate
self.endDate = endDate
}
}
@Model
class BusinessTrip: Trip {
var purpose: String
var expenseCode: String
init(name: String, destination: String, startDate: Date, endDate: Date,
purpose: String, expenseCode: String) {
self.purpose = purpose
self.expenseCode = expenseCode
super.init(name: name, destination: destination, startDate: startDate, endDate: endDate)
}
}
@Model
class PersonalTrip: Trip {
var reason: String
init(name: String, destination: String, startDate: Date, endDate: Date, reason: String) {
self.reason = reason
super.init(name: name, destination: destination, startDate: startDate, endDate: endDate)
}
}Type-Based Queries
// Query all trips (includes subclasses)
@Query(sort: \Trip.startDate)
var allTrips: [Trip]
// Query specific subclass using type predicate
let businessTripPredicate = #Predicate<Trip> { $0 is BusinessTrip }
@Query(filter: businessTripPredicate)
var businessTrips: [Trip]
// Combined filtering with subclass properties
let vacationPredicate = #Predicate<Trip> {
if let personal = $0 as? PersonalTrip {
return personal.reason == "vacation"
}
return false
}Polymorphic Relationships
@Model
class TravelPlanner {
var name: String
@Relationship(deleteRule: .cascade)
var trips: [Trip] = [] // Can contain BusinessTrip and PersonalTrip
}Testing
In-Memory Container
@MainActor
func makePreviewContainer() -> ModelContainer {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try! ModelContainer(for: Item.self, configurations: config)
// Add sample data
let context = container.mainContext
context.insert(Item(title: "Sample Item"))
try! context.save()
return container
}
#Preview {
ContentView()
.modelContainer(makePreviewContainer())
}Mock Repository
final class MockItemRepository: Repository {
typealias T = Item
var items: [Item] = []
var insertCalled = false
var deleteCalled = false
func fetch(predicate: Predicate<Item>?, sortBy: [SortDescriptor<Item>]) async throws -> [Item] {
items
}
func fetchOne(predicate: Predicate<Item>) async throws -> Item? {
items.first
}
func insert(_ item: Item) async throws {
insertCalled = true
items.append(item)
}
func delete(_ item: Item) async throws {
deleteCalled = true
items.removeAll { $0.id == item.id }
}
func save() async throws {}
}import Foundation
import SwiftData
import CloudKit
/// CloudKit configuration for iCloud sync.
///
/// Prerequisites:
/// 1. Enable iCloud capability in Xcode
/// 2. Check "CloudKit" service
/// 3. Add container identifier
/// 4. Add required entitlements
///
/// Usage:
/// ```swift
/// let container = try CloudKitConfiguration.createContainer(
/// for: [Item.self],
/// containerIdentifier: "iCloud.com.yourcompany.yourapp"
/// )
/// ```
enum CloudKitConfiguration {
// MARK: - Container Identifier
/// Your CloudKit container identifier.
/// Format: iCloud.com.yourcompany.yourapp
///
/// Must match:
/// - Xcode capability configuration
/// - Entitlements file
/// - CloudKit Dashboard
static let containerIdentifier = "iCloud.com.yourcompany.yourapp"
// MARK: - Container Creation
/// Create a ModelContainer with CloudKit sync enabled.
@MainActor
static func createContainer(
for modelTypes: [any PersistentModel.Type],
containerIdentifier: String = CloudKitConfiguration.containerIdentifier
) throws -> ModelContainer {
let schema = Schema(modelTypes)
let configuration = ModelConfiguration(
schema: schema,
cloudKitDatabase: .private(containerIdentifier)
)
return try ModelContainer(
for: schema,
configurations: configuration
)
}
/// Create a ModelContainer with automatic CloudKit database selection.
/// Uses private database for user-specific data.
@MainActor
static func createAutoContainer(
for modelTypes: [any PersistentModel.Type]
) throws -> ModelContainer {
let schema = Schema(modelTypes)
let configuration = ModelConfiguration(
schema: schema,
cloudKitDatabase: .automatic
)
return try ModelContainer(
for: schema,
configurations: configuration
)
}
// MARK: - Account Status
/// Check if the user is signed into iCloud.
static func checkAccountStatus() async throws -> CKAccountStatus {
try await CKContainer.default().accountStatus()
}
/// Check if CloudKit is available for this user.
static func isCloudKitAvailable() async -> Bool {
do {
let status = try await checkAccountStatus()
return status == .available
} catch {
return false
}
}
}
// MARK: - Container Identifier Validation
extension CloudKitConfiguration {
/// Validate that the container identifier is properly formatted.
static func validateContainerIdentifier(_ identifier: String) -> Bool {
// Must start with "iCloud."
guard identifier.hasPrefix("iCloud.") else { return false }
// Must have a valid bundle ID format after "iCloud."
let bundleIDPart = String(identifier.dropFirst("iCloud.".count))
let bundleIDRegex = #"^[a-zA-Z][a-zA-Z0-9\-\.]*[a-zA-Z0-9]$"#
return bundleIDPart.range(of: bundleIDRegex, options: .regularExpression) != nil
}
}
// MARK: - Entitlements Reference
/*
Required entitlements for iCloud sync:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.com.yourcompany.yourapp</string>
</array>
<key>com.apple.developer.icloud-services</key>
<array>
<string>CloudKit</string>
</array>
</dict>
</plist>
```
For background sync, also add to Info.plist:
```xml
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
```
*/
import Foundation
import SwiftData
/// Example SwiftData model.
///
/// This demonstrates common patterns for SwiftData models.
/// Replace or extend with your own models.
///
/// Usage:
/// ```swift
/// let item = Item(title: "Buy groceries")
/// item.isCompleted = true
/// ```
@Model
final class Item {
// MARK: - Stored Properties
/// Unique identifier for the item.
@Attribute(.unique)
var id: UUID
/// The item's title.
var title: String
/// When the item was created.
var createdAt: Date
/// When the item was last modified.
var modifiedAt: Date
/// Whether the item is completed.
var isCompleted: Bool
/// Optional notes for the item.
var notes: String?
/// Priority level (0 = none, 1 = low, 2 = medium, 3 = high).
var priority: Int
// MARK: - Transient Properties (Not Persisted)
/// UI selection state (not saved to database).
@Transient
var isSelected: Bool = false
// MARK: - Relationships
// Example: Uncomment to add a relationship to a Project model
// @Relationship(inverse: \Project.items)
// var project: Project?
// Example: Tags relationship
// @Relationship(deleteRule: .nullify)
// var tags: [Tag] = []
// MARK: - Initialization
init(
title: String,
notes: String? = nil,
priority: Int = 0,
isCompleted: Bool = false
) {
self.id = UUID()
self.title = title
self.createdAt = .now
self.modifiedAt = .now
self.isCompleted = isCompleted
self.notes = notes
self.priority = priority
}
// MARK: - Methods
/// Mark the item as completed.
func complete() {
isCompleted = true
modifiedAt = .now
}
/// Mark the item as not completed.
func uncomplete() {
isCompleted = false
modifiedAt = .now
}
/// Toggle completion status.
func toggleCompletion() {
isCompleted.toggle()
modifiedAt = .now
}
/// Update the title.
func updateTitle(_ newTitle: String) {
title = newTitle
modifiedAt = .now
}
}
// MARK: - Convenience Extensions
extension Item {
/// Priority level as human-readable string.
var priorityLabel: String {
switch priority {
case 0: return "None"
case 1: return "Low"
case 2: return "Medium"
case 3: return "High"
default: return "Unknown"
}
}
/// Whether this is a high-priority item.
var isHighPriority: Bool {
priority >= 3
}
}
// MARK: - Sample Data
extension Item {
/// Sample items for previews and testing.
static var sampleItems: [Item] {
[
Item(title: "Complete project proposal", priority: 3),
Item(title: "Review pull request", priority: 2),
Item(title: "Update documentation", priority: 1),
Item(title: "Schedule team meeting", notes: "Discuss Q4 goals"),
Item(title: "Buy coffee", isCompleted: true),
]
}
}
import Foundation
import SwiftData
/// Central persistence controller managing the SwiftData container.
///
/// Usage:
/// ```swift
/// @main
/// struct MyApp: App {
/// var body: some Scene {
/// WindowGroup {
/// ContentView()
/// .modelContainer(PersistenceController.shared.container)
/// }
/// }
/// }
/// ```
@MainActor
final class PersistenceController: Sendable {
// MARK: - Singleton
static let shared = PersistenceController()
// MARK: - Container
/// The main model container.
let container: ModelContainer
// MARK: - Initialization
private init() {
// Define schema with all model types
let schema = Schema([
Item.self,
// Add more models here as your app grows
])
// Configure storage
// For iCloud sync, use CloudKitConfiguration instead
let configuration = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: false,
allowsSave: true
)
do {
container = try ModelContainer(
for: schema,
configurations: configuration
)
} catch {
fatalError("Failed to create ModelContainer: \(error)")
}
}
// MARK: - Preview Container
/// In-memory container for SwiftUI previews.
static var preview: ModelContainer {
let schema = Schema([Item.self])
let configuration = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: true
)
do {
let container = try ModelContainer(
for: schema,
configurations: configuration
)
// Add sample data for previews
let context = container.mainContext
let sampleItems = [
Item(title: "First Item"),
Item(title: "Second Item"),
Item(title: "Third Item"),
]
for item in sampleItems {
context.insert(item)
}
try context.save()
return container
} catch {
fatalError("Failed to create preview container: \(error)")
}
}
}
// MARK: - Convenience Extensions
extension PersistenceController {
/// Main context for UI operations.
var mainContext: ModelContext {
container.mainContext
}
/// Create a new background context for heavy operations.
func newBackgroundContext() -> ModelContext {
ModelContext(container)
}
}
import Foundation
import SwiftData
/// Generic repository protocol for data access.
///
/// Provides abstraction over SwiftData for:
/// - Testability (mock implementations)
/// - Separation of concerns
/// - Consistent data access patterns
///
/// Usage:
/// ```swift
/// let repository: any Repository<Item> = SwiftDataRepository(modelContext: context)
/// let items = try await repository.fetch(sortBy: [SortDescriptor(\.timestamp)])
/// ```
protocol Repository<T>: Sendable {
associatedtype T: PersistentModel
/// Fetch all items matching the predicate.
func fetch(
predicate: Predicate<T>?,
sortBy: [SortDescriptor<T>]
) async throws -> [T]
/// Fetch a single item matching the predicate.
func fetchOne(predicate: Predicate<T>) async throws -> T?
/// Count items matching the predicate.
func count(predicate: Predicate<T>?) async throws -> Int
/// Insert a new item.
func insert(_ item: T) async throws
/// Delete an item.
func delete(_ item: T) async throws
/// Delete all items matching the predicate.
func deleteAll(predicate: Predicate<T>?) async throws
/// Save pending changes.
func save() async throws
}
// MARK: - Default Implementations
extension Repository {
/// Fetch all items with default sorting.
func fetch(sortBy: [SortDescriptor<T>] = []) async throws -> [T] {
try await fetch(predicate: nil, sortBy: sortBy)
}
/// Fetch all items.
func fetchAll() async throws -> [T] {
try await fetch(predicate: nil, sortBy: [])
}
/// Count all items.
func countAll() async throws -> Int {
try await count(predicate: nil)
}
/// Delete all items.
func deleteAll() async throws {
try await deleteAll(predicate: nil)
}
}
// MARK: - Repository Error
/// Errors that can occur during repository operations.
enum RepositoryError: Error, LocalizedError {
case fetchFailed(Error)
case insertFailed(Error)
case deleteFailed(Error)
case saveFailed(Error)
case notFound
var errorDescription: String? {
switch self {
case .fetchFailed(let error):
return "Failed to fetch data: \(error.localizedDescription)"
case .insertFailed(let error):
return "Failed to insert data: \(error.localizedDescription)"
case .deleteFailed(let error):
return "Failed to delete data: \(error.localizedDescription)"
case .saveFailed(let error):
return "Failed to save data: \(error.localizedDescription)"
case .notFound:
return "Item not found"
}
}
}
import Foundation
import SwiftData
/// SwiftData implementation of the Repository protocol.
///
/// Usage:
/// ```swift
/// let context = PersistenceController.shared.mainContext
/// let repository = SwiftDataRepository<Item>(modelContext: context)
///
/// // Fetch items
/// let items = try await repository.fetch(
/// predicate: #Predicate { $0.isCompleted == false },
/// sortBy: [SortDescriptor(\.timestamp, order: .reverse)]
/// )
///
/// // Insert
/// let newItem = Item(title: "New Task")
/// try await repository.insert(newItem)
/// ```
@MainActor
final class SwiftDataRepository<T: PersistentModel>: Repository {
// MARK: - Properties
private let modelContext: ModelContext
// MARK: - Initialization
init(modelContext: ModelContext) {
self.modelContext = modelContext
}
// MARK: - Fetch
func fetch(
predicate: Predicate<T>? = nil,
sortBy: [SortDescriptor<T>] = []
) async throws -> [T] {
let descriptor = FetchDescriptor<T>(
predicate: predicate,
sortBy: sortBy
)
do {
return try modelContext.fetch(descriptor)
} catch {
throw RepositoryError.fetchFailed(error)
}
}
func fetchOne(predicate: Predicate<T>) async throws -> T? {
var descriptor = FetchDescriptor<T>(predicate: predicate)
descriptor.fetchLimit = 1
do {
return try modelContext.fetch(descriptor).first
} catch {
throw RepositoryError.fetchFailed(error)
}
}
// MARK: - Count
func count(predicate: Predicate<T>? = nil) async throws -> Int {
let descriptor = FetchDescriptor<T>(predicate: predicate)
do {
return try modelContext.fetchCount(descriptor)
} catch {
throw RepositoryError.fetchFailed(error)
}
}
// MARK: - Insert
func insert(_ item: T) async throws {
modelContext.insert(item)
try await save()
}
// MARK: - Delete
func delete(_ item: T) async throws {
modelContext.delete(item)
try await save()
}
func deleteAll(predicate: Predicate<T>? = nil) async throws {
do {
try modelContext.delete(model: T.self, where: predicate)
try await save()
} catch {
throw RepositoryError.deleteFailed(error)
}
}
// MARK: - Save
func save() async throws {
guard modelContext.hasChanges else { return }
do {
try modelContext.save()
} catch {
throw RepositoryError.saveFailed(error)
}
}
}
// MARK: - Pagination Support
extension SwiftDataRepository {
/// Fetch items with pagination.
func fetchPage(
page: Int,
pageSize: Int,
predicate: Predicate<T>? = nil,
sortBy: [SortDescriptor<T>] = []
) async throws -> [T] {
var descriptor = FetchDescriptor<T>(
predicate: predicate,
sortBy: sortBy
)
descriptor.fetchOffset = page * pageSize
descriptor.fetchLimit = pageSize
do {
return try modelContext.fetch(descriptor)
} catch {
throw RepositoryError.fetchFailed(error)
}
}
}
// MARK: - Batch Operations
extension SwiftDataRepository {
/// Insert multiple items efficiently.
func insertBatch(_ items: [T]) async throws {
for item in items {
modelContext.insert(item)
}
try await save()
}
/// Update items matching predicate using a transform.
func updateBatch(
predicate: Predicate<T>? = nil,
transform: (T) -> Void
) async throws {
let items = try await fetch(predicate: predicate, sortBy: [])
for item in items {
transform(item)
}
try await save()
}
}
import Foundation
import SwiftUI
import CloudKit
/// Monitors iCloud sync status for UI feedback.
///
/// Usage:
/// ```swift
/// struct ContentView: View {
/// @Environment(SyncStatus.self) var syncStatus
///
/// var body: some View {
/// VStack {
/// if syncStatus.isSyncing {
/// ProgressView("Syncing...")
/// }
///
/// if let error = syncStatus.error {
/// Text("Sync error: \(error.localizedDescription)")
/// }
/// }
/// }
/// }
/// ```
@MainActor
@Observable
final class SyncStatus {
// MARK: - Singleton
static let shared = SyncStatus()
// MARK: - State
/// Whether a sync operation is in progress.
private(set) var isSyncing = false
/// The last time sync completed successfully.
private(set) var lastSyncDate: Date?
/// Current iCloud account status.
private(set) var accountStatus: CKAccountStatus = .couldNotDetermine
/// Any error from the last sync attempt.
private(set) var error: SyncError?
/// Whether iCloud is available for this user.
var isCloudAvailable: Bool {
accountStatus == .available
}
/// Human-readable status description.
var statusDescription: String {
if let error {
return error.localizedDescription
}
switch accountStatus {
case .available:
if isSyncing {
return "Syncing..."
} else if let lastSync = lastSyncDate {
return "Last synced: \(lastSync.formatted(.relative(presentation: .named)))"
} else {
return "iCloud connected"
}
case .noAccount:
return "Sign in to iCloud to sync"
case .restricted:
return "iCloud access restricted"
case .couldNotDetermine:
return "Checking iCloud status..."
case .temporarilyUnavailable:
return "iCloud temporarily unavailable"
@unknown default:
return "Unknown status"
}
}
// MARK: - Initialization
private init() {
startMonitoring()
}
// MARK: - Monitoring
private func startMonitoring() {
// Check initial status
Task {
await checkAccountStatus()
}
// Monitor account changes
NotificationCenter.default.addObserver(
forName: .CKAccountChanged,
object: nil,
queue: .main
) { [weak self] _ in
Task { @MainActor in
await self?.checkAccountStatus()
}
}
}
/// Check the current iCloud account status.
func checkAccountStatus() async {
do {
accountStatus = try await CKContainer.default().accountStatus()
error = nil
} catch {
self.error = .accountCheckFailed(error)
}
}
// MARK: - Sync Operations
/// Mark sync as started.
func syncStarted() {
isSyncing = true
error = nil
}
/// Mark sync as completed successfully.
func syncCompleted() {
isSyncing = false
lastSyncDate = .now
error = nil
}
/// Mark sync as failed.
func syncFailed(_ error: Error) {
isSyncing = false
self.error = .syncFailed(error)
}
}
// MARK: - Sync Error
/// Errors related to iCloud sync.
enum SyncError: Error, LocalizedError {
case accountCheckFailed(Error)
case syncFailed(Error)
case networkUnavailable
case quotaExceeded
var errorDescription: String? {
switch self {
case .accountCheckFailed(let error):
return "Could not check iCloud status: \(error.localizedDescription)"
case .syncFailed(let error):
return "Sync failed: \(error.localizedDescription)"
case .networkUnavailable:
return "Network unavailable. Changes will sync when connected."
case .quotaExceeded:
return "iCloud storage quota exceeded"
}
}
}
// MARK: - Environment Key
private struct SyncStatusKey: EnvironmentKey {
@MainActor
static let defaultValue = SyncStatus.shared
}
extension EnvironmentValues {
var syncStatus: SyncStatus {
get { self[SyncStatusKey.self] }
set { self[SyncStatusKey.self] = newValue }
}
}
// MARK: - SwiftUI View Extension
extension View {
/// Add a sync status indicator to the view.
func syncStatusIndicator() -> some View {
modifier(SyncStatusIndicatorModifier())
}
}
private struct SyncStatusIndicatorModifier: ViewModifier {
@Environment(SyncStatus.self) private var syncStatus
func body(content: Content) -> some View {
content
.overlay(alignment: .topTrailing) {
if syncStatus.isSyncing {
ProgressView()
.padding(8)
}
}
}
}
// MARK: - Preview
#Preview {
VStack(spacing: 20) {
Text("Sync Status Demo")
.font(.headline)
Text(SyncStatus.shared.statusDescription)
.foregroundStyle(.secondary)
if SyncStatus.shared.isSyncing {
ProgressView("Syncing...")
}
Button("Check Status") {
Task {
await SyncStatus.shared.checkAccountStatus()
}
}
}
.padding()
}