Papyrus aims to hit the sweet spot between saving raw API responses to the file system and a fully fledged database like Realm.
struct Car: Papyrus {
let id: String
let model: String
let manufacturer: String
}
let car = Car(id: "abc...", model: "Model S", manufacturer: "Tesla")
let store = PapyrusStore()
await store.save(car)
In Xcode:
Project
.Package Dependencies
.+
.https://github.com/reddavis/Papyrus
.Papyrus
to your app target.Anything that conforms to the Papyrus
protocol can be stored.
The Papyrus
protocol is simply an umbrella of these three protocols:
Codable
Equatable
Identifiable where ID: LosslessStringConvertible
struct Car: Papyrus {
let id: String
let model: String
let manufacturer: String
}
let car = Car(id: "abc...", model: "Model S", manufacturer: "Tesla")
let store = PapyrusStore()
await store.save(car)
Papyrus also understands relationships. If we continue with our Car
modelling...Let's imagine we have an app that fetches a list of car manufacturers and their cars.
Our models could look like:
struct Manufacturer: Papyrus {
let id: String
let name: String
@HasMany let cars: [Car]
@HasOne let address: Address
}
struct Car: Papyrus {
let id: String
let model: String
}
struct Address: Papyrus {
let id: UUID
let lineOne: String
let lineTwo: String?
}
let modelS = Car(id: "abc...", model: "Model S")
let address = Address(id: UUID(), lineOne: "blah blah", lineTwo: nil)
let tesla = Manufacturer(
id: "abc...",
name: "Tesla",
cars: [modelS],
address: address
)
let store = PapyrusStore()
await store.save(tesla)
Because Car
and Address
also conforms to Papyrus
and the @HasMany
and @HasOne
property wrappers have been used, PapyrusStore
will also persist the cars and the address when it saves the manufacturer. This means that we are able to perform direct queries on Car
's and Address
es.
A common use case when dealing with API's is to fetch a collection of objects and the merge the results into your local collection.
Papyrus provides a function for this:
let carA = Car(id: "abc...", model: "Model S", manufacturer: "Tesla")
let carB = Car(id: "def...", model: "Model 3", manufacturer: "Tesla")
let carC = Car(id: "ghi...", model: "Model X", manufacturer: "Tesla")
let store = PapyrusStore()
store.save(objects: [carA, carB])
await store.merge(with: [carA, carC])
await store
.objects(type: Car.self)
.execute()
// #=> [carA, carC]
Fetching objects has two forms:
let store = PapyrusStore()
let tesla = try await store.object(id: "abc...", of: Manufacturer.self).execute()
You also have the option of a Publisher that will fire an event on first fetch and then when the object changes or is deleted.
When the object doesn't exist a PapyrusStore.QueryError
error is sent.
let store = PapyrusStore()
let cancellable = store.object(id: "abc...", of: Manufacturer.self)
.publisher()
.sink(
receiveCompletion: { ... },
receiveValue: { ... }
)
With Swift 5.5 came async/await, which also introduced AsyncSequence
.
When the object doesn't exist a PapyrusStore.QueryError
error is thrown.
let store = PapyrusStore()
let stream = store.object(id: "abc...", of: Manufacturer.self).stream()
do {
for try await object in stream {
...
}
} catch {
//.. Do something
}
Papryrus gives you the ability to fetch, filter and observe colletions of objects.
let manufacturers = await self.store
.objects(type: Manufacturer.self)
.execute()
let manufacturers = await self.store
.objects(type: Manufacturer.self)
.filter { $0.name == "Tesla" }
.execute()
let manufacturers = await self.store
.objects(type: Manufacturer.self)
.sort { $0.name < $1.name }
.execute()
Calling publisher()
on a PapryrusStore.CollectionQuery
object will return a Combine publisher which will emit the collection of objects. Unless specified the publisher will continue to emit a collection objects whenever a change is detected.
A change constitutes of:
self.store
.objects(type: Manufacturer.self)
.publisher()
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.main)
.sink { self.updateUI(with: $0) }
.store(in: &self.cancellables)
self.store
.objects(type: Manufacturer.self)
.filter { $0.name == "Tesla" }
.sort { $0.name < $1.name }
.publisher()
.subscribe(on: DispatchQueue.global())
.receive(on: DispatchQueue.main)
.sink { self.updateUI(with: $0) }
.store(in: &self.cancellables)
Calling stream()
on a PapryrusStore.CollectionQuery
object will return a AsyncThrowingStream
which will emit the collection of objects. Unless specified the stream will continue to emit a collection objects whenever a change is detected.
A change constitutes of:
let stream = self.store
.objects(type: Manufacturer.self)
.filter { $0.name == "Tesla" }
.sort { $0.name < $1.name }
.stream()
do {
for try await manufacturers in stream {
// ... Do something with [Manufacturer].
}
} catch {
//.. Do something
}
There are several methods for deleting objects.
let store = PapyrusStore()
let tesla = store.object(id: "abc...", of: Manufacturer.self)
await store.delete(tesla)
let store = PapyrusStore()
await store.delete(id: "abc...", of: Manufacturer.self)
let store = PapyrusStore()
let tesla = store.object(id: "abc...", of: Manufacturer.self)
let ford = store.object(id: "xyz...", of: Manufacturer.self)
await store.delete(objects: [tesla, ford])
If the wish is to keep existing data when introducing schema changes you can register a migration.
struct Car: Papyrus {
let id: String
let model: String
let manufacturer: String
}
struct CarV2: Papyrus {
let id: String
let model: String
let manufacturer: String
let year: Int
}
let migration = Migration<Car, CarV2> { oldObject in
CarV2(
id: oldObject.id,
model: oldObject.model,
manufacturer: oldObject.manufacturer,
year: 0
)
}
await self.store.register(migration: migration)
link |
Stars: 24 |
Last commit: 1 week ago |
Full Changelog: https://github.com/reddavis/Papyrus/compare/v0.11.0...v0.12.0
Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics