mirror of
https://github.com/zhigang1992/Boutique.git
synced 2026-01-13 09:00:11 +08:00
1 line
27 KiB
JSON
1 line
27 KiB
JSON
{"primaryContentSections":[{"kind":"content","content":[{"anchor":"Overview","level":2,"type":"heading","text":"Overview"},{"type":"paragraph","inlineContent":[{"type":"text","text":"The "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" is the data storage primitive of a Boutique app, providing two layers of persistence. For every item you save to the "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" there will be an item saved to memory, and that same item will be saved to your "},{"type":"codeVoice","code":"StorageEngine"},{"type":"text","text":". The "},{"type":"codeVoice","code":"StorageEngine"},{"type":"text","text":" is a concept from "},{"type":"reference","isActive":true,"identifier":"https:\/\/github.com\/mergesort\/Boutique"},{"type":"text","text":", representing any data storage mechanism that persists data. If you’re looking into a library like Boutique chances are you’ve used a database, saved files to disk, or stored values in "},{"type":"codeVoice","code":"UserDefaults"},{"type":"text","text":", and Boutique aims to streamline that process. If you’re using a database like CoreData, Realm, or even CloudKit, you can build a "},{"type":"codeVoice","code":"StorageEngine"},{"type":"text","text":" tailored to your needs, but Bodega comes with a few built in options you can use. The default suggestion is to use "},{"type":"codeVoice","code":"SQLiteStorageEngine"},{"type":"text","text":", a simple and fast way to save data to a database in your app."}]},{"type":"paragraph","inlineContent":[{"type":"text","text":"Once you setup your "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" you’ll never have to think about your persistence layer ever again. Rather than interacting with a database and making queries, you’ll always be using Boutique’s memory layer. That may sound complex, but all that means is you’ll be saving to and reading from an array. Because you’re working with a regular array you won’t have to make changes to your app to accommodate the "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":", everything you’ve come to expect will work out the box, no changes required."}]},{"anchor":"Initializing-a-Store","level":2,"type":"heading","text":"Initializing a Store"},{"type":"paragraph","inlineContent":[{"type":"text","text":"To start working with a "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":", you’ll first need to initialize a "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" with a "},{"type":"codeVoice","code":"StorageEngine"}]},{"type":"codeListing","syntax":"swift","code":["let store = Store<Item>("," storage: SQLiteStorageEngine(directory: .defaultStorageDirectory(appendingPath: \"Items\")),"," cacheIdentifier: \\.id",")"]},{"type":"unorderedList","items":[{"content":[{"type":"paragraph","inlineContent":[{"type":"text","text":"The "},{"type":"codeVoice","code":"storage"},{"type":"text","text":" parameter is populated with a "},{"type":"codeVoice","code":"StorageEngine"},{"type":"text","text":", you can read more about it in "},{"type":"reference","isActive":true,"identifier":"https:\/\/mergesort.github.io\/Bodega\/documentation\/bodega\/using-storageengines"},{"type":"text","text":". Our SQLite database will be created in the platform’s default storage directory, nested in an "},{"type":"codeVoice","code":"Items"},{"type":"text","text":" subdirectory. On macOS this will be the "},{"type":"codeVoice","code":"Application Support"},{"type":"text","text":" directory, and on every other platform such as iOS this will be the "},{"type":"codeVoice","code":"Documents"},{"type":"text","text":" directory. If you need finer control over the location you can specify a "},{"type":"codeVoice","code":"FileManager.Directory"},{"type":"text","text":" such as "},{"type":"codeVoice","code":".documents"},{"type":"text","text":", "},{"type":"codeVoice","code":".caches"},{"type":"text","text":", "},{"type":"codeVoice","code":".temporary"},{"type":"text","text":", or even provide your own URL, also explored in "},{"overridingTitleInlineContent":[{"type":"text","text":"Bodega’s StorageEngine documentation"}],"isActive":true,"type":"reference","identifier":"https:\/\/mergesort.github.io\/Bodega\/documentation\/bodega\/using-storageengines","overridingTitle":"Bodega’s StorageEngine documentation"},{"type":"text","text":"."}]}]},{"content":[{"type":"paragraph","inlineContent":[{"type":"text","text":"The "},{"type":"codeVoice","code":"cacheIdentifier"},{"type":"text","text":" is a "},{"type":"codeVoice","code":"KeyPath<Model, String>"},{"type":"text","text":" that your model must provide. That may seem unconventional at first, so let’s break it down. Much like how protocols enforce a contract, the KeyPath is doing the same for our model. To be inserted into our "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" and saved to disk our models must conform to "},{"type":"codeVoice","code":"Codable & Equatable"},{"type":"text","text":", both of which are reasonable constraints given the data has to be serializable and searchable. But what we’re trying to avoid is making our models have to conform to a specialized caching protocol, we want to be able to save any ol’ object you already have in your app. Instead of creating a protocol like "},{"type":"codeVoice","code":"Storable"},{"type":"text","text":", we instead ask the model to tell us how we can derive a unique string which will be used as a key when storing the item."}]}]}]},{"type":"paragraph","inlineContent":[{"type":"text","text":"If your model (in this case "},{"type":"codeVoice","code":"Item"},{"type":"text","text":") already conforms to "},{"type":"codeVoice","code":"Identifiable"},{"type":"text","text":", we can simplify our initializer by eschewing the "},{"type":"codeVoice","code":"cacheIdentifier"},{"type":"text","text":" parameter."}]},{"type":"codeListing","syntax":"swift","code":["let store = Store<Item>("," storage: SQLiteStorageEngine(directory: .defaultStorageDirectory(appendingPath: \"Items\"))",")"]},{"type":"paragraph","inlineContent":[{"type":"text","text":"And since "},{"type":"codeVoice","code":"SQLiteStorageEngine"},{"type":"text","text":" is provided to Boutique by Bodega, Bodega exposes a "},{"type":"codeVoice","code":"default(appendingPath:)"},{"type":"text","text":" initializer we can use."}]},{"type":"codeListing","syntax":"swift","code":["static let store = Store<Item>("," storage: SQLiteStorageEngine.default(appendingPath: \"Items\")",")"]},{"type":"paragraph","inlineContent":[{"type":"text","text":"This is how simple it is to create a full database-backed persistence layer, only one line of code for something that can take hundreds of lines to implement otherwise."}]},{"anchor":"How-Are-Items-Stored","level":2,"type":"heading","text":"How Are Items Stored?"},{"type":"paragraph","inlineContent":[{"type":"text","text":"We’ll explore how to use "},{"type":"codeVoice","code":".insert(item: Item)"},{"type":"text","text":" to save items, but it’s worth taking a minute to discuss how items are stored in the "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":". When an item is saved to a "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":", that item is added to an array of "},{"type":"codeVoice","code":"items"},{"type":"text","text":", and it is also persisted by the "},{"type":"codeVoice","code":"StorageEngine"},{"type":"text","text":". If we use "},{"type":"codeVoice","code":"DiskStorageEngine"},{"type":"text","text":" the item will be saved to a disk, or if we use "},{"type":"codeVoice","code":"SQLiteStorageEngine"},{"type":"text","text":" the item will be saved to a database. The items are saved to the directory specified in the "},{"type":"codeVoice","code":"DiskStorageEngine"},{"type":"text","text":" or "},{"type":"codeVoice","code":"SQLiteStorageEngine"},{"type":"text","text":" initializer, and each item will be stored uniquely based on it’s "},{"type":"codeVoice","code":"cacheIdentifier"},{"type":"text","text":" key."}]},{"type":"paragraph","inlineContent":[{"type":"text","text":"The "},{"type":"codeVoice","code":"cacheIdentifier"},{"type":"text","text":" provides a mechanism for disambiguating objects, guaranteeing uniqueness of the items in our "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":". You never have to think about whether the item needs to be added or inserted (overwriting a matching item), or what index to insert an item at. Since we have a "},{"type":"codeVoice","code":"cacheIdentifier"},{"type":"text","text":" for every item we will know when an item should be added or overwritten inside of the "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":". This behavior means the "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" operates more like a "},{"type":"codeVoice","code":"Set"},{"type":"text","text":" than an "},{"type":"codeVoice","code":"Array"},{"type":"text","text":", because we are inserting items into a bag of objects, and don’t care in what order."}]},{"type":"paragraph","inlineContent":[{"type":"text","text":"As a result the only operations you have to know are "},{"type":"codeVoice","code":".insert"},{"type":"text","text":", "},{"type":"codeVoice","code":".remove"},{"type":"text","text":", and "},{"type":"codeVoice","code":".removeAll"},{"type":"text","text":", all of which are explored in the "},{"type":"strong","inlineContent":[{"type":"text","text":"Store Operations"}]},{"type":"text","text":" section below. If you do need to sort the items into a particular order, for example if you’re displaying the items alphabetically, you can always use the "},{"type":"codeVoice","code":"items"},{"type":"text","text":" property of a "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" and sort, filter, map, or transform it as you would any other array."}]},{"type":"paragraph","inlineContent":[{"type":"text","text":"To see how this looks I would highly recommend checking out the "},{"type":"reference","isActive":true,"identifier":"https:\/\/github.com\/mergesort\/Boutique\/tree\/main\/Demo"},{"type":"text","text":", as it shows off more complex examples of what Boutique and the Store can do. There’s even an example of how to sort items in a View based on time of creation "},{"type":"reference","isActive":true,"identifier":"https:\/\/github.com\/mergesort\/Boutique\/blob\/main\/Demo\/Demo\/Components\/FavoritesCarouselView.swift#L152-L154"},{"type":"text","text":"."}]},{"anchor":"Store-Operations","level":2,"type":"heading","text":"Store Operations"},{"type":"paragraph","inlineContent":[{"type":"text","text":"The "},{"type":"codeVoice","code":"items"},{"type":"text","text":" property of a "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" has an access control of "},{"type":"codeVoice","code":"public private (set)"},{"type":"text","text":", preventing the direct modification of the "},{"type":"codeVoice","code":"items"},{"type":"text","text":" array. If you want to mutate the "},{"type":"codeVoice","code":"items"},{"type":"text","text":" of a "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" you need to use the three functions the "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" exposes. The API surface area is very small though, there are only three functions you need to know."}]},{"type":"paragraph","inlineContent":[{"type":"text","text":"Inserts an item into the "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"}]},{"type":"codeListing","syntax":"swift","code":["let coat = Item(name: \"coat\")","try await store.insert(coat)"]},{"type":"paragraph","inlineContent":[{"type":"text","text":"Remove an item from the "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"}]},{"type":"codeListing","syntax":"swift","code":["try await store.remove(coat)"]},{"type":"paragraph","inlineContent":[{"type":"text","text":"Remove all the items a "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"}]},{"type":"codeListing","syntax":"swift","code":["try await store.removeAll()"]},{"type":"paragraph","inlineContent":[{"type":"text","text":"You can even chain operations using the "},{"type":"codeVoice","code":".run()"},{"type":"text","text":" function, executing them in the order they are appended to the "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":". This is really useful for situations where you want to clear your "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" before adding new items, such as downloading a fresh set of data from a server."}]},{"type":"codeListing","syntax":"swift","code":["try await store"," .removeAll()"," .insert(coat)"," .run()"]},{"anchor":"Sync-or-Async","level":2,"type":"heading","text":"Sync or Async?"},{"type":"paragraph","inlineContent":[{"type":"text","text":"To work with @"},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Stored"},{"type":"text","text":" or alternative property wrappers the "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" must be initialized synchronously. This means that the "},{"type":"codeVoice","code":"items"},{"type":"text","text":" of your "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" will be loaded in the background, and may not be available immediately. However this can be an issue if you are using the "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" directly and need to show the contents of the "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" immediately, such as on your app’s launch’. The "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" provides you with two options to handle a scenario like this."}]},{"type":"paragraph","inlineContent":[{"type":"text","text":"By using the "},{"type":"codeVoice","code":"async"},{"type":"text","text":" overload of the "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" initializer your "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" will be returned once all of the "},{"type":"codeVoice","code":"items"},{"type":"text","text":" are loaded."}]},{"type":"codeListing","syntax":"swift","code":["let store: Store<Item>","","init() async throws {"," store = try await Store(...)"," \/\/ Now the store will have `items` already loaded."," let items = await store.items","}"]},{"type":"paragraph","inlineContent":[{"type":"text","text":"Alternatively you can use the synchronous initializer, and then await for items to load before accessing them."}]},{"type":"codeListing","syntax":"swift","code":["let store: Store<Item> = Store(...)","","func getItems() async -> [Item] {"," try await store.itemsHaveLoaded() "," return await store.items","}"]},{"type":"paragraph","inlineContent":[{"type":"text","text":"The synchronous initializer is a sensible default, but if your app’s needs dictate displaying data only once you’ve loaded all of the necessary items the asynchronous initializers are there to help."}]},{"anchor":"Further-Exploration-Stored-And-More","level":2,"type":"heading","text":"Further Exploration, @Stored And More"},{"type":"paragraph","inlineContent":[{"type":"text","text":"Building an app using the "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" can be really powerful because it leans into SwiftUI’s state-driven architecture, while providing you with offline-first capabilities, realtime updates across your app, with almost no additional code required."}]},{"type":"paragraph","inlineContent":[{"type":"text","text":"We’ve introduced the "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":", but the real power lies when you start to use Boutique’s property wrappers, @"},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Stored"},{"type":"text","text":", @"},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/StoredValue"},{"type":"text","text":", @"},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/SecurelyStoredValue"},{"type":"text","text":", and @"},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/AsyncStoredValue"},{"type":"text","text":". These property wrappers help deliver on the promise of working with regular Swift values and arrays yet having data persisted automatically, without ever having to think about the concept of a database."}]},{"type":"paragraph","inlineContent":[{"type":"text","text":"The next step is to explore how they work, with a small example SwiftUI app."}]},{"type":"unorderedList","items":[{"content":[{"type":"paragraph","inlineContent":[{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/The-@Stored-Family-Of-Property-Wrappers"}]}]}]},{"type":"paragraph","inlineContent":[{"type":"text","text":"As a reminder you can always play around with the code yourself."}]},{"type":"unorderedList","items":[{"content":[{"type":"paragraph","inlineContent":[{"overridingTitleInlineContent":[{"type":"text","text":"A Boutique Demo App"}],"isActive":true,"type":"reference","identifier":"https:\/\/github.com\/mergesort\/Boutique\/tree\/main\/Demo","overridingTitle":"A Boutique Demo App"}]}]}]},{"type":"paragraph","inlineContent":[{"type":"text","text":"Or read through an in-depth technical walkthrough of Boutique, and how it powers the Model View Controller Store architecture."}]},{"type":"unorderedList","items":[{"content":[{"type":"paragraph","inlineContent":[{"type":"reference","isActive":true,"identifier":"https:\/\/build.ms\/2022\/06\/22\/model-view-controller-store"}]}]}]}]}],"schemaVersion":{"major":0,"minor":3,"patch":0},"sections":[],"variants":[{"paths":["\/documentation\/boutique\/using-stores"],"traits":[{"interfaceLanguage":"swift"}]}],"identifier":{"url":"doc:\/\/Boutique\/documentation\/Boutique\/Using-Stores","interfaceLanguage":"swift"},"abstract":[{"type":"text","text":"The "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" is at the heart of what makes Boutique, Boutique."}],"kind":"article","metadata":{"roleHeading":"Article","title":"Using Stores","role":"article","modules":[{"name":"Boutique"}]},"hierarchy":{"paths":[["doc:\/\/Boutique\/documentation\/Boutique"]]},"references":{"https://github.com/mergesort/Boutique/tree/main/Demo":{"title":"Boutique demo app","titleInlineContent":[{"type":"text","text":"Boutique demo app"}],"type":"link","identifier":"https:\/\/github.com\/mergesort\/Boutique\/tree\/main\/Demo","url":"https:\/\/github.com\/mergesort\/Boutique\/tree\/main\/Demo"},"https://github.com/mergesort/Boutique/blob/main/Demo/Demo/Components/FavoritesCarouselView.swift#L152-L154":{"title":"here","titleInlineContent":[{"type":"text","text":"here"}],"type":"link","identifier":"https:\/\/github.com\/mergesort\/Boutique\/blob\/main\/Demo\/Demo\/Components\/FavoritesCarouselView.swift#L152-L154","url":"https:\/\/github.com\/mergesort\/Boutique\/blob\/main\/Demo\/Demo\/Components\/FavoritesCarouselView.swift#L152-L154"},"https://mergesort.github.io/Bodega/documentation/bodega/using-storageengines":{"title":"Bodega’s StorageEngine documentation","titleInlineContent":[{"type":"text","text":"Bodega’s StorageEngine documentation"}],"type":"link","identifier":"https:\/\/mergesort.github.io\/Bodega\/documentation\/bodega\/using-storageengines","url":"https:\/\/mergesort.github.io\/Bodega\/documentation\/bodega\/using-storageengines"},"https://github.com/mergesort/Boutique":{"title":"Bodega","titleInlineContent":[{"type":"text","text":"Bodega"}],"type":"link","identifier":"https:\/\/github.com\/mergesort\/Boutique","url":"https:\/\/github.com\/mergesort\/Boutique"},"https://build.ms/2022/06/22/model-view-controller-store":{"title":"Model View Controller Store: Reinventing MVC for SwiftUI with Boutique","titleInlineContent":[{"type":"text","text":"Model View Controller Store: Reinventing MVC for SwiftUI with Boutique"}],"type":"link","identifier":"https:\/\/build.ms\/2022\/06\/22\/model-view-controller-store","url":"https:\/\/build.ms\/2022\/06\/22\/model-view-controller-store"},"doc://Boutique/documentation/Boutique":{"role":"collection","title":"Boutique","abstract":[{"type":"text","text":"A simple but surprisingly fancy data store and so much more"}],"identifier":"doc:\/\/Boutique\/documentation\/Boutique","kind":"symbol","type":"topic","url":"\/documentation\/boutique"},"doc://Boutique/documentation/Boutique/SecurelyStoredValue":{"role":"symbol","title":"SecurelyStoredValue","fragments":[{"kind":"keyword","text":"struct"},{"kind":"text","text":" "},{"kind":"identifier","text":"SecurelyStoredValue"}],"abstract":[{"type":"text","text":"The @"},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/SecurelyStoredValue"},{"type":"text","text":" property wrapper automagically persists a single "},{"type":"codeVoice","code":"Item"},{"type":"text","text":" in the system "},{"type":"codeVoice","code":"Keychain"},{"type":"text","text":" "},{"type":"text","text":"rather than an array of items that would be persisted in a "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" or using @"},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Stored"},{"type":"text","text":"."}],"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/SecurelyStoredValue","kind":"symbol","type":"topic","navigatorTitle":[{"kind":"identifier","text":"SecurelyStoredValue"}],"url":"\/documentation\/boutique\/securelystoredvalue"},"doc://Boutique/documentation/Boutique/The-@Stored-Family-Of-Property-Wrappers":{"role":"article","title":"The @Stored Family Of Property Wrappers","abstract":[{"type":"text","text":"Property Wrappers that take the "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" and make it magical. ✨"}],"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/The-@Stored-Family-Of-Property-Wrappers","kind":"article","type":"topic","url":"\/documentation\/boutique\/the-@stored-family-of-property-wrappers"},"doc://Boutique/documentation/Boutique/StoredValue":{"role":"symbol","title":"StoredValue","fragments":[{"kind":"keyword","text":"struct"},{"kind":"text","text":" "},{"kind":"identifier","text":"StoredValue"}],"abstract":[{"type":"text","text":"The @"},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/StoredValue"},{"type":"text","text":" property wrapper to automagically persist a single "},{"type":"codeVoice","code":"Item"},{"type":"text","text":" in "},{"type":"codeVoice","code":"UserDefaults"},{"type":"text","text":" "},{"type":"text","text":"rather than an array of items that would be persisted in a "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" or using @"},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Stored"},{"type":"text","text":"."}],"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/StoredValue","kind":"symbol","type":"topic","navigatorTitle":[{"kind":"identifier","text":"StoredValue"}],"url":"\/documentation\/boutique\/storedvalue"},"doc://Boutique/documentation/Boutique/Store":{"role":"symbol","title":"Store","fragments":[{"kind":"keyword","text":"class"},{"kind":"text","text":" "},{"kind":"identifier","text":"Store"}],"abstract":[{"type":"text","text":"A fancy persistence layer."}],"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store","kind":"symbol","type":"topic","navigatorTitle":[{"kind":"identifier","text":"Store"}],"url":"\/documentation\/boutique\/store"},"doc://Boutique/documentation/Boutique/Stored":{"role":"symbol","title":"Stored","fragments":[{"kind":"keyword","text":"struct"},{"kind":"text","text":" "},{"kind":"identifier","text":"Stored"}],"abstract":[{"type":"text","text":"The @"},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Stored"},{"type":"text","text":" property wrapper to automagically initialize a "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":"."}],"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Stored","kind":"symbol","type":"topic","navigatorTitle":[{"kind":"identifier","text":"Stored"}],"url":"\/documentation\/boutique\/stored"},"doc://Boutique/documentation/Boutique/AsyncStoredValue":{"role":"symbol","title":"AsyncStoredValue","fragments":[{"kind":"keyword","text":"struct"},{"kind":"text","text":" "},{"kind":"identifier","text":"AsyncStoredValue"}],"abstract":[{"type":"text","text":"The @"},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/AsyncStoredValue"},{"type":"text","text":" property wrapper to automagically persist a single "},{"type":"codeVoice","code":"Item"},{"type":"text","text":" in a "},{"type":"codeVoice","code":"StorageEngine"},{"type":"text","text":" "},{"type":"text","text":"rather than an array of items that would be persisted in a "},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Store"},{"type":"text","text":" or using @"},{"type":"reference","isActive":true,"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/Stored"},{"type":"text","text":"."}],"identifier":"doc:\/\/Boutique\/documentation\/Boutique\/AsyncStoredValue","kind":"symbol","type":"topic","navigatorTitle":[{"kind":"identifier","text":"AsyncStoredValue"}],"url":"\/documentation\/boutique\/asyncstoredvalue"}}} |