Skip to content

Commit 8a1f9a1

Browse files
committed
Updating readme for 3.0 APIs
1 parent 1035441 commit 8a1f9a1

File tree

1 file changed

+67
-49
lines changed

1 file changed

+67
-49
lines changed

README.md

Lines changed: 67 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
### A simple but surprisingly fancy data store and so much more
44

5-
>*"I ripped out Core Data, this is the way it should work"*
5+
>*"I ripped out Core Data, this is the way it should work"*
66
77
[Josh Holtz](https://github.com/joshdholtz)
88

@@ -32,7 +32,7 @@ Table of Contents
3232

3333
Boutique is a simple but powerful persistence library, a small set of property wrappers and types that enable building incredibly simple state-driven apps for SwiftUI, UIKit, and AppKit. With its dual-layered memory + disk caching architecture, Boutique provides a way to build apps that update in real time with full offline storage in only a few lines of code — using an incredibly simple API.
3434

35-
- Boutique is used in hundreds of apps, and I thoroughly test every API in my app [Plinky](https://plinky.app). (Which I highly recommend [downloading](https://plinky.app/download) and maybe even subscribing to. 😉)
35+
- Boutique is used in hundreds of apps, and I thoroughly test every API in my app [Plinky](https://plinky.app). (Which I highly recommend [downloading](https://plinky.app/download) and maybe even subscribing to. 😉)
3636
- Boutique is built atop [Bodega](https://github.com/mergesort/Bodega), and you can find a demo app built atop the Model View Controller Store architecture in this [repo](https://github.com/mergesort/Boutique/tree/main/Demo) which shows you how to make an offline-ready SwiftUI app in only a few lines of code. You can read more about the thinking behind the architecture in this blog post exploring the [MVCS architecture](https://build.ms/2022/06/22/model-view-controller-store).
3737

3838
---
@@ -90,7 +90,7 @@ try await store
9090
.insert(dog)
9191
.insert(cat)
9292
.run()
93-
93+
9494
print(store.items) // Prints [dog, cat]
9595

9696
// This is a good way to clear stale cached data
@@ -105,26 +105,41 @@ print(store.items) // Prints [redPanda]
105105
And if you're building a SwiftUI app you don't have to change a thing, Boutique was made for and with SwiftUI in mind. (But works well in UIKit and AppKit of course. 😉)
106106

107107
```swift
108-
// Since items is a @Published property
109-
// you can subscribe to any changes in realtime.
110-
store.$items.sink({ items in
111-
print("Items was updated", items)
108+
// Since @Store, @StoredValue, and @SecurelyStoredValue are `@Observable`, you can subscribe
109+
// to changes in realtime using any of Swift's built-in observability mechanisms.
110+
.onChange(of: store.items) {
111+
self.items = self.items.sorted(by: { $0.createdAt > $1.createdAt})
112112
})
113+
```
113114

114-
// Works great with SwiftUI out the box for more complex pipelines.
115-
.onReceive(store.$items, perform: {
116-
self.allItems = $0.filter({ $0.id > 100 })
117-
})
115+
```swift
116+
// You can also use Boutique's Granular Events Tracking API to be notified of individual changes.
117+
func monitorNotesStoreEvents() async {
118+
for await event in self.notesController.$notes.events {
119+
switch event.operation {
120+
121+
case .initialized:
122+
print("[Store Event: initial] Our Notes Store has initialized")
123+
124+
case .loaded:
125+
print("[Store Event: loaded] Our Notes Store has loaded with notes", event.notes.map(\.text))
126+
127+
case .insert:
128+
print("[Store Event: insert] Our Notes Store inserted notes", event.notes.map(\.text))
129+
130+
case .remove:
131+
print("[Store Event: remove] Our Notes Store removed notes", event.notes.map(\.text))
132+
}
133+
}
134+
}
118135
```
119136
---
120137

121-
¹ You can have as many or as few Stores as you'd like. It may be a good strategy to have one Store for all of the images you download in your app, but you may also want to have one Store per model-type you'd like to cache. You can even create separate stores for tests, Boutique isn't prescriptive and the choice for how you'd like to model your data is yours. You'll also notice, that's a concept from Bodega which you can read about in Bodega's [StorageEngine documentation](https://mergesort.github.io/Bodega/documentation/bodega/using-storageengines).
122-
123-
² Under the hood the Store is doing the work of saving all changes to disk when you add or remove items.
138+
¹ You can have as many or as few Stores as you'd like. It may be a good strategy to have one Store for all of the notes you download in your app, but you may also want to have one Store per model-type you'd like to cache. You can even create separate stores for tests, Boutique isn't prescriptive and the choice for how you'd like to model your data is yours. You'll also notice, that's a concept from Bodega which you can read about in Bodega's [StorageEngine documentation](https://mergesort.github.io/Bodega/documentation/bodega/using-storageengines).
124139

125-
³ In SwiftUI you can even power your `View`s with `$items` and use `.onReceive()` to update and manipulate data published by the Store's `$items`.
140+
² Under the hood the Store is doing the work of saving all changes to disk when you add or remove items.
126141

127-
> **Warning** Storing images or other binary data in Boutique is technically supported but not recommended. The reason is that storing images in Boutique's can balloon up the in-memory store, and your app's memory as a result. For similar reasons as it's not recommended to store images or binary blobs in a database, it's not recommended to store images or binary blobs in Boutique.
142+
> **Warning** Storing binary blobs (such as images or Data) in Boutique is technically supported, but not recommended. The reason not to store images in Boutique is that storing images in Boutique's can balloon up the in-memory store, and your app's memory as a result. For similar reasons as it's not recommended to store images or binary blobs in a database, it's not recommended to store images or binary blobs in Boutique. Bodega provides a good solution for this problem, which you can read about in [this tutorial](https://mergesort.github.io/Bodega/documentation/bodega/building-an-image-cache/).
128143
129144
---
130145

@@ -136,57 +151,60 @@ We'll go through a high level overview of the `@Stored` property wrapper below,
136151
That was easy, but I want to show you something that makes Boutique feel downright magical. The `Store` is a simple way to gain the benefits of offline storage and realtime updates, but by using the `@Stored` property wrapper we can cache any property in-memory and on disk with just one line of code.
137152

138153
```swift
139-
extension Store where Item == RemoteImage {
140-
// Initialize a Store to save our images into
141-
static let imagesStore = Store<RemoteImage>(
142-
storage: SQLiteStorageEngine.default(appendingPath: "Images")
154+
extension Store where Item == Note {
155+
// Initialize a Store to save our notes into
156+
static let notesStore = Store<Note>(
157+
storage: SQLiteStorageEngine.default(appendingPath: "Notes")
143158
)
144159

145160
}
146161

147-
final class ImagesController: ObservableObject {
148-
/// Creates a @Stored property to handle an in-memory and on-disk cache of images. ⁴
149-
@Stored(in: .imagesStore) var images
162+
@Observable
163+
final class NotesController {
164+
/// Creates an @Stored property to handle an in-memory and on-disk cache of notes. ³
165+
@Stored(in: .notesStore) var notes
150166

151-
/// Fetches `RemoteImage` from the API, providing the user with a red panda if the request succeeds.
152-
func fetchImage() async throws -> RemoteImage {
167+
/// Fetches `Notes` from the API, providing the user with a red panda note if the request succeeds.
168+
func fetchNotes() async throws -> Note {
153169
// Hit the API that provides you a random image's metadata
154-
let imageURL = URL(string: "https://image.redpanda.club/random/json")!
155-
let randomImageRequest = URLRequest(url: imageURL)
156-
let (imageResponse, _) = try await URLSession.shared.data(for: randomImageRequest)
170+
let noteURL = URL(string: "https://notes.redpanda.club/random/json")!
171+
let randomNoteRequest = URLRequest(url: noteURL)
172+
let (noteResponse, _) = try await URLSession.shared.data(for: randomNoteRequest)
157173

158-
return RemoteImage(createdAt: .now, url: imageResponse.url, width: imageResponse.width, height: imageResponse.height, imageData: imageResponse.imageData)
174+
return Note(createdAt: .now, url: noteResponse.url, text: noteResponse.text)
159175
}
160-
161-
/// Saves an image to the `Store` in memory and on disk.
162-
func saveImage(image: RemoteImage) async throws {
163-
try await self.$images.insert(image)
176+
177+
/// Saves an note to the `Store` in memory and on disk.
178+
func saveNote(note: Note) async throws {
179+
try await self.$notes.insert(note)
164180
}
165-
166-
/// Removes one image from the `Store` in memory and on disk.
167-
func removeImage(image: RemoteImage) async throws {
168-
try await self.$images.remove(image)
181+
182+
/// Removes one note from the `Store` in memory and on disk.
183+
func removeNote(note: Note) async throws {
184+
try await self.$notes.remove(note)
169185
}
170-
171-
/// Removes all of the images from the `Store` in memory and on disk.
172-
func clearAllImages() async throws {
173-
try await self.$images.removeAll()
186+
187+
/// Removes all of the notes from the `Store` in memory and on disk.
188+
func clearAllNotes() async throws {
189+
try await self.$notes.removeAll()
174190
}
175191
}
176192
```
177193

178-
That's it, that's really it. This technique scales very well, and sharing this data across many views is exactly how Boutique scales from simple to complex apps without adding API complexity. It's hard to believe that now your app can update its state in real time with full offline storage thanks to only one line of code. `@Stored(in: .imagesStore) var images`
194+
That's it, that's really it. This technique scales very well, and sharing this data across many views is exactly how Boutique scales from simple to complex apps without adding API complexity. It's hard to believe that now your app can update its state in real time with full offline storage thanks to only one line of code. `@Stored(in: .notesStore) var notes`
179195

180196
---
181197

182-
(If you'd prefer to decouple the store from your view model, controller, or manager object, you can inject stores into the object like this.)
198+
³ (If you'd prefer to decouple the store from your view model, controller, or manager object, you can inject stores into the object like this.)
183199

184200
```swift
185-
final class ImagesController: ObservableObject {
186-
@Stored var images: [RemoteImage]
201+
@Observable
202+
final class NotesController {
203+
@ObservationIgnored
204+
@Stored var notes: [Note]
187205

188-
init(store: Store<RemoteImage>) {
189-
self._images = Stored(in: store)
206+
init(store: Store<Note>) {
207+
self._notes = Stored(in: store)
190208
}
191209
}
192210
```
@@ -252,7 +270,7 @@ As I was building v1 I noticed that people who got Boutique loved it, and people
252270

253271
### Further Exploration
254272

255-
Boutique is very useful on its own for building realtime offline-ready apps with just a few lines of code, but it's even more powerful when you use the Model View Controller Store architecture I've developed, demonstrated in the `ImagesController` above. MVCS brings together the familiarity and simplicity of the [MVC architecture](https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html) you know and love with the power of a `Store`, to give your app a simple but well-defined state management and data architecture.
273+
Boutique is very useful on its own for building realtime offline-ready apps with just a few lines of code, but it's even more powerful when you use the Model View Controller Store architecture I've developed, demonstrated in the `NotesController` above. MVCS brings together the familiarity and simplicity of the [MVC architecture](https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html) you know and love with the power of a `Store`, to give your app a simple but well-defined state management and data architecture.
256274

257275
If you'd like to learn more about how it works you can read about the philosophy in a [blog post](https://build.ms/2022/06/22/model-view-controller-store) where I explore MVCS for SwiftUI, and you can find a reference implementation of an offline-ready realtime MVCS app powered by Boutique in this [repo](https://github.com/mergesort/MVCS).
258276

@@ -304,7 +322,7 @@ If you prefer not to use SPM, you can integrate Boutique into your project manua
304322

305323
### About me
306324

307-
Hi, I'm [Joe](http://fabisevi.ch) everywhere on the web, but especially on [Mastodon](https://macaw.social/@mergesort).
325+
Hi, I'm [Joe](http://fabisevi.ch) everywhere on the web, but especially on [Bluesky](https://bsky.app/profile/mergesort.me).
308326

309327
### License
310328

0 commit comments

Comments
 (0)