From boutique
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.
npx claudepluginhub mergesort/boutique --plugin boutiqueThis skill uses the workspace's default tool permissions.
Use this skill when migrating to Swift 6, troubleshooting concurrency issues, setting up tests, or following Boutique's recommended patterns.
Creates and manages Boutique Store for Swift data persistence: initialization, @Stored controllers, CRUD operations, chaining, event monitoring. For item arrays and data controllers.
Guides iOS app architecture using Swift 6, iOS 18+, SwiftUI, SwiftData, Observation framework, and concurrency. Modernizes legacy patterns like Core Data, ObservableObject, and GCD.
Builds SwiftUI views using modern MV architecture, @Observable state management, view composition, environment wiring, async loading with .task, and iOS 18+ performance guidelines.
Share bugs, ideas, or general feedback.
Use this skill when migrating to Swift 6, troubleshooting concurrency issues, setting up tests, or following Boutique's recommended patterns.
Boutique's Store, StoredValue, and SecurelyStoredValue are all @MainActor isolated. In Swift 6 with strict concurrency, this means:
insert, remove, removeAll) must be called from a @MainActor context.@Stored, @StoredValue, or @SecurelyStoredValue are implicitly @MainActor since the property wrappers are @MainActor.@MainActor, so no extra annotation is needed there.// From a background task or non-isolated function
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
}
When using @Stored, @StoredValue, or @SecurelyStoredValue inside an @Observable class, you must mark them with @ObservationIgnored.
Store, StoredValue, and SecurelyStoredValue are themselves @Observable. If you place an @Observable property inside another @Observable class without @ObservationIgnored, SwiftUI may track changes at both levels, leading to redundant view updates or unexpected behavior.
@Observable
final class NotesController {
@ObservationIgnored
@Stored var notes: [Note]
init(store: Store<Note>) {
self._notes = Stored(in: store)
}
}
@Observable
final class Preferences {
@ObservationIgnored
@StoredValue(key: "theme")
var theme: Theme = .light
@ObservationIgnored
@SecurelyStoredValue<String>(key: "authToken")
var authToken
}
// DO NOT do this
@Observable
final class NotesController {
@Stored var notes: [Note] // Missing @ObservationIgnored
}
All items must conform to StorableItem, which is Codable & Sendable.
Structs get Sendable conformance automatically when all stored properties are Sendable.
struct Note: Codable, Sendable, Identifiable, Equatable {
let id: String
let text: String
let createdAt: Date
}
Enums work as stored items too, as long as they conform to the required protocols.
enum Theme: String, Codable, Sendable, Equatable {
case light
case dark
case system
}
Always inject Store instances through initializers rather than creating them inline. This enables swapping in test stores.
@Observable
final class NotesController {
@ObservationIgnored
@Stored var notes: [Note]
init(store: Store<Note>) {
self._notes = Stored(in: store)
}
}
extension Store where Item == Note {
static let notesStore = Store<Note>(
storage: SQLiteStorageEngine.default(appendingPath: "Notes")
)
}
let controller = NotesController(store: .notesStore)
Create an in-memory store for test isolation. Use a unique temporary path per test to avoid collisions.
@Test
func testInsertNote() async throws {
let store = Store<Note>(
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 }))
}
For SwiftUI previews, use Store.previewStore(items:) (DEBUG only) to create in-memory stores with pre-populated data.
#Preview {
let store = Store<Note>.previewStore(items: [
Note(id: "1", text: "Preview note", createdAt: .now),
])
NotesListView(notesController: NotesController(store: store))
}
Variants:
Store.previewStore(items:) when Item: Identifiable, ID == StringStore.previewStore(items:) when Item: Identifiable, ID == UUIDStore.previewStore(items:cacheIdentifier:) for custom identifiersPreview stores do not persist to disk and are only available in DEBUG builds.
You are calling set on the wrappedValue instead of the projectedValue. Add a $ prefix.
// Wrong
storedValue.set(newValue)
// Correct
$storedValue.set(newValue)
@SecurelyStoredValue already wraps the value as optional. Do not declare the type as optional.
// Wrong: creates Item??
@SecurelyStoredValue<String?>(key: "token")
var token
// Correct: wrappedValue is String?
@SecurelyStoredValue<String>(key: "token")
var token
The synchronous Store initializer loads items in a background task. If you access store.items immediately, it may be empty.
Fix: Use the async initializer, or call itemsHaveLoaded() before reading items.
// Option 1: Async init
let store = try await Store<Note>(storage: ...)
// Option 2: Wait for loading
let store = Store<Note>(storage: ...)
try await store.itemsHaveLoaded()
When using @Stored in a controller that's used by SwiftUI, items load automatically and the view re-renders when ready. Use onStoreDidLoad for explicit loading states.
Chained operations are not executed until .run() is called.
// Operations created but never executed
try await store.removeAll().insert(items)
// Correct: executes the chain
try await store.removeAll().insert(items).run()
// Inefficient: multiple @MainActor dispatches
for note in notes {
try await store.insert(note)
}
// Correct: single batch operation
try await store.insert(notes)
@Observable controllers per data domain (NotesController, PhotosController), not one giant controller.addNote, removeNote) on controllers rather than exposing the Store directly to views.@Observable classes grouped by feature area.@MainActor default isolation.boutique-store skill for Store setup and @Stored controller patterns.boutique-stored-values skill for @StoredValue and @SecurelyStoredValue APIs.boutique-swiftui skill for SwiftUI view integration patterns.