Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
71
89%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
Deep reference for custom data stores, history tracking, CloudKit integration, Core Data coexistence, batch operations, complex predicates, composite attributes, model inheritance, multiple containers, undo/redo, and preview patterns.
Implement the DataStore protocol to replace the default SQLite-backed store
with a custom persistence backend (JSON files, in-memory caches, REST APIs,
etc.).
final class JSONStore: DataStore {
typealias Configuration = JSONStoreConfiguration
typealias Snapshot = DefaultSnapshot
let configuration: JSONStoreConfiguration
let identifier: String
let schema: Schema
init(_ configuration: JSONStoreConfiguration,
migrationPlan: (any SchemaMigrationPlan.Type)?) throws {
self.configuration = configuration
self.identifier = configuration.name
self.schema = configuration.schema ?? Schema()
}
func fetch<T: PersistentModel>(
_ request: DataStoreFetchRequest<T>
) throws -> DataStoreFetchResult<T, DefaultSnapshot> {
// Load data from JSON file, apply predicate/sort from request.descriptor
let snapshots: [DefaultSnapshot] = [] // Populate from file
return DataStoreFetchResult(
descriptor: request.descriptor,
fetchedSnapshots: snapshots,
relatedSnapshots: [:]
)
}
func fetchCount<T: PersistentModel>(
_ request: DataStoreFetchRequest<T>
) throws -> Int {
try fetch(request).fetchedSnapshots.count
}
func fetchIdentifiers<T: PersistentModel>(
_ request: DataStoreFetchRequest<T>
) throws -> [PersistentIdentifier] {
try fetch(request).fetchedSnapshots.map(\.persistentIdentifier)
}
func save(
_ request: DataStoreSaveChangesRequest<DefaultSnapshot>
) throws -> DataStoreSaveChangesResult<DefaultSnapshot> {
// Persist inserted, updated; remove deleted
return DataStoreSaveChangesResult(
for: identifier,
remappedIdentifiers: [:],
snapshotsToReregister: [:]
)
}
func erase() throws {
// Remove all persisted data
}
func initializeState(for editingState: EditingState) {}
func invalidateState(for editingState: EditingState) {}
func cachedSnapshots(
for identifiers: [PersistentIdentifier],
editingState: EditingState
) throws -> [PersistentIdentifier: DefaultSnapshot] {
[:]
}
}struct JSONStoreConfiguration: DataStoreConfiguration {
typealias Store = JSONStore
let name: String
var schema: Schema?
let fileURL: URL
init(name: String, fileURL: URL) {
self.name = name
self.fileURL = fileURL
}
func validate() throws {
// Validate file URL is accessible
}
}let config = JSONStoreConfiguration(
name: "JSONStore",
fileURL: URL.documentsDirectory.appending(path: "data.json")
)
let container = try ModelContainer(
for: Trip.self,
configurations: config
)DataStoreBatching: Implement delete(_:) for batch delete support.HistoryProviding: Implement fetchHistory(_:) and deleteHistory(_:)
for change tracking.Handle these when implementing custom stores:
| Case | Meaning |
|---|---|
.invalidPredicate | Predicate cannot be evaluated by the store |
.preferInMemoryFilter | Store cannot filter; framework filters in memory |
.preferInMemorySort | Store cannot sort; framework sorts in memory |
.unsupportedFeature | Store does not support the requested operation |
Set the author property on ModelContext to tag changes with an identifier.
Mark attributes with .preserveValueOnDeletion to retain values in tombstones
after deletion.
@Model
class Trip {
@Attribute(.preserveValueOnDeletion) var name: String
@Attribute(.preserveValueOnDeletion) var destination: String
var startDate: Date
init(name: String, destination: String, startDate: Date) {
self.name = name
self.destination = destination
self.startDate = startDate
}
}
// Tag context for history attribution
modelContext.author = "mainApp"var descriptor = HistoryDescriptor<DefaultHistoryTransaction>()
// Filter by token (only new changes since last check)
if let lastToken = savedToken {
descriptor.predicate = #Predicate<DefaultHistoryTransaction> { transaction in
transaction.token > lastToken
}
}
// iOS 26+: Sort by timestamp
descriptor.sortBy = [SortDescriptor(\.timestamp, order: .reverse)]
let transactions = try modelContext.fetchHistory(descriptor)
for transaction in transactions {
for change in transaction.changes {
switch change {
case .insert(let insert):
let insertedID = insert.changedPersistentIdentifier
// Process new record
case .update(let update):
let updatedID = update.changedPersistentIdentifier
let changedAttributes = update.updatedAttributes
// Process modification
case .delete(let delete):
let deletedID = delete.changedPersistentIdentifier
let tombstone = delete.tombstone
// Access preserved values
if let name = tombstone[\.name] as? String {
// Use preserved name for sync/audit
}
}
}
// Save token for next incremental fetch
savedToken = transaction.token
}let cutoffDate = Calendar.current.date(byAdding: .month, value: -3, to: .now)!
var descriptor = HistoryDescriptor<DefaultHistoryTransaction>()
descriptor.predicate = #Predicate<DefaultHistoryTransaction> { transaction in
transaction.timestamp < cutoffDate
}
try modelContext.deleteHistory(descriptor)| Property | Type | Description |
|---|---|---|
author | String? | The context author that made the change |
changes | [HistoryChange] | Insert, update, delete changes |
storeIdentifier | String | Store that owns the transaction |
timestamp | Date | When the transaction occurred |
token | DefaultHistoryToken | Opaque token for incremental queries |
transactionIdentifier | ... | Unique transaction ID |
bundleIdentifier | String | Bundle that made the change |
processIdentifier | String | Process that made the change |
Use bundleIdentifier and processIdentifier to differentiate changes from
widgets, extensions, or the main app.
for transaction in transactions {
if transaction.author == "widget" {
// Handle widget-originated changes
}
}// Automatic: uses CloudKit entitlement from the app
let autoConfig = ModelConfiguration(
cloudKitDatabase: .automatic
)
// Explicit private database
let privateConfig = ModelConfiguration(
cloudKitDatabase: .private("iCloud.com.example.myapp")
)
// No CloudKit sync
let localConfig = ModelConfiguration(
cloudKitDatabase: .none
)com.apple.developer.icloud-services).ModelConfiguration.@Model
class SyncedNote {
// Use optional properties -- records may arrive partially from other devices
var title: String?
var body: String?
// Encrypt sensitive fields in CloudKit
@Attribute(.allowsCloudEncryption) var secretContent: String?
// Store large data externally
@Attribute(.externalStorage) var attachment: Data?
// Avoid .unique with CloudKit -- CloudKit does not enforce server-side uniqueness
// Use @Attribute(.unique) only for local-only stores
init(title: String? = nil, body: String? = nil) {
self.title = title
self.body = body
}
}@Attribute(.unique) and #Unique on CloudKit-synced models. Use
cloudKitDatabase: .none for local-only stores that need uniqueness..cascade may cause unexpected deletions when sync delivers
partial data. Test thoroughly.let localConfig = ModelConfiguration(
"Local",
schema: Schema([DraftNote.self]),
cloudKitDatabase: .none
)
let syncedConfig = ModelConfiguration(
"Synced",
schema: Schema([PublishedNote.self]),
cloudKitDatabase: .private("iCloud.com.example.app")
)
let container = try ModelContainer(
for: Schema([DraftNote.self, PublishedNote.self]),
configurations: [localConfig, syncedConfig]
)| Strategy | When to Use |
|---|---|
| Pure Core Data | No migration needed; maintain existing stack |
| Full SwiftData | Greenfield app or complete rewrite |
| Coexistence | Gradual migration; both stacks share the same store |
Both stacks read/write the same SQLite file. Critical requirements:
Enable persistent history tracking on the Core Data side:
let description = NSPersistentStoreDescription()
description.setOption(
true as NSNumber,
forKey: NSPersistentHistoryTrackingKey
)Match entity names between Core Data .xcdatamodeld and SwiftData
@Model classes.
Use different class names to avoid conflicts:
// Core Data side
class CDTrip: NSManagedObject { /* ... */ }
// SwiftData side
@Model
class Trip { /* entity name "Trip" matches Core Data entity */ }Point both stacks at the same store URL.
| Scenario | Location |
|---|---|
| Default | Application Support directory |
| App group entitlement | Root of app group container |
| Explicit URL | ModelConfiguration(url: customURL) |
Step-by-step:
VersionedSchema matching the current Core Data model.@Model classes with matching entity/attribute names.SchemaMigrationPlan for future changes.@Query / FetchDescriptor.ModelContext operations.Process large result sets without loading all objects into memory:
try modelContext.enumerate(
FetchDescriptor<Trip>(),
batchSize: 5000,
allowEscapingMutations: false
) { trip in
trip.isProcessed = true
}batchSize: Number of objects loaded per batch (default 5000).allowEscapingMutations: Set to true only if mutations need to persist
beyond the enumeration block.try modelContext.delete(
model: Trip.self,
where: #Predicate { $0.isArchived == true },
includeSubclasses: true // iOS 26+ with inheritance
)When full objects are not needed (e.g., for counting or cross-actor references):
let ids = try modelContext.fetchIdentifiers(FetchDescriptor<Trip>())let count = try modelContext.fetchCount(
FetchDescriptor<Trip>(predicate: #Predicate { $0.isFavorite == true })
)Fetch only specific properties to reduce memory:
var descriptor = FetchDescriptor<Trip>()
descriptor.propertiesToFetch = [\.name, \.startDate]
let trips = try modelContext.fetch(descriptor)Avoid N+1 query problems by prefetching related objects:
var descriptor = FetchDescriptor<Trip>()
descriptor.relationshipKeyPathsForPrefetching = [\.accommodation, \.tags]
let trips = try modelContext.fetch(descriptor)fetchLimit and fetchOffset for pagination.enumerate instead of fetch for processing large datasets.fetchCount when only the count is needed.fetchIdentifiers when only IDs are needed.propertiesToFetch to limit loaded data.@Attribute(.externalStorage) for Data properties over ~100KB.includePendingChanges if unsaved data is not needed in results.modelContext.save() periodically during large imports to flush memory.// Trips with at least one high-priority tag
#Predicate<Trip> { trip in
trip.tags.contains { tag in
tag.priority > 5
}
}
// Trips where all items are packed
#Predicate<Trip> { trip in
trip.packingList.allSatisfy { item in
item.isPacked == true
}
}// Trips with accommodation in a specific city
#Predicate<Trip> { trip in
trip.accommodation?.city == "Paris"
}
// Nil coalescing
#Predicate<Trip> { trip in
(trip.accommodation?.rating ?? 0) >= 4
}// Case-insensitive search
#Predicate<Trip> { trip in
trip.destination.localizedStandardContains(searchText)
}
// Prefix matching
#Predicate<Trip> { trip in
trip.name.starts(with: "Summer")
}let startOfYear = Calendar.current.date(from: DateComponents(year: 2026, month: 1, day: 1))!
let endOfYear = Calendar.current.date(from: DateComponents(year: 2026, month: 12, day: 31))!
#Predicate<Trip> { trip in
trip.startDate >= startOfYear && trip.startDate <= endOfYear
}
// Arithmetic
#Predicate<Trip> { trip in
trip.budget - trip.spent > 100.0
}#Predicate<Trip> { trip in
(trip.isFavorite ? trip.name : trip.destination).localizedStandardContains(searchText)
}Build predicates incrementally using captured variables:
func buildPredicate(
searchText: String,
onlyFavorites: Bool,
minDate: Date?
) -> Predicate<Trip> {
#Predicate<Trip> { trip in
(searchText.isEmpty || trip.name.localizedStandardContains(searchText))
&& (!onlyFavorites || trip.isFavorite == true)
&& (minDate == nil || trip.startDate >= (minDate ?? .distantPast))
}
}// Filter for business trips only
#Predicate<Trip> { trip in
trip is BusinessTrip
}Codable structs stored as composite (nested) attributes in the database.
struct Address: Codable {
var street: String
var city: String
var state: String
var zip: String
}
@Model
class Person {
var name: String
var homeAddress: Address // Stored as composite attribute
var workAddress: Address?
init(name: String, homeAddress: Address) {
self.name = name
self.homeAddress = homeAddress
}
}Composite attributes appear as Schema.CompositeAttribute in the schema.
Sub-properties are stored inline in the same table. Query individual fields
via key-path navigation in #Predicate:
#Predicate<Person> { person in
person.homeAddress.city == "San Francisco"
}@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 PersonalTrip: Trip {
var companion: String?
}
@Model
class BusinessTrip: Trip {
var company: String
var expenseReport: Data?
init(name: String, destination: String, startDate: Date, endDate: Date,
company: String) {
self.company = company
super.init(name: name, destination: destination,
startDate: startDate, endDate: endDate)
}
}// Fetch all trips (includes PersonalTrip and BusinessTrip)
let allTrips = try modelContext.fetch(FetchDescriptor<Trip>())
// Fetch only business trips
let businessTrips = try modelContext.fetch(FetchDescriptor<BusinessTrip>())
// Delete with subclass inclusion
try modelContext.delete(
model: Trip.self,
where: #Predicate { $0.destination == "Cancelled" },
includeSubclasses: true
)Register the base class; subclasses are included automatically:
let container = try ModelContainer(for: Trip.self)
// PersonalTrip and BusinessTrip are included via inheritance// Local-only data (no sync)
let localConfig = ModelConfiguration(
"Local",
schema: Schema([AppSettings.self, CacheEntry.self]),
isStoredInMemoryOnly: false,
cloudKitDatabase: .none
)
// Synced data
let syncConfig = ModelConfiguration(
"Synced",
schema: Schema([UserDocument.self, SharedNote.self]),
cloudKitDatabase: .private("iCloud.com.example.app")
)
let container = try ModelContainer(
for: Schema([AppSettings.self, CacheEntry.self, UserDocument.self, SharedNote.self]),
configurations: [localConfig, syncConfig]
)let bundledURL = Bundle.main.url(forResource: "seed", withExtension: "store")!
let readOnlyConfig = ModelConfiguration(
"SeedData",
schema: Schema([ReferenceItem.self]),
url: bundledURL,
allowsSave: false
)let sharedConfig = ModelConfiguration(
groupContainer: .identifier("group.com.example.myapp")
)
let container = try ModelContainer(for: Trip.self, configurations: sharedConfig)let context = ModelContext(container)
context.undoManager = UndoManager()@main
struct MyApp: App {
let container: ModelContainer
init() {
do {
container = try ModelContainer(for: Trip.self)
container.mainContext.undoManager = UndoManager()
} catch {
fatalError("Failed to create ModelContainer: \(error)")
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}struct TripEditorView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.undoManager) private var undoManager
var body: some View {
VStack {
// ... editing UI ...
}
.toolbar {
ToolbarItemGroup {
Button("Undo") {
modelContext.undoManager?.undo()
}
.disabled(!(modelContext.undoManager?.canUndo ?? false))
Button("Redo") {
modelContext.undoManager?.redo()
}
.disabled(!(modelContext.undoManager?.canRedo ?? false))
}
}
.onAppear {
modelContext.undoManager = undoManager
}
}
}Process pending changes to register undo actions:
modelContext.insert(trip)
modelContext.processPendingChanges()
// Now undo is available for the insertion@MainActor
let previewContainer: ModelContainer = {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try! ModelContainer(for: Trip.self, configurations: config)
// Seed sample data
let sampleTrips = [
Trip(name: "Summer in Paris", destination: "Paris",
startDate: .now, endDate: .now.addingTimeInterval(86400 * 7)),
Trip(name: "Tokyo Adventure", destination: "Tokyo",
startDate: .now.addingTimeInterval(86400 * 30),
endDate: .now.addingTimeInterval(86400 * 37)),
]
for trip in sampleTrips {
container.mainContext.insert(trip)
}
return container
}()
#Preview {
TripListView()
.modelContainer(previewContainer)
}#Preview {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try! ModelContainer(
for: Trip.self, LivingAccommodation.self,
configurations: config
)
let trip = Trip(name: "Beach Trip", destination: "Malibu",
startDate: .now, endDate: .now.addingTimeInterval(86400 * 3))
let hotel = LivingAccommodation(name: "Beach Resort")
trip.accommodation = hotel
container.mainContext.insert(trip)
return TripDetailView(trip: trip)
.modelContainer(container)
}Use PreviewModifier for reusable preview configurations:
struct SampleDataPreview: PreviewModifier {
static func makeSharedContext() async throws -> ModelContainer {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: Trip.self, configurations: config)
// Insert sample data
return container
}
func body(content: Content, context: ModelContainer) -> some View {
content.modelContainer(context)
}
}
extension PreviewTrait where T == Preview.ViewTraits {
static var sampleData: Self = .modifier(SampleDataPreview())
}
#Preview(traits: .sampleData) {
TripListView()
}NotificationCenter.default.publisher(for: ModelContext.didSave, object: modelContext)
.sink { notification in
if let insertedIDs = notification.userInfo?[
ModelContext.NotificationKey.insertedIdentifiers
] as? Set<PersistentIdentifier> {
// Handle new insertions
}
if let updatedIDs = notification.userInfo?[
ModelContext.NotificationKey.updatedIdentifiers
] as? Set<PersistentIdentifier> {
// Handle updates
}
if let deletedIDs = notification.userInfo?[
ModelContext.NotificationKey.deletedIdentifiers
] as? Set<PersistentIdentifier> {
// Handle deletions
}
}| Key | Description |
|---|---|
.insertedIdentifiers | IDs of newly inserted models |
.updatedIdentifiers | IDs of updated models |
.deletedIdentifiers | IDs of deleted models |
.invalidatedAllIdentifiers | All data invalidated (e.g., store reset) |
.queryGeneration | Query generation token |
do {
let trips = try modelContext.fetch(descriptor)
} catch let error as SwiftDataError {
switch error {
case SwiftDataError.unsupportedPredicate:
// Predicate uses unsupported operations
case SwiftDataError.unsupportedSortDescriptor:
// Sort descriptor cannot be processed
case SwiftDataError.modelValidationFailure:
// Model fails validation (e.g., unique constraint)
case SwiftDataError.loadIssueModelContainer:
// Container could not load the store
default:
// Handle other SwiftData errors
}
} catch {
// Handle non-SwiftData errors
}| Category | Errors |
|---|---|
| Fetch | .unsupportedPredicate, .unsupportedSortDescriptor, .unsupportedKeyPath, .includePendingChangesWithBatchSize |
| Configuration | .duplicateConfiguration, .configurationFileNameContainsInvalidCharacters, .configurationSchemaNotFoundInContainerSchema |
| Container | .loadIssueModelContainer |
| Context | .modelValidationFailure, .missingModelContext |
| Migration | .backwardMigration, .unknownSchema |
| History (iOS 18+) | .historyTokenExpired, .invalidTransactionFetchRequest |
skills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references