RxSwift utilities are available through the RxCoreStore external module.
Combine
Combine publishers are available from the DataStack, ListPublisher, and ObjectPublisher's .reactive namespace property.
DataStack.reactive
Adding a storage through DataStack.reactive.addStorage(_:) returns a publisher that reports a MigrationProgressenum value. The .migrating value is only emitted if the storage goes through a migration.
dataStack.reactive
.addStorage(
SQLiteStore(fileName: "core_data.sqlite")
)
.sink(
receiveCompletion: { result in
// ...
},
receiveValue: { (progress) in
print("\(round(progress.fractionCompleted * 100)) %") // 0.0 ~ 1.0
switch progress {
case .migrating(let storage, let nsProgress):
// ...
case .finished(let storage, let migrationRequired):
// ...
}
}
)
.store(in: &cancellables)
Transactions are also available as publishers through DataStack.reactive.perform(_:), which returns a Combine Future that emits any type returned from the closure parameter:
dataStack.reactive
.perform(
asynchronous: { (transaction) -> (inserted: Set<NSManagedObject>, deleted: Set<NSManagedObject>) in
// ...
return (
transaction.insertedObjects(),
transaction.deletedObjects()
)
}
)
.sink(
receiveCompletion: { result in
// ...
},
receiveValue: { value in
let inserted = dataStack.fetchExisting(value0.inserted)
let deleted = dataStack.fetchExisting(value0.deleted)
// ...
}
)
.store(in: &cancellables)
For importing convenience, ImportableObject and ImportableUniqueObjects can be imported directly through DataStack.reactive.import[Unique]Object(_:source:) and DataStack.reactive.import[Unique]Objects(_:sourceArray:) without having to create a transaction block. In this case the publisher emits objects that are already usable directly from the main queue:
ListPublishers can be used to emit ListSnapshots through Combine using ListPublisher.reactive.snapshot(emitInitialValue:). The snapshot values are emitted in the main queue:
listPublisher.reactive
.snapshot(emitInitialValue: true)
.sink(
receiveCompletion: { result in
// ...
},
receiveValue: { (listSnapshot) in
dataSource.apply(
listSnapshot,
animatingDifferences: true
)
}
)
.store(in: &cancellables)
ObjectPublisher.reactive
ObjectPublishers can be used to emit ObjectSnapshots through Combine using ObjectPublisher.reactive.snapshot(emitInitialValue:). The snapshot values are emitted in the main queue:
objectPublisher.reactive
.snapshot(emitInitialValue: true)
.sink(
receiveCompletion: { result in
// ...
},
receiveValue: { (objectSnapshot) in
tableViewCell.setObject(objectSnapshot)
}
)
.store(in: &tableViewCell.cancellables)
SwiftUI Utilities
Observing list and object changes in SwiftUI can be done through a couple of approaches. One is by creating views that autoupdates their contents, or by declaring property wrappers that trigger view updates. Both approaches are implemented almost the same internally, but this lets you be flexible depending on the structure of your custom Views.
SwiftUI Views
CoreStore provides View containers that automatically update their contents when data changes.
ListReader
A ListReader observes changes to a ListPublisher and creates its content views dynamically. The builder closure receives a ListSnapshot value that can be used to create the contents:
let people: ListPublisher<Person>
var body: some View {
List {
ListReader(self.people) { listSnapshot in
ForEach(objectIn: listSnapshot) { person in
// ...
}
}
}
.animation(.default)
}
As shown above, a typical use case is to use it together with CoreStore's ForEach extensions.
A KeyPath can also be optionally provided to extract specific properties of the ListSnapshot:
let people: ListPublisher<Person>
var body: some View {
ListReader(self.people, keyPath: \.count) { count in
Text("Number of members: \(count)")
}
}
ObjectReader
An ObjectReader observes changes to an ObjectPublisher and creates its content views dynamically. The builder closure receives an ObjectSnapshot value that can be used to create the contents:
let person: ObjectPublisher<Person>
var body: some View {
ObjectReader(self.person) { objectSnapshot in
// ...
}
.animation(.default)
}
A KeyPath can also be optionally provided to extract specific properties of the ObjectSnapshot:
let person: ObjectPublisher<Person>
var body: some View {
ObjectReader(self.person, keyPath: \.fullName) { fullName in
Text("Name: \(fullName)")
}
}
By default, an ObjectReader does not create its views wheen the object observed is deleted from the store. In those cases, the placeholder: argument can be used to provide a custom View to display when the object is deleted:
let person: ObjectPublisher<Person>
var body: some View {
ObjectReader(
self.person,
content: { objectSnapshot in
// ...
},
placeholder: { Text("Record not found") }
)
}
SwiftUI Property Wrappers
As an alternative to ListReader and ObjectReader, CoreStore also provides property wrappers that trigger view updates when the data changes.
ListState
A @ListState property exposes a ListSnapshot value that automatically updates to the latest changes.
@ListState
var people: ListSnapshot<Person>
init(listPublisher: ListPublisher<Person>) {
self._people = .init(listPublisher)
}
var body: some View {
List {
ForEach(objectIn: self.people) { objectSnapshot in
// ...
}
}
.animation(.default)
}
As shown above, a typical use case is to use it together with CoreStore's ForEach extensions.
If a ListPublisher instance is not available yet, the fetch can be done inline by providing the fetch clauses and the DataStack instance. By doing so the property can be declared without an initial value:
@ListState(
From<Person>()
.sectionBy(\.age)
.where(\.isMember == true)
.orderBy(.ascending(\.lastName))
)
var people: ListSnapshot<Person>
var body: some View {
List {
ForEach(sectionIn: self.people) { section in
Section(header: Text(section.sectionID)) {
ForEach(objectIn: section) { person in
// ...
}
}
}
}
.animation(.default)
}
For other initialization variants, refer to the ListState.swift source documentations.
ObjectState
An @ObjectState property exposes an optional ObjectSnapshot value that automatically updates to the latest changes.
@ObjectState
var person: ObjectSnapshot<Person>?
init(objectPublisher: ObjectPublisher<Person>) {
self._person = .init(objectPublisher)
}
var body: some View {
HStack {
if let person = self.person {
AsyncImage(person.$avatarURL)
Text(person.$fullName)
}
else {
Text("Record removed")
}
}
}
As shown above, the property's value will be nil if the object has been deleted, so this can be used to display placeholders if needed.
SwiftUI Extensions
For convenience, CoreStore provides extensions to the standard SwiftUI types.
ForEach
Several ForEach initializer overloads are available. Choose depending on your input data and the expected closure data. Refer to the table below (Take note of the argument labels as they are important):
Data
Example
Signature:
ForEach(_: [ObjectSnapshot<O>])
Closure:
ObjectSnapshot<O>
let array: [ObjectSnapshot<Person>]
var body: some View {
List {
ForEach(self.array) { objectSnapshot in
// ...
}
}
}
Signature:
ForEach(objectIn: ListSnapshot<O>)
Closure:
ObjectPublisher<O>
let listSnapshot: ListSnapshot<Person>
var body: some View {
List {
ForEach(objectIn: self.listSnapshot) { objectPublisher in
// ...
}
}
}
Signature:
ForEach(objectIn: [ObjectSnapshot<O>])
Closure:
ObjectPublisher<O>
let array: [ObjectSnapshot<Person>]
var body: some View {
List {
ForEach(objectIn: self.array) { objectPublisher in
// ...
}
}
}
Signature:
ForEach(sectionIn: ListSnapshot<O>)
Closure:
[ListSnapshot<O>.SectionInfo]
let listSnapshot: ListSnapshot<Person>
var body: some View {
List {
ForEach(sectionIn: self.listSnapshot) { sectionInfo in
// ...
}
}
}
Signature:
ForEach(objectIn: ListSnapshot<O>.SectionInfo)
Closure:
ObjectPublisher<O>
let listSnapshot: ListSnapshot<Person>
var body: some View {
List {
ForEach(sectionIn: self.listSnapshot) { sectionInfo in
ForEach(objectIn: sectionInfo) { objectPublisher in
// ...
}
}
}
}