### Install Boutique Plugin for Claude Code Source: https://github.com/mergesort/boutique/blob/main/README.md Install the Boutique plugin for Claude Code by running the provided commands in the Claude Code environment. Ensure to reload plugins after installation. ```bash /plugin marketplace add mergesort/Boutique /plugin install boutique@boutique /reload-plugins ``` -------------------------------- ### Initialize Store using Default SQLiteStorageEngine Source: https://github.com/mergesort/boutique/blob/main/Sources/Boutique/Documentation.docc/Articles/Using Stores.md A concise way to initialize a Store using the default SQLiteStorageEngine and a specified path. This is the recommended approach for simple setups. ```swift static let store = Store( storage: SQLiteStorageEngine.default(appendingPath: "Items") ) ``` -------------------------------- ### Define Static Stores for Different Items Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-store/SKILL.md Provides examples of defining static `Store` instances for different item types (`Note` and `Photo`), each configured with its own `SQLiteStorageEngine`. ```swift extension Store where Item == Note { static let notesStore = Store( storage: SQLiteStorageEngine.default(appendingPath: "Notes") ) } extension Store where Item == Photo { static let photosStore = Store( storage: SQLiteStorageEngine.default(appendingPath: "Photos") ) } ``` -------------------------------- ### Configure Keychain Service and Group Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-stored-values/SKILL.md Example of configuring a securely stored value with custom keychain service and group identifiers. ```swift @SecurelyStoredValue( key: "authToken", service: KeychainService(value: "com.example.auth"), group: KeychainGroup(value: "group.com.example.shared") ) var authToken ``` -------------------------------- ### Observe Stored Value Changes Asynchronously Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-stored-values/SKILL.md An example of asynchronously observing changes to a stored value using an async sequence. ```swift func monitorAuthState() async { for await token in securityManager.$authToken.values { if let token { print("Authenticated") } else { print("Logged out") } } } ``` -------------------------------- ### Test Usage with In-Memory Store Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-best-practices/SKILL.md Create an in-memory store for test isolation. Use a unique temporary path per test to avoid collisions. This example demonstrates inserting and verifying a note. ```swift @Test func testInsertNote() async throws { let store = Store( storage: SQLiteStorageEngine(directory: .temporary(appendingPath: UUID().uuidString))! ) let controller = NotesController(store: store) try await store.itemsHaveLoaded() let note = Note(id: "1", text: "Test", createdAt: .now) try await controller.addNote(note) #expect(controller.notes.contains(where: { $0.id == note.id })) } ``` -------------------------------- ### Setup @StoredValue Property Wrappers Source: https://github.com/mergesort/boutique/blob/main/README.md Demonstrates how to set up @StoredValue for various data types including booleans, optional dates, enums, and complex Codable objects. These values are persisted in UserDefaults. ```swift @StoredValue(key: "hasHapticsEnabled") var hasHapticsEnabled = false ``` ```swift // You can also store nil values @StoredValue(key: "lastOpenedDate") var lastOpenedDate: Date? = nil ``` ```swift // Enums work as well, as long as it conforms to `Codable` and `Equatable`. @StoredValue(key: "currentTheme") var currentlySelectedTheme = .light ``` ```swift // Complex objects work as well struct UserPreferences: Codable, Equatable { var hasHapticsEnabled: Bool var prefersDarkMode: Bool var prefersWideScreen: Bool var spatialAudioEnabled: Bool } @StoredValue(key: "userPreferences") var preferences = UserPreferences() ``` -------------------------------- ### Observe Store Changes with SwiftUI's onChange Source: https://github.com/mergesort/boutique/blob/main/llms.txt Shows how to subscribe to changes in a Boutique Store's items using SwiftUI's .onChange modifier. This example sorts the items by creation date whenever the store.items observable property changes. ```swift // Since @Store, @StoredValue, and @SecurelyStoredValue are `@Observable`, you can subscribe // to changes in realtime using any of Swift's built-in observability mechanisms. .onChange(of: store.items) { self.items = self.items.sorted(by: { $0.createdAt > $1.createdAt}) }) ``` -------------------------------- ### Struct Model Conformance to StorableItem Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-best-practices/SKILL.md Structs are preferred for models as they automatically get Sendable conformance when all stored properties are Sendable. Ensure conformance to Codable, Sendable, Identifiable, and Equatable. ```swift struct Note: Codable, Sendable, Identifiable, Equatable { let id: String let text: String let createdAt: Date } ``` -------------------------------- ### Initialize NotesController Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-store/SKILL.md Shows how to instantiate `NotesController` by injecting the pre-defined `notesStore`. This pattern aids in testability. ```swift // At your app's entry point or in a DI container let notesController = NotesController(store: .notesStore) ``` -------------------------------- ### Create, Insert, and Remove Items from Store Source: https://github.com/mergesort/boutique/blob/main/README.md Demonstrates the creation of a Store, inserting single items and arrays, removing items, and handling uniqueness. Also shows chaining commands for multiple operations. ```swift let store = Store( storage: SQLiteStorageEngine.default(appendingPath: "Animals"), cacheIdentifier: \.id ) ``` ```swift let redPanda = Animal(id: "red_panda") try await store.insert(redPanda) ``` ```swift try await store.remove(redPanda) ``` ```swift let dog = Animal(id: "dog") let cat = Animal(id: "cat") try await store.insert([dog, cat]) ``` ```swift print(store.items) // Prints [dog, cat] ``` ```swift let secondDog = Animal(id: "dog") try await store.insert(secondDog) print(store.items) // Prints [dog, cat] ``` ```swift store.removeAll() print(store.items) // Prints [] ``` ```swift try await store .insert(dog) .insert(cat) .run() print(store.items) // Prints [dog, cat] ``` ```swift try await store .removeAll() .insert(redPanda) .run() print(store.items) // Prints [redPanda] ``` -------------------------------- ### Store Complex Codable Types with StoredValue Source: https://github.com/mergesort/boutique/blob/main/Sources/Boutique/Documentation.docc/Articles/The @Stored Family Of Property Wrappers.md Persist complex data structures by conforming them to Codable and using StoredValue. This example shows storing a UserPreferences struct. ```swift struct UserPreferences: Codable, Sendable, Equatable { var hasHapticsEnabled: Bool var prefersDarkMode: Bool var prefersWideScreen: Bool var spatialAudioEnabled: Bool } @Observable final class PreferencesManager { @ObservationIgnored @StoredValue(key: "userPreferences") var preferences = UserPreferences() } ``` -------------------------------- ### Create and Use Boutique Store in Swift Source: https://github.com/mergesort/boutique/blob/main/llms.txt Demonstrates creating a Store, inserting, removing, and reading items. The Store automatically handles uniqueness and allows for chaining commands. ```swift // Create a Store ¹ let store = Store( storage: SQLiteStorageEngine.default(appendingPath: "Animals"), cacheIdentifier: \.id ) // Insert an item into the Store ² let redPanda = Animal(id: "red_panda") try await store.insert(redPanda) // Remove an animal from the Store try await store.remove(redPanda) // Insert two more animals to the Store let dog = Animal(id: "dog") let cat = Animal(id: "cat") try await store.insert([dog, cat]) // You can read items directly print(store.items) // Prints [dog, cat] // You also don't have to worry about maintaining uniqueness, the Store handles uniqueness for you let secondDog = Animal(id: "dog") try await store.insert(secondDog) print(store.items) // Prints [dog, cat] // Clear your store by removing all the items at once. store.removeAll() print(store.items) // Prints [] // You can even chain commands together try await store .insert(dog) .insert(cat) .run() print(store.items) // Prints [dog, cat] // This is a good way to clear stale cached data try await store .removeAll() .insert(redPanda) .run() print(store.items) // Prints [redPanda] ``` -------------------------------- ### Preview Store with Identifiable Items Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-swiftui/SKILL.md Demonstrates setting up a preview store with identifiable items for SwiftUI previews. Ensure items conform to `Identifiable`. ```swift #Preview { let store = Store.previewStore(items: [ Note(id: "1", text: "First note", createdAt: .now), Note(id: "2", text: "Second note", createdAt: .now), ]) let controller = NotesController(store: store) NotesListView(notesController: controller) } ``` -------------------------------- ### Initialize Boutique Store in Swift Source: https://github.com/mergesort/boutique/blob/main/llms.txt Shows the basic initialization of a Store for persisting data automatically and exposing it as a Swift array. ```swift Store ``` -------------------------------- ### TextField with @SecurelyStoredValue Binding in SwiftUI Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-swiftui/SKILL.md Handle optional bindings for @SecurelyStoredValue in a SecureField. This example shows how to manage an optional auth token, converting between optional and non-optional string types. ```swift struct TokenView: View { @State var securityManager: SecurityManager var body: some View { let tokenBinding = Binding( get: { self.securityManager.authToken ?? "" }, set: { try? self.securityManager.$authToken.set($0.isEmpty ? nil : $0) } ) SecureField("Auth Token", text: tokenBinding) } } ``` -------------------------------- ### Synchronous Store Initialization with Await Source: https://github.com/mergesort/boutique/blob/main/Sources/Boutique/Documentation.docc/Articles/Using Stores.md Initialize a Store synchronously using `Store(...)` and then use `await store.itemsHaveLoaded()` to wait for all items to be loaded before accessing them. This approach is suitable when immediate data availability is not critical upon initialization. ```swift let store: Store = Store(...) func getItems() async -> [Item] { try await store.itemsHaveLoaded() return await store.items } ``` -------------------------------- ### Initialize Notes Store with SQLiteStorageEngine Source: https://github.com/mergesort/boutique/blob/main/README.md Initializes a `Store` specifically for `Note` objects using `SQLiteStorageEngine` for persistent storage. ```swift extension Store where Item == Note { // Initialize a Store to save our notes into static let notesStore = Store( storage: SQLiteStorageEngine.default(appendingPath: "Notes") ) } ``` -------------------------------- ### Production Usage of Controller with Store Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-best-practices/SKILL.md Instantiate the controller by passing the pre-defined production store. ```swift let controller = NotesController(store: .notesStore) ``` -------------------------------- ### Async Store Initialization Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-store/SKILL.md Initialize a Store asynchronously, ensuring all items are loaded into the cache before the initialization completes. ```swift let store = try await Store( storage: SQLiteStorageEngine.default(appendingPath: "Notes") ) // store.items is already populated here ``` -------------------------------- ### Chaining Store Operations Source: https://github.com/mergesort/boutique/blob/main/Sources/Boutique/Documentation.docc/Articles/Using Stores.md Demonstrates chaining multiple store operations using the `.run()` function. ```APIDOC ## Chaining Operations ### Description Allows chaining of multiple store operations (e.g., `removeAll`, `insert`) and executing them sequentially using the `.run()` function. This is useful for batch updates or clearing and repopulating the store. ### Method ```swift run() ``` ### Parameters None ### Request Example ```swift try await store .removeAll() .insert(coat) .run() ``` ### Response #### Success Response (200) Indicates all chained operations were executed successfully. #### Response Example None explicitly provided, operation is typically side-effect based. ``` -------------------------------- ### Inject Boutique Store into Controller for Dependency Injection Source: https://github.com/mergesort/boutique/blob/main/llms.txt This snippet demonstrates how to decouple a store from the controller by injecting it via initialization. This approach enables better testability and dependency management. ```swift final class ImagesController: ObservableObject { @Stored var images: [RemoteImage] init(store: Store) { self._images = Stored(in: store) } } ``` -------------------------------- ### Initialize Store with Custom Storage Directory Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-store/SKILL.md Specify a custom directory for storing data by initializing the SQLiteStorageEngine with a specific directory path. ```swift let store = Store( storage: SQLiteStorageEngine(directory: .documents(appendingPath: "Notes"))! ) ``` -------------------------------- ### Initialize Store with SQLiteStorageEngine and Cache Identifier Source: https://github.com/mergesort/boutique/blob/main/Sources/Boutique/Documentation.docc/Articles/Using Stores.md Initialize a Store with a specific SQLiteStorageEngine and a KeyPath for cache identification. The storage path is configurable, defaulting to the platform's storage directory. ```swift let store = Store( storage: SQLiteStorageEngine(directory: .defaultStorageDirectory(appendingPath: "Items")), cacheIdentifier: \.id ) ``` -------------------------------- ### Monitor Granular Store Events Source: https://github.com/mergesort/boutique/blob/main/README.md Demonstrates how to use the Granular Events Tracking API to monitor individual store operations like initialization, loading, insertion, and removal. ```swift func monitorNotesStoreEvents() async { for await event in self.notesController.$notes.events { switch event.operation { case .initialized: print("[Store Event: initial] Our Notes Store has initialized") case .loaded: print("[Store Event: loaded] Our Notes Store has loaded with notes", event.notes.map(\.text)) case .insert: print("[Store Event: insert] Our Notes Store inserted notes", event.notes.map(\.text)) case .remove: print("[Store Event: remove] Our Notes Store removed notes", event.notes.map(\.text)) } } } ``` -------------------------------- ### Asynchronous Store Initialization Source: https://github.com/mergesort/boutique/blob/main/Sources/Boutique/Documentation.docc/Articles/Using Stores.md Initializes a Store asynchronously, ensuring all items are loaded before the initializer returns. ```APIDOC ## Asynchronous Initialization ### Description Initializes a ``Store`` asynchronously. The initializer will not return until all items are loaded into the store, ensuring that the `items` property is immediately available and populated. ### Method ```swift init() async throws ``` ### Parameters None ### Request Example ```swift let store: Store init() async throws { store = try await Store(...) // Now the store will have `items` already loaded. let items = await store.items } ``` ### Response #### Success Response (200) Returns an initialized ``Store`` with all items loaded. #### Response Example None explicitly provided. ``` -------------------------------- ### SwiftUI Preview with Preview Store Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-best-practices/SKILL.md Use Store.previewStore(items:) for SwiftUI previews to create in-memory stores with pre-populated data. Preview stores are only available in DEBUG builds and do not persist to disk. ```swift #Preview { let store = Store.previewStore(items: [ Note(id: "1", text: "Preview note", createdAt: .now), ]) NotesListView(notesController: NotesController(store: store)) } ``` -------------------------------- ### Asynchronous Store Initialization Source: https://github.com/mergesort/boutique/blob/main/Sources/Boutique/Documentation.docc/Articles/Using Stores.md Initialize a Store asynchronously using `await Store(...)`. This ensures that all items are loaded and available immediately after initialization, preventing race conditions when accessing the `items` property. ```swift let store: Store init() async throws { store = try await Store(...) // Now the store will have `items` already loaded. let items = await store.items } ``` -------------------------------- ### Set Value Using Keypath Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-stored-values/SKILL.md Demonstrates setting a specific property within a stored value using its keypath. This operation can throw errors. ```swift try $credentials.set(\.accessToken, to: "new-token") ``` -------------------------------- ### Initializing Store with Async or itemsHaveLoaded() Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-best-practices/SKILL.md Accessing `store.items` immediately after initializing a synchronous `Store` may yield empty results because loading occurs in a background task. Use the async initializer or `itemsHaveLoaded()` to ensure data is available. ```swift // Option 1: Async init let store = try await Store(storage: ...) // Option 2: Wait for loading let store = Store(storage: ...) try await store.itemsHaveLoaded() ``` -------------------------------- ### Initialize Store with String ID Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-store/SKILL.md Create a Store instance for items conforming to Identifiable with a String ID. The cache identifier is inferred automatically. ```swift let store = Store( storage: SQLiteStorageEngine.default(appendingPath: "Notes") ) ``` -------------------------------- ### Waiting for Items to Load (Synchronous Initialization) Source: https://github.com/mergesort/boutique/blob/main/Sources/Boutique/Documentation.docc/Articles/Using Stores.md Provides a method to wait for items to load when using a synchronous Store initializer. ```APIDOC ## Wait for Items to Load ### Description When using a synchronous ``Store`` initializer, this method can be called to asynchronously wait until all items have been loaded into the store. After this method completes, the `items` property can be safely accessed. ### Method ```swift itemsHaveLoaded() async throws ``` ### Parameters None ### Request Example ```swift let store: Store = Store(...) func getItems() async -> [Item] { try await store.itemsHaveLoaded() return await store.items } ``` ### Response #### Success Response (200) Indicates that all items in the store have finished loading. #### Response Example None explicitly provided. ``` -------------------------------- ### Initialize Store with Custom Cache Identifier Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-store/SKILL.md Provide a custom cache identifier using a KeyPath for items that are not Identifiable or require a specific string representation. ```swift struct Bookmark: Codable, Sendable { let url: URL let title: String } let store = Store( storage: SQLiteStorageEngine.default(appendingPath: "Bookmarks"), cacheIdentifier: \.url.absoluteString ) ``` -------------------------------- ### Static Store Definition for Production Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-best-practices/SKILL.md Define static store instances for easy access and reuse in production code. ```swift extension Store where Item == Note { static let notesStore = Store( storage: SQLiteStorageEngine.default(appendingPath: "Notes") ) } ``` -------------------------------- ### Add Boutique Plugin for Codex Source: https://github.com/mergesort/boutique/blob/main/README.md Add the Boutique plugin to Codex by executing the given command in the Codex environment. The plugin will appear in the list when you type '/plugins'. ```bash codex plugin marketplace add mergesort/Boutique ``` -------------------------------- ### Initialize Store with SQLiteStorageEngine (Identifiable Model) Source: https://github.com/mergesort/boutique/blob/main/Sources/Boutique/Documentation.docc/Articles/Using Stores.md Initialize a Store when your model conforms to Identifiable, simplifying the initializer by omitting the cacheIdentifier parameter. ```swift let store = Store( storage: SQLiteStorageEngine(directory: .defaultStorageDirectory(appendingPath: "Items")) ) ``` -------------------------------- ### Preview Store with Custom Cache Identifier Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-swiftui/SKILL.md Shows how to configure a preview store with a custom cache identifier. This is useful when your items do not have a default identifiable property. ```swift #Preview { let store = Store.previewStore( items: [Bookmark(url: URL(string: "https://example.com")!, title: "Example")], cacheIdentifier: \.url.absoluteString ) // ... } ``` -------------------------------- ### Refresh Cache from API Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-store/SKILL.md Demonstrates a common pattern for refreshing the local cache by first removing all existing items and then inserting new ones fetched from an API. The operations are chained and executed using `.run()`. ```swift func refreshNotes() async throws { let freshNotes = try await self.api.fetchAllNotes() try await self.$notes .removeAll() .insert(freshNotes) .run() } ``` -------------------------------- ### Structure Preferences with Observable Classes Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-stored-values/SKILL.md Organizes multiple preferences into dedicated Observable classes for better management. ```swift @Observable final class Preferences { var userExperience = UserExperiencePreferences() var notifications = NotificationPreferences() } @Observable final class UserExperiencePreferences { @ObservationIgnored @StoredValue(key: "hasSoundEffectsEnabled") var hasSoundEffectsEnabled = false @ObservationIgnored @StoredValue(key: "hasHapticsEnabled") var hasHapticsEnabled = true } @Observable final class NotificationPreferences { @ObservationIgnored @StoredValue(key: "pushEnabled") var pushEnabled = true @ObservationIgnored @StoredValue(key: "emailDigestEnabled") var emailDigestEnabled = false } ``` -------------------------------- ### NotesController with @Stored Property Wrapper Source: https://github.com/mergesort/boutique/blob/main/Sources/Boutique/Documentation.docc/Articles/The @Stored Family Of Property Wrappers.md Demonstrates a NotesController using @Stored to manage an array of notes, including fetching, adding, removing, and clearing notes while synchronizing with a server and a local Store. ```swift import Boutique @Observable final class NotesController { @ObservationIgnored @Stored var notes: [Note] init(store: Store) { self._notes = Stored(in: store) } func fetchNotesFromAPI() async throws -> [Note] { // This would be an API call we make to our server try await self.fetchAllNotesFromServer() // Insert all of the notes we fetched into the local Store once the request succeeds, to keep the state in sync try await self.$notes.insert(notes) } func addNote(note: Note) async throws { // This would be an API call we make to our server try await self.insertNoteOnServer(note) // Insert our note into the local Store once the request succeeds, to keep the state in sync try await self.$notes.insert(note) } func removeNote(note: Note) async throws { // This would be an API call we make to our server try await self.removeRemoteNoteFromServer(note) // Remove our note from the local Store once the request succeeds, to keep the state in sync try await self.$notes.remove(note) } func clearAllNotes() async throws { // This would be an API call we make to our server try await self.removeAllNotesOnServer() // Remove all notes from the local Store once the request succeeds, to keep the state in sync try await self.$notes.removeAll() } } ``` -------------------------------- ### Wait for Items to Load After Sync Init Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-store/SKILL.md If the store was initialized synchronously, use `itemsHaveLoaded()` to wait for all items to be loaded into the cache before accessing them. ```swift let store = Store( storage: SQLiteStorageEngine.default(appendingPath: "Notes") ) // Later, when you need items to be ready: try await store.itemsHaveLoaded() let notes = store.items ``` -------------------------------- ### Dependency Injection for Production Controller Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-best-practices/SKILL.md Inject Store instances through initializers for better testability. This allows swapping in test stores during testing. ```swift @Observable final class NotesController { @ObservationIgnored @Stored var notes: [Note] init(store: Store) { self._notes = Stored(in: store) } } ``` -------------------------------- ### Read Items from Store Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-store/SKILL.md Access all items currently stored in the Boutique Store. ```swift let allNotes = store.items // [Note] ``` -------------------------------- ### Set, Remove, and Nilify Stored Values Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-stored-values/SKILL.md Demonstrates how to set, remove, or set a stored value to nil using its projected value. Throws on keychain errors. ```swift // Set a value (throws on keychain errors) try $authToken.set("eyJhbGciOiJIUzI1NiIs...") // Remove from keychain try $authToken.remove() // Set to nil (same as remove) try $authToken.set(nil) ``` -------------------------------- ### Add Boutique Dependency with Swift Package Manager Source: https://github.com/mergesort/boutique/blob/main/README.md Add Boutique as a dependency to your Swift package by including the provided URL and version in your Package.swift file. ```swift dependencies: [ .package(url: "https://github.com/mergesort/Boutique.git", .upToNextMajor(from: "1.0.0")) ] ``` -------------------------------- ### Initialize Store with UUID ID Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-store/SKILL.md Initialize a Store for items with a UUID ID. The store automatically converts UUIDs to string identifiers. ```swift struct Photo: Codable, Sendable, Identifiable { let id: UUID let url: URL } let store = Store( storage: SQLiteStorageEngine.default(appendingPath: "Photos") ) ``` -------------------------------- ### Implement Data Controller with @Stored in Swift Source: https://github.com/mergesort/boutique/blob/main/llms.txt Demonstrates a NotesController using the @Stored property wrapper for managing an array of Notes. It includes methods for fetching, adding, removing, and clearing notes, with placeholders for API synchronization. ```swift @Observable final class NotesController { @ObservationIgnored @Stored var notes: [Note] init(store: Store) { self._notes = Stored(in: store) } func fetchNotesFromAPI() async throws -> [Note] { // This would be an API call we make to our server try await self.fetchAllNotesFromServer() // Insert all of the notes we fetched into the local Store once the request succeeds, to keep the state in sync try await self.$notes.insert(notes) } func addNote(note: Note) async throws { // This would be an API call we make to our server try await self.insertNoteOnServer(note) // Insert our note into the local Store once the request succeeds, to keep the state in sync try await self.$notes.insert(note) } func removeNote(note: Note) async throws { // This would be an API call we make to our server try await self.removeRemoteNoteFromServer(note) // Remove our note from the local Store once the request succeeds, to keep the state in sync try await self.$notes.remove(note) } func clearAllNotes() async throws { // This would be an API call we make to our server try await self.removeAllNotesOnServer() // Remove all notes from the local Store once the request succeeds, to keep the state in sync try await self.$notes.removeAll() } } ``` -------------------------------- ### Wait for Store Items to Load in Swift Source: https://github.com/mergesort/boutique/blob/main/llms.txt Ensures all items are loaded after synchronous Store initialization before accessing them. Use `itemsHaveLoaded()` to guarantee data availability. ```swift let store: Store = Store(...) func getItems() async -> [Item] { try await store.itemsHaveLoaded() return await store.items } ``` -------------------------------- ### Waiting for Store to Load with onStoreDidLoad (Callback-based) Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-swiftui/SKILL.md Use onStoreDidLoad with a callback to manage loading states and trigger actions once store items are ready. This is useful when the Store is initialized synchronously and items load in a background task. ```swift struct NotesView: View { @State var notesController: NotesController @State private var isLoaded = false var body: some View { Group { if self.isLoaded { NotesList(notes: self.notesController.notes) } else { ProgressView() } } .onStoreDidLoad(self.notesController.$notes, onLoad: { self.isLoaded = true }, onError: { error in print("Failed to load notes:", error) }) } } ``` -------------------------------- ### Observable App Preferences with Stored Values Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-swiftui/SKILL.md Illustrates creating an observable class for app preferences using `@StoredValue` for persistence. Use bindings to connect these preferences to SwiftUI controls. ```swift @Observable final class AppPreferences { @ObservationIgnored @StoredValue(key: "hasHapticsEnabled") var hasHapticsEnabled = true @ObservationIgnored @StoredValue(key: "prefersDarkMode") var prefersDarkMode = false @ObservationIgnored @StoredValue(key: "fontSize") var fontSize: Double = 16.0 } struct SettingsView: View { @State var preferences: AppPreferences var body: some View { Form { Section("Experience") { Toggle("Haptics", isOn: self.preferences.$hasHapticsEnabled.binding) Toggle("Dark Mode", isOn: self.preferences.$prefersDarkMode.binding) } Section("Display") { Slider( value: self.preferences.$fontSize.binding, in: 12...24, step: 1 ) { Text("Font Size: \(Int(self.preferences.fontSize))") } } } } } ``` -------------------------------- ### Use @StoredValue Property Wrapper for Persistence Source: https://github.com/mergesort/boutique/blob/main/llms.txt Demonstrates the usage of the @StoredValue property wrapper. This wrapper simplifies the persistence of individual Swift values using Boutique's storage mechanisms. ```swift @StoredValue ``` -------------------------------- ### Build @Observable Controller with @Stored Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-store/SKILL.md Use the @Stored property wrapper to connect a Boutique Store to an @Observable class, exposing store items and the underlying store. ```swift @Observable final class NotesController { @ObservationIgnored @Stored var notes: [Note] init(store: Store) { 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() } } ``` -------------------------------- ### Subscribe to Store Changes with @Published in Swift Source: https://github.com/mergesort/boutique/blob/main/llms.txt Shows how to subscribe to changes in a @Published property of a Boutique store using `.sink` for real-time updates and `.onReceive` for integration with SwiftUI, filtering items based on a condition. ```swift // Since items is a @Published property // you can subscribe to any changes in realtime. store.$items.sink({ print("Items was updated", $0) }) // Works great with SwiftUI out the box for more complex pipelines. .onReceive(store.$items, perform: { self.allItems = $0.filter({ $0.id > 100 }) }) ``` -------------------------------- ### Observe Store Changes with .onChange Source: https://github.com/mergesort/boutique/blob/main/Sources/Boutique/Documentation.docc/Articles/Using Stores.md Use SwiftUI's `.onChange` modifier to observe changes to a Store's items. Set `initial: true` to trigger the handler when the view first appears. ```swift struct NotesListView: View { @State var notesController: NotesController @State private var notes: [Note] = [] var body: some View { VStack { ForEach(self.notes) { note in Text(note.text) .onTapGesture { Task { try await notesController.removeNote(note) } } } } .onChange(of: notesController.notes, initial: true) { _, newValue in // We can even create complex pipelines, for example filtering all notes smaller than a tweet self.notes = newValue.filter { $0.length < 280 } } } } ``` -------------------------------- ### Monitor Granular Store Events Source: https://github.com/mergesort/boutique/blob/main/Sources/Boutique/Documentation.docc/Articles/The @Stored Family Of Property Wrappers.md Utilize the Store's `events` AsyncStream to track specific operations like initialization, loading, insertion, or removal of items. ```swift func monitorNotesEvents() async { for await event in notesController.$notes.events { switch event.operation { case .initialized: print("Notes Store has initialized") case .loaded: print("Notes Store has loaded with notes", event.items) case .insert: print("Notes Store inserted notes", event.items) case .remove: print("Notes Store removed notes", event.items) } } } ``` -------------------------------- ### Avoid Performance Pitfalls with Direct Filtering Source: https://github.com/mergesort/boutique/blob/main/Sources/Boutique/Documentation.docc/Articles/The @Stored Family Of Property Wrappers.md Demonstrates a less performant approach to filtering data directly within the `ForEach` loop, which can lead to unnecessary re-renders. ```swift struct NotesListView: View { @State var notesController: NotesController var body: some View { VStack { ForEach(notesController.notes.filter({ $0.length > 280 })) { note in Text(note.text) } } } } ``` -------------------------------- ### Reacting to Store Changes with onChange in SwiftUI Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-swiftui/SKILL.md Use .onChange(of:initial:) to execute code when stored items change. Setting initial: true ensures the closure also fires on the first appearance. ```swift struct NotesListView: View { @State var notesController: NotesController @State private var filteredNotes: [Note] = [] var body: some View { List(self.filteredNotes) { note in Text(note.text) } .onChange(of: self.notesController.notes, initial: true) { _, newValue in self.filteredNotes = newValue.filter({ $0.text.count < 280 }) } } } ``` -------------------------------- ### Cache Notes with @Stored in Swift Source: https://github.com/mergesort/boutique/blob/main/llms.txt Demonstrates using the @Stored property wrapper to cache notes in memory and on disk. Includes fetching notes from an API and performing CRUD operations. ```swift extension Store where Item == Note { // Initialize a Store to save our notes into static let notesStore = Store( storage: SQLiteStorageEngine.default(appendingPath: "Notes") ) } @Observable final class NotesController { /// Creates an @Stored property to handle an in-memory and on-disk cache of notes. ³ @Stored(in: .notesStore) var notes /// Fetches `Notes` from the API, providing the user with a red panda note if the request succeeds. func fetchNotes() async throws -> Note { // Hit the API that provides you a random image's metadata let noteURL = URL(string: "https://notes.redpanda.club/random/json")! let randomNoteRequest = URLRequest(url: noteURL) let (noteResponse, _) = try await URLSession.shared.data(for: randomNoteRequest) return Note(createdAt: .now, url: noteResponse.url, text: noteResponse.text) } /// Saves an note to the `Store` in memory and on disk. ``` -------------------------------- ### Direct Initialization of StoredValue Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-stored-values/SKILL.md Initialize StoredValue directly without using property wrapper syntax, useful in contexts where property wrappers are not supported. ```swift let hasHapticsEnabled = StoredValue(key: "hasHapticsEnabled", default: false) ``` -------------------------------- ### NotesController with @Stored Property Source: https://github.com/mergesort/boutique/blob/main/README.md Demonstrates using the @Stored property wrapper to cache notes in memory and on disk. This controller includes methods for fetching, saving, removing, and clearing notes. ```swift @Observable final class NotesController { /// Creates an @Stored property to handle an in-memory and on-disk cache of notes. ³ @Stored(in: .notesStore) var notes /// Fetches `Notes` from the API, providing the user with a red panda note if the request succeeds. func fetchNotes() async throws -> Note { // Hit the API that provides you a random image's metadata let noteURL = URL(string: "https://notes.redpanda.club/random/json")! let randomNoteRequest = URLRequest(url: noteURL) let (noteResponse, _) = try await URLSession.shared.data(for: randomNoteRequest) return Note(createdAt: .now, url: noteResponse.url, text: noteResponse.text) } /// Saves an note to the `Store` in memory and on disk. func saveNote(note: Note) async throws { try await self.$notes.insert(note) } /// Removes one note from the `Store` in memory and on disk. func removeNote(note: Note) async throws { try await self.$notes.remove(note) } /// Removes all of the notes from the `Store` in memory and on disk. func clearAllNotes() async throws { try await self.$notes.removeAll() } } ``` -------------------------------- ### Observe Store Changes with .onChange Source: https://github.com/mergesort/boutique/blob/main/Sources/Boutique/Documentation.docc/Articles/The @Stored Family Of Property Wrappers.md Use SwiftUI's .onChange modifier to react to changes in a Store's items. Set `initial: true` to trigger the handler on view appearance. ```swift struct NotesListView: View { @State var notesController: NotesController @State private var notes: [Note] = [] var body: some View { VStack { ForEach(self.notes) { note in Text(note.text) .onTapGesture { Task { try await notesController.removeNote(note) } } } } .onChange(of: notesController.notes, initial: true) { // We can even create complex pipelines, for example filtering all notes smaller than a tweet self.notes = newValue.filter { $0.length < 280 } } } } ``` -------------------------------- ### Executing Chained Operations with .run() Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-best-practices/SKILL.md Chained operations on a `Store` are not executed until `.run()` is explicitly called. Ensure `.run()` is appended to the chain to perform the intended operations. ```swift // Operations created but never executed try await store.removeAll().insert(items) // Correct: executes the chain try await store.removeAll().insert(items).run() ``` -------------------------------- ### Insert an Item into a Store Source: https://github.com/mergesort/boutique/blob/main/Sources/Boutique/Documentation.docc/Articles/Using Stores.md Use the `.insert(item:)` method to add a new item to the Store. This operation handles both adding new items and overwriting existing ones based on their `cacheIdentifier`. ```swift let coat = Item(name: "coat") try await store.insert(coat) ``` -------------------------------- ### Implement @Stored for Image Cache Management in Swift Source: https://github.com/mergesort/boutique/blob/main/llms.txt Demonstrates using the @Stored property wrapper for efficient in-memory and on-disk image caching. It includes a static store extension for images and methods to fetch, save, and remove `RemoteImage` objects. ```swift extension Store where Item == RemoteImage { // Initialize a Store to save our images into static let imagesStore = Store( storage: SQLiteStorageEngine.default(appendingPath: "Images") ) } final class ImagesController: ObservableObject { /// Creates a @Stored property to handle an in-memory and on-disk cache of images. ⁴ @Stored(in: .imagesStore) var images /// Fetches `RemoteImage` from the API, providing the user with a red panda if the request succeeds. func fetchImage() async throws -> RemoteImage { // Hit the API that provides you a random image's metadata let imageURL = URL(string: "https://image.redpanda.club/random/json")! let randomImageRequest = URLRequest(url: imageURL) let (imageResponse, _) = try await URLSession.shared.data(for: randomImageRequest) return RemoteImage(createdAt: .now, url: imageResponse.url, width: imageResponse.width, height: imageResponse.height, imageData: imageResponse.imageData) } /// Saves an image to the `Store` in memory and on disk. func saveImage(image: RemoteImage) async throws { try await self.$images.insert(image) } /// Removes one image from the `Store` in memory and on disk. func removeImage(image: RemoteImage) async throws { try await self.$images.remove(image) } /// Removes all of the images from the `Store` in memory and on disk. ``` -------------------------------- ### Monitor Store Events Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-store/SKILL.md An asynchronous function that iterates over the `events` stream of the `notes` property to monitor specific store operations like initialization, loading, insertion, and removal. ```swift 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) } } } ``` -------------------------------- ### Removing All Items Source: https://github.com/mergesort/boutique/blob/main/Sources/Boutique/Documentation.docc/Articles/Using Stores.md Removes all items from the Store. ```APIDOC ## Remove All Items ### Description Removes all items currently stored in the ``Store``. ### Method ```swift removeAll() ``` ### Parameters #### Path Parameters None #### Query Parameters None #### Request Body None ### Request Example ```swift try await store.removeAll() ``` ### Response #### Success Response (200) Indicates all items were successfully removed. #### Response Example None explicitly provided, operation is typically side-effect based. ``` -------------------------------- ### Call Store Operations from Non-MainActor Contexts Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-best-practices/SKILL.md Demonstrates how to call main-actor-isolated store operations from a background task. The main-actor hop happens automatically. ```swift func syncData() async throws { let data = try await self.api.fetchData() // Can run off main actor try await self.controller.updateStore(with: data) // MainActor hop happens automatically } ``` -------------------------------- ### Chain Store Operations (Clear and Insert) Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-store/SKILL.md Chain multiple store operations, such as removing all items and then inserting new data, into a single batch to optimize performance and prevent UI flickering. ```swift // Clear stale cache and insert fresh data try await store .removeAll() .insert(freshNotes) .run() ``` -------------------------------- ### Batch Inserting Items into Store Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-best-practices/SKILL.md Avoid inserting items into the `Store` within a loop, as this can lead to multiple `@MainActor` dispatches and inefficiency. Use the batch `insert` method for a single, optimized operation. ```swift // Inefficient: multiple @MainActor dispatches for note in notes { try await store.insert(note) } // Correct: single batch operation try await store.insert(notes) ``` -------------------------------- ### Use @SecurelyStoredValue Property Wrapper Source: https://github.com/mergesort/boutique/blob/main/llms.txt Illustrates the use of the @SecurelyStoredValue property wrapper for persisting individual Swift values with enhanced security. This wrapper is part of Boutique's storage solutions. ```swift @SecurelyStoredValue ``` -------------------------------- ### Monitor Boutique Store Events with Granular API Source: https://github.com/mergesort/boutique/blob/main/llms.txt Monitors individual store events using the Granular Events Tracking API. This function prints messages to the console for different operations like initialization, loading, insertion, and removal of notes. ```swift // You can also use Boutique's Granular Events Tracking API to be notified of individual changes. func monitorNotesStoreEvents() async { for await event in self.notesController.$notes.events { switch event.operation { case .initialized: print("[Store Event: initial] Our Notes Store has initialized") case .loaded: print("[Store Event: loaded] Our Notes Store has loaded with notes", event.notes.map(\.text)) case .insert: print("[Store Event: insert] Our Notes Store inserted notes", event.notes.map(\.text)) case .remove: print("[Store Event: remove] Our Notes Store removed notes", event.notes.map(\.text)) } } } ``` -------------------------------- ### Chain Store Operations Source: https://github.com/mergesort/boutique/blob/main/Sources/Boutique/Documentation.docc/Articles/Using Stores.md Chain multiple Store operations, such as `removeAll()` and `insert()`, using the `.run()` function to execute them sequentially. This is ideal for workflows like clearing existing data before loading fresh content from a server. ```swift try await store .removeAll() .insert(coat) .run() ``` -------------------------------- ### Subscribe to Store Item Changes in SwiftUI Source: https://github.com/mergesort/boutique/blob/main/README.md Shows how to subscribe to changes in the store's items using the .onChange modifier in SwiftUI, allowing for real-time UI updates. ```swift .onChange(of: store.items) { self.items = self.items.sorted(by: { $0.createdAt > $1.createdAt}) }) ``` -------------------------------- ### Insert Items into Store Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-store/SKILL.md Insert single or multiple items into the store. Inserting an item with an existing cache identifier will replace the existing item. ```swift // Single item try await store.insert(note) // Multiple items (preferred over calling insert in a loop) try await store.insert([note1, note2, note3]) ``` -------------------------------- ### Chain Store Operations (Remove and Insert) Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-store/SKILL.md Combine operations like removing specific outdated items and inserting updated ones in a single atomic batch using operation chaining. ```swift // Remove specific items and insert new ones try await store .remove(outdatedNote) .insert(updatedNote) .run() ``` -------------------------------- ### Inserting an Item Source: https://github.com/mergesort/boutique/blob/main/Sources/Boutique/Documentation.docc/Articles/Using Stores.md Inserts an item into the Store. The Store handles uniqueness based on the item's cacheIdentifier. ```APIDOC ## Insert Item ### Description Inserts an item into the ``Store``. If an item with the same `cacheIdentifier` already exists, it will be overwritten. ### Method ```swift insert(_ item: Item) ``` ### Parameters #### Path Parameters None #### Query Parameters None #### Request Body - **item** (Item) - The item to insert into the store. ### Request Example ```swift let coat = Item(name: "coat") try await store.insert(coat) ``` ### Response #### Success Response (200) Indicates the item was successfully inserted. #### Response Example None explicitly provided, operation is typically side-effect based. ``` -------------------------------- ### Waiting for Store to Load with onStoreDidLoad (Binding-based) Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-swiftui/SKILL.md Use onStoreDidLoad with a binding to automatically update a state variable when the store has finished loading. This provides a concise way to manage loading states. ```swift struct NotesView: View { @State var notesController: NotesController @State private var hasLoaded = false var body: some View { Group { if self.hasLoaded { NotesList(notes: self.notesController.notes) } else { ProgressView() } } .onStoreDidLoad(self.notesController.$notes, update: self.$hasLoaded) } } ``` -------------------------------- ### Performing Store Operations from SwiftUI Views Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-swiftui/SKILL.md Wrap asynchronous store operations, such as deleting a note, within Task blocks triggered by button actions or gestures in SwiftUI views. This ensures proper handling of async operations. ```swift struct NoteRow: View { let note: Note @State var notesController: NotesController var body: some View { Text(self.note.text) .swipeActions(edge: .trailing) { Button("Delete", role: .destructive) { Task { try await self.notesController.removeNote(self.note) } } } } } ``` -------------------------------- ### Displaying Store Items in a SwiftUI View Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-swiftui/SKILL.md Inject an @Observable controller as @State and access items directly within a List. ```swift struct NotesListView: View { @State var notesController: NotesController var body: some View { List(self.notesController.notes) { note in Text(note.text) } } } ``` -------------------------------- ### Define Storable Item Model Source: https://github.com/mergesort/boutique/blob/main/plugins/boutique/skills/boutique-store/SKILL.md Define a model that conforms to Codable, Sendable, and Identifiable for use with Boutique Store. Ensure the ID is a String for automatic cache identifier inference. ```swift struct Note: Codable, Sendable, Identifiable { let id: String let text: String let createdAt: Date } ``` -------------------------------- ### Using @StoredValue for Persistent Storage in Swift Source: https://github.com/mergesort/boutique/blob/main/llms.txt Demonstrates storing various data types like booleans, dates, enums, and complex Codable objects using @StoredValue. Includes methods for setting values and toggling booleans. ```swift // Setup a `@StoredValue has the same API. @StoredValue(key: "hasHapticsEnabled") var hasHapticsEnabled = false // You can also store nil values @StoredValue(key: "lastOpenedDate") var lastOpenedDate: Date? = nil // Enums work as well, as long as it conforms to `Codable` and `Equatable`. @StoredValue(key: "currentTheme") var currentlySelectedTheme = .light // Complex objects work as well struct UserPreferences: Codable, Equatable { var hasHapticsEnabled: Bool var prefersDarkMode: Bool var prefersWideScreen: Bool var spatialAudioEnabled: Bool } @StoredValue(key: "userPreferences") var preferences = UserPreferences() // Set the lastOpenedDate to now $lastOpenedDate.set(.now) // currentlySelected is now .dark $currentlySelectedTheme.set(.dark) // StoredValues that are backed by a boolean also have a toggle() function $hasHapticsEnabled.toggle() ```