Shush is your favorite librarian. Its goal is to make sure your simple data persistence needs are met.
Have you ever had a couple user preferences you'd like to sync without having to think (I know, I know) too much ? What about an array of the last 30 queries your user ran in your app ? Or some lightweight user generated data that you want persisted and synchronized using iCloud ?
Those are all handled easily, while trying its best to stay out of your way.
Let's imagine you have a simple browser app where your user can save favorites and archive pages.
Note
All following property wrappers and constructs have variants depending on your need to use iCloud sync or not. To use it you need to already have setup your iCloud and ubiquitous containers entitlements, see the official documentation
import Shush
extension NSNotification.Name {
static let settingsChanged = NSNotification.Name("settings_changed")
}
struct Preferences {
let shared = Preferences()
@ShushValue(key: "using_dark_theme", defaultValue: false, notification: .settingsChanged)
var usingDarkTheme: Bool
}
That's it! You now have access to Preferences.shared.usingDarkTheme
across your whole app and will be notified when it changes, either from inside your app, of from another device.
Let's create our struct
struct WebFavorite: Codable, Identifiable {
let date: Date
let url: URL
var id: String { url.standardized.absoluteString }
}
Now let's persist it!
extension NSNotification.Name {
static let favoritesChanged = NSNotification.Name("favorites_changed")
}
struct Preferences {
...
@ShushValues(prefix: "favorite", sortedBy: \.date, order: .desc, notification: .favoritesChanged)
var favorites: [WebFavorite]
func insertFavorite(_ favorite: WebFavorite) {
_favorites.insert(favorite)
}
}
As above, you'll now be alerted each time an item changes, from a device or another. This will create multiple UserDefaults
and NSUbiquitousKeyValueStore
keys, each associated to one element of your array, allowing for conflict-free updates on all devices.
Let's imagine a simple archive structure. The archiveData
attribute could for instance contain a PDF version of the archived page.
struct WebArchive {
let date: Date
let url: URL
let archiveData: Data
}
How do we synchronize this ? You'll need to conform to Persistable
. In the spirit of avoiding unnecessary decoding, you'll also need a secondary struct that holds your archive metadata. Here is our updated code :
struct WebArchiveMetadata {
let date: Date
let url: URL
}
extension WebArchiveMetadata: PersistablePartial {}
extension WebArchive: Persistable {
static var fileExtension: String {
return "webarchive"
}
var partialRepresentation: WebArchiveMetadata {
return .init(date: date, url: url)
}
static func decodePersisted(_ data: Data) throws -> WebArchive {
return try JSONDecoder().decode(WebArchive.self, from: data)
}
static func decodePersistedPartially(_ data: Data) throws -> WebArchiveMetadata {
return try JSONDecoder().decode(WebArchiveMetadata.self, from: data)
}
static func encodePersisted(_ data: WebArchive) throws -> Data {
return try JSONEncoder().encode(data)
}
}
extension WebArchive: PersistableIdentifiable {
static func suggestedFilename(for persistable: WebArchive) -> String {
return "Archive \(date)"
}
}
And now let's persist it!
import Shush
extension NSNotification.Name {
static let archivesChanged = NSNotification.Name("archives_changed")
}
struct Preferences {
...
let archives: ShushFiles<WebArchive, Date> = .init(
ubiquityContainer: "iCloud.com.example.App",
sortedBy: \.partial.date, order: .desc,
notification: .archivesChanged
)
}
You can now easily:
// List your files
Preferences.shared.archives.files
// Insert a file
let archive = ...
let archiveFile = Preferences.shared.archives.insert(archive)
// Read the full content of a file
let fullContent = Preferences.shared.archives.read(archiveFile)
// Delete a file
Preferences.shared.files.remove(archiveFile)
Use it as you like in every project you want, redistribute as much as you want, preferably with mentions of my name when it applies and don't blame me if it breaks :)
-- dvkch