This version update isn't just one version update, it's two. Boutique's 2.0 depends on Bodega version 2.0, which is a huge update in its own right. The Bring Your Own Database feature is powered by Bodega, which means you get all of the functionality with no API changes to Boutique. And of course it's still only a couple of lines of code to have an app with a single source of truth, realtime updates, the offline support you've come to know and love, and now 5-10x faster out of the box.
But before we talk about the database, let's see what else Boutique 2.0 has to offer.
Warning
This version contains breaking changes
@StoredValue
Most data your app works with is in the shape of an array, but sometimes you need to store a single value. That's what @StoredValue is for. As the name implies @StoredValue allows you to store a value, which is great for saving user preferences, configurations, or even individual value like lastOpenedDate.
Creating a @StoredValue is easy, it even supports default values like you would expect with any other Swift property.
@StoredValue<RedPanda>(key: "pandaRojo")
private var spanishRedPanda = RedPanda(cuteRating: 100)
A more complex example may look like this, for example if you were building a Youtube-like video app.
struct UserPreferences: Codable, Equatable {
var hasProvidedNotificationsAccess: Bool
var hasHapticsEnabled: Bool
var prefersDarkMode: Bool
var prefersWideScreenVideos: Bool
var spatialAudioEnabled: Bool
}
struct UserPreferences: Codable, Equatable {
var hasProvidedNotificationsAccess: Bool
var hasHapticsEnabled: Bool
var prefersDarkMode: Bool
var prefersWideScreenVideos: Bool
var spatialAudioEnabled: Bool
}
struct LikedVideos: Codable, Equatable {
let ids: [Int]
}
struct DownloadedVideos: Codable, Equatable {
let ids: [Int]
}
struct AppState {
@StoredValue<UserPreferences>(key: "userPreferences")
var preferences
@StoredValue(key: "likedEpisodes")
var likedVideos = LikedVideos(ids: [1, 2, 3])
@StoredValue<DownloadedVideos>(key: "downloadedVideos")
var downloadedVideos
@StoredValue(key: "openLinksInSafari")
var openLinksInSafari = true
}
Thank you to @iankeen for helping me iterate on @StoredValue, and working through some nuances as the final version took shape.
AppKit/UIKit support
This one does what it says on the tin, Boutique is no longer constrained to SwiftUI. @Stored and the new @StoredValue will work in UIKit and AppKit apps!
Chained Operations
This is a breaking change, but a very worthwhile one. Previously when you added an item there was an removingExistingItems parameter that would provide a form of cache invalidation. But as they say, the two hardest problems in computer science are naming, cache invalidation, and off by one errors, so let's fix all three in one fell swoop.
public func add(_ item: Item) async throws -> Operation
The reason for the removingExistingItems parameter was to remove cached items and add new items in one operation, preventing multiple dispatches to the @MainActor. We wanted to avoid multiple dispatches to avoid multiple SwiftUI render cycles, and now we can avoid that thanks to Operation chaining. But what is Operation? An Operation is a type you never have to think about, but it allows us to chain commands together transparently, like this.
self.store.removeAll().add(items: [1, 2, 3]).run() // The Store now contains [1, 2, 3]
self.store.remove(1).add(items: [4, 5, 6]).run() // The Store now contains [2, 3, 4, 5, 6]
This fluent syntax is much more intuitive, and no longer do you have a confusing parameter that conflates cache invalidation and adding items due to an unexpected side effect of how SwiftUI renders occur.
Thank you to @davedelong for helping me think through and prototyping chained operations, I really appreciate what came to be and wouldn't have gotten there without his help.
defaultStorageDirectory
Previously the default folder location a Store was initialized was the Documents directory. This makes sense on iOS, tvOS, and more locked down platforms, but on macOS it makes more sense to store data in the Application Support folder. Support for defaultStorageDirectory comes from Bodega, but if you're initializing a Boutique Store the location will now default to the expected folder on each platform.
Bring Your Own Database
In the Version 1.x series of Bodega the DiskStorage type was responsible for persisting data to disk. As the name implies DiskStorage was backed by the file system, but what if you don't want to save Data to disk? Saving data to disk is a simple and effective starting point, but can get slow when working with large data sets. One of Bodega's goals is to work with every app without causing developers to make tradeoffs, so version 2.0 is focused on eliminating those tradeoffs without ruining the streamlined simplicity Bodega brings, and brings that to Boutique.
In the spirit of not making tradeoffs here's how Bodega works with any database you want, say hello to the new StorageEngine protocol.
By providing your own write, read, remove, key, and timestamp related functions, you can make any persistence layer compatible with ObjectStorage. Whether your app is backed by Realm, Core Data, or even CloudKit, when you create a new StorageEngine it automatically becomes usable by ObjectStorage, with one drop dead simple API.
The first StorageEngine to be implemented is an SQLiteStorageEngine, bundled with Bodega. I'll explain all the possibilities below, but first let's take a second to see how much faster your apps using Bodega and Boutique will be.
If it's not obvious, a SQLite foundation for Bodega is incredibly faster than using the file system. The DiskStorageStorageEngine is still available, but if you use the SQLiteStorageEngine loading 10,000 objects into memory will be more than 400% faster, and writing 5,000 objects is more than 500% faster. With this release I feel confident that you should be able to use Bodega and Boutique in the largest of apps, while counterintuitively becoming a more flexible framework.
Breaking
Now that you can provide a StorageEngine the Store initializer goes from this
let animalsStore = Store<Animal>(
storagePath: Store<Animal>.documentsDirectory(appendingPath: "Animals"),
cacheIdentifier: \.id
)
This update's a big one!
This version update isn't just one version update, it's two. Boutique's 2.0 depends on Bodega version 2.0, which is a huge update in its own right. The Bring Your Own Database feature is powered by Bodega, which means you get all of the functionality with no API changes to Boutique. And of course it's still only a couple of lines of code to have an app with a single source of truth, realtime updates, the offline support you've come to know and love, and now 5-10x faster out of the box.
But before we talk about the database, let's see what else Boutique 2.0 has to offer.
@StoredValue
Most data your app works with is in the shape of an array, but sometimes you need to store a single value. That's what
@StoredValue
is for. As the name implies@StoredValue
allows you to store a value, which is great for saving user preferences, configurations, or even individual value likelastOpenedDate
.Creating a
@StoredValue
is easy, it even supports default values like you would expect with any other Swift property.A more complex example may look like this, for example if you were building a Youtube-like video app.
Thank you to @iankeen for helping me iterate on
@StoredValue
, and working through some nuances as the final version took shape.AppKit/UIKit support
This one does what it says on the tin, Boutique is no longer constrained to SwiftUI.
@Stored
and the new@StoredValue
will work in UIKit and AppKit apps!Chained Operations
This is a breaking change, but a very worthwhile one. Previously when you added an item there was an
removingExistingItems
parameter that would provide a form of cache invalidation. But as they say, the two hardest problems in computer science are naming, cache invalidation, and off by one errors, so let's fix all three in one fell swoop.What used to look like this
Now becomes much simpler
The reason for the
removingExistingItems
parameter was to remove cached items and add new items in one operation, preventing multiple dispatches to the@MainActor
. We wanted to avoid multiple dispatches to avoid multiple SwiftUI render cycles, and now we can avoid that thanks toOperation
chaining. But what isOperation
? AnOperation
is a type you never have to think about, but it allows us to chain commands together transparently, like this.This fluent syntax is much more intuitive, and no longer do you have a confusing parameter that conflates cache invalidation and adding items due to an unexpected side effect of how SwiftUI renders occur.
Thank you to @davedelong for helping me think through and prototyping chained operations, I really appreciate what came to be and wouldn't have gotten there without his help.
defaultStorageDirectory
Previously the default folder location a
Store
was initialized was theDocuments
directory. This makes sense on iOS, tvOS, and more locked down platforms, but on macOS it makes more sense to store data in theApplication Support
folder. Support fordefaultStorageDirectory
comes from Bodega, but if you're initializing a BoutiqueStore
the location will now default to the expected folder on each platform.Bring Your Own Database
In the Version 1.x series of Bodega the
DiskStorage
type was responsible for persisting data to disk. As the name impliesDiskStorage
was backed by the file system, but what if you don't want to saveData
to disk? Saving data to disk is a simple and effective starting point, but can get slow when working with large data sets. One of Bodega's goals is to work with every app without causing developers to make tradeoffs, so version 2.0 is focused on eliminating those tradeoffs without ruining the streamlined simplicity Bodega brings, and brings that to Boutique.In the spirit of not making tradeoffs here's how Bodega works with any database you want, say hello to the new
StorageEngine
protocol.By providing your own
write
,read
,remove
,key
, andtimestamp
related functions, you can make any persistence layer compatible withObjectStorage
. Whether your app is backed by Realm, Core Data, or even CloudKit, when you create a newStorageEngine
it automatically becomes usable byObjectStorage
, with one drop dead simple API.The first
StorageEngine
to be implemented is anSQLiteStorageEngine
, bundled with Bodega. I'll explain all the possibilities below, but first let's take a second to see how much faster your apps using Bodega and Boutique will be.If it's not obvious, a SQLite foundation for Bodega is incredibly faster than using the file system. The
DiskStorageStorageEngine
is still available, but if you use theSQLiteStorageEngine
loading 10,000 objects into memory will be more than 400% faster, and writing 5,000 objects is more than 500% faster. With this release I feel confident that you should be able to use Bodega and Boutique in the largest of apps, while counterintuitively becoming a more flexible framework.Breaking
Now that you can provide a
StorageEngine
theStore
initializer goes from thisTo this
Or even simpler if you use the new default
SQLiteStorageEngine
in the defaultData
database.For a backwards compatible
StorageEngine
you can use the newDiskStorageEngine
, which was powering your data in v1.P.S. If you build something useful to others, by all means file a pull request so I can add it to Boutique!