npx claudepluginhub mergesort/boutique --plugin boutiqueThis skill uses the workspace's default tool permissions.
Use this skill when you need to persist arrays of items using Boutique's `Store`, build `@Observable` data controllers with `@Stored`, chain store operations, or monitor granular store events.
Outlines best practices for Boutique with Swift 6 concurrency (@MainActor isolation), @ObservationIgnored on @Observable classes, Sendable conformance, preview store testing, and dependency injection. Use for troubleshooting, Swift 6 migration, or test setup.
Guides iOS storage decisions: structured data (SwiftData/Core Data/SQLite) vs files, local directories (Documents/Caches) vs cloud sync (CloudKit/iCloud).
Sets up Core Data for iOS persistence with stacks, SwiftUI @FetchRequest integration, predicates, background contexts, and SwiftData migration.
Share bugs, ideas, or general feedback.
Use this skill when you need to persist arrays of items using Boutique's Store, build @Observable data controllers with @Stored, chain store operations, or monitor granular store events.
Codable, Sendable, and Identifiable (recommended).@MainActor default isolation).All items stored in a Store must conform to StorableItem, which is a typealias for Codable & Sendable.
struct Note: Codable, Sendable, Identifiable {
let id: String
let text: String
let createdAt: Date
}
When your item conforms to Identifiable with ID == String, the cacheIdentifier is inferred automatically.
let store = Store<Note>(
storage: SQLiteStorageEngine.default(appendingPath: "Notes")
)
When ID == UUID, the store automatically converts to a string identifier.
struct Photo: Codable, Sendable, Identifiable {
let id: UUID
let url: URL
}
let store = Store<Photo>(
storage: SQLiteStorageEngine.default(appendingPath: "Photos")
)
For items that are not Identifiable or need a custom key, provide a KeyPath<Item, String>.
struct Bookmark: Codable, Sendable {
let url: URL
let title: String
}
let store = Store<Bookmark>(
storage: SQLiteStorageEngine.default(appendingPath: "Bookmarks"),
cacheIdentifier: \.url.absoluteString
)
let store = Store<Note>(
storage: SQLiteStorageEngine(directory: .documents(appendingPath: "Notes"))!
)
let store = try await Store<Note>(
storage: SQLiteStorageEngine.default(appendingPath: "Notes")
)
// store.items is already populated here
let store = Store<Note>(
storage: SQLiteStorageEngine.default(appendingPath: "Notes")
)
// Later, when you need items to be ready:
try await store.itemsHaveLoaded()
let notes = store.items
// Single item
try await store.insert(note)
// Multiple items (preferred over calling insert in a loop)
try await store.insert([note1, note2, note3])
Inserting an item with the same cacheIdentifier as an existing item replaces it. The Store handles uniqueness automatically.
// Single item
try await store.remove(note)
// Multiple items
try await store.remove([note1, note2])
// All items
try await store.removeAll()
let allNotes = store.items // [Note]
Chain multiple operations into a single batch to avoid multiple @MainActor dispatches. This prevents flickering in SwiftUI.
// Clear stale cache and insert fresh data
try await store
.removeAll()
.insert(freshNotes)
.run()
// Remove specific items and insert new ones
try await store
.remove(outdatedNote)
.insert(updatedNote)
.run()
You must call .run() at the end of a chain. Without it, the operations are created but never executed.
The @Stored property wrapper connects a Store to an @Observable class, exposing items as a plain [Item] array and projecting the underlying Store via $.
@Observable
final class NotesController {
@ObservationIgnored
@Stored var notes: [Note]
init(store: Store<Note>) {
self._notes = Stored(in: store)
}
func fetchNotes() async throws {
let notes = try await self.fetchNotesFromServer()
try await self.$notes.insert(notes)
}
func addNote(_ note: Note) async throws {
try await self.createNoteOnServer(note)
try await self.$notes.insert(note)
}
func removeNote(_ note: Note) async throws {
try await self.deleteNoteOnServer(note)
try await self.$notes.remove(note)
}
func clearAllNotes() async throws {
try await self.deleteAllNotesOnServer()
try await self.$notes.removeAll()
}
}
self.notes gives you the [Note] array (the wrappedValue).self.$notes gives you the Store<Note> (the projectedValue) for calling insert, remove, removeAll.@Stored with @ObservationIgnored inside @Observable classes to prevent duplicate observation tracking.Store via init for testability.extension Store where Item == Note {
static let notesStore = Store<Note>(
storage: SQLiteStorageEngine.default(appendingPath: "Notes")
)
}
// At your app's entry point or in a DI container
let notesController = NotesController(store: .notesStore)
The events property provides an AsyncStream<StoreEvent<Item>> for observing specific operations.
func monitorNotesEvents() async {
for await event in notesController.$notes.events {
switch event.operation {
case .initialized:
print("Store initialized")
case .loaded:
print("Loaded \(event.items.count) notes from disk")
case .insert:
print("Inserted notes:", event.items)
case .remove:
print("Removed notes:", event.items)
}
}
}
| Operation | When it fires | event.items contains |
|---|---|---|
.initialized | Store created, before loading | Empty array |
.loaded | Items loaded from storage engine | All loaded items |
.insert | After insert completes | The newly inserted items |
.remove | After remove/removeAll completes | The removed items |
func refreshNotes() async throws {
let freshNotes = try await self.api.fetchAllNotes()
try await self.$notes
.removeAll()
.insert(freshNotes)
.run()
}
extension Store where Item == Note {
static let notesStore = Store<Note>(
storage: SQLiteStorageEngine.default(appendingPath: "Notes")
)
}
extension Store where Item == Photo {
static let photosStore = Store<Photo>(
storage: SQLiteStorageEngine.default(appendingPath: "Photos")
)
}
Store operations are @MainActor isolated and async throws.OrderedDictionary internally so item order is preserved.insert([items]) over looping insert(item) to batch @MainActor dispatches.boutique-swiftui skill for integrating stores with SwiftUI views.boutique-best-practices skill for testing patterns with Store.previewStore.