Create and use Boutique Store for Swift data persistence, including initialization, @Stored controllers, CRUD operations, operation chaining, and granular event monitoring. Use when persisting arrays of items, building data controllers, or working with Boutique's Store type.
Create and use Boutique Store for Swift data persistence, including initialization, @Stored controllers, CRUD operations, operation chaining, and granular event monitoring. Use when persisting arrays of items, building data controllers, or working with Boutique's Store type.
Boutique Store
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.
Prerequisites
Boutique added as a dependency via Swift Package Manager.
Models conform to Codable, Sendable, and Identifiable (recommended).
iOS 17+ / macOS 14+ deployment target.
Swift 6.2+ (Boutique uses @MainActor default isolation).
Item Requirements
All items stored in a Store must conform to StorableItem, which is a typealias for Codable & Sendable.
structNote: Codable, Sendable, Identifiable {
let id: Stringlet text: Stringlet createdAt: Date
}
Creating a Store
Shortest form (Identifiable with String ID)
When your item conforms to Identifiable with , the is inferred automatically.
ID == String
cacheIdentifier
let store =Store<Note>(
storage: SQLiteStorageEngine.default(appendingPath: "Notes")
)
Identifiable with UUID ID
When ID == UUID, the store automatically converts to a string identifier.
structPhoto: Codable, Sendable, Identifiable {
let id: UUIDlet url: URL
}
let store =Store<Photo>(
storage: SQLiteStorageEngine.default(appendingPath: "Photos")
)
Custom cache identifier
For items that are not Identifiable or need a custom key, provide a KeyPath<Item, String>.
structBookmark: Codable, Sendable {
let url: URLlet title: String
}
let store =Store<Bookmark>(
storage: SQLiteStorageEngine.default(appendingPath: "Bookmarks"),
cacheIdentifier: \.url.absoluteString
)
Custom storage directory
let store =Store<Note>(
storage: SQLiteStorageEngine(directory: .documents(appendingPath: "Notes"))!
)
Async initialization (items loaded before returning)
let store =tryawaitStore<Note>(
storage: SQLiteStorageEngine.default(appendingPath: "Notes")
)
// store.items is already populated here
Waiting for items to load after sync init
let store =Store<Note>(
storage: SQLiteStorageEngine.default(appendingPath: "Notes")
)
// Later, when you need items to be ready:tryawait store.itemsHaveLoaded()
let notes = store.items
CRUD Operations
Insert
// Single itemtryawait store.insert(note)
// Multiple items (preferred over calling insert in a loop)tryawait store.insert([note1, note2, note3])
Inserting an item with the same cacheIdentifier as an existing item replaces it. The Store handles uniqueness automatically.
Remove
// Single itemtryawait store.remove(note)
// Multiple itemstryawait store.remove([note1, note2])
// All itemstryawait store.removeAll()
Read
let allNotes = store.items // [Note]
Operation Chaining
Chain multiple operations into a single batch to avoid multiple @MainActor dispatches. This prevents flickering in SwiftUI.
// Clear stale cache and insert fresh datatryawait store
.removeAll()
.insert(freshNotes)
.run()
// Remove specific items and insert new onestryawait 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.
Building @Observable Controllers with @Stored
The @Stored property wrapper connects a Store to an @Observable class, exposing items as a plain [Item] array and projecting the underlying Store via $.
self.notes gives you the [Note] array (the wrappedValue).
self.$notes gives you the Store<Note> (the projectedValue) for calling insert, remove, removeAll.
Always mark @Stored with @ObservationIgnored inside @Observable classes to prevent duplicate observation tracking.
Inject the Store via init for testability.
Creating the store and controller
extensionStorewhereItem==Note {
staticlet notesStore =Store<Note>(
storage: SQLiteStorageEngine.default(appendingPath: "Notes")
)
}
// At your app's entry point or in a DI containerlet notesController =NotesController(store: .notesStore)
Granular Event Monitoring
The events property provides an AsyncStream<StoreEvent<Item>> for observing specific operations.
funcmonitorNotesEvents() async {
forawait 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)
}
}
}