Unleashing the real power of Core Data with the elegance and safety of Swift
Swift 2.7 is bundled with Xcode 14, and CoreStore 9.0.0 will be the officially supported version from here on out.
Full Changelog: https://github.com/JohnEstropia/CoreStore/compare/8.1.0...9.0.0
RxSwift utilities are available through the RxCoreStore external module.
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 MigrationProgress
enum
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:
dataStack.reactive
.importUniqueObjects(
Into<Person>(),
sourceArray: [
["name": "John"],
["name": "Bob"],
["name": "Joe"]
]
)
.sink(
receiveCompletion: { result in
// ...
},
receiveValue: { (people) in
XCTAssertEqual(people?.count, 3)
// ...
}
)
.store(in: &cancellables)
ListPublisher.reactive
ListPublisher
s can be used to emit ListSnapshot
s 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
ObjectPublisher
s can be used to emit ObjectSnapshot
s 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)
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 View
s.
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") }
)
}
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.
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>]
|
Signature:
ForEach(objectIn: ListSnapshot<O>)
Closure:
ObjectPublisher<O>
|
let listSnapshot: ListSnapshot<Person>
|
Signature:
ForEach(objectIn: [ObjectSnapshot<O>])
Closure:
ObjectPublisher<O>
|
let array: [ObjectSnapshot<Person>]
|
Signature:
ForEach(sectionIn: ListSnapshot<O>)
Closure:
[ListSnapshot<O>.SectionInfo]
|
let listSnapshot: ListSnapshot<Person>
|
Signature:
ForEach(objectIn: ListSnapshot<O>.SectionInfo)
Closure:
ObjectPublisher<O>
|
let listSnapshot: ListSnapshot<Person>
|
The old CoreStoreDemo app has been renamed to LegacyDemo, and a new Demo app now showcases CoreStore features through SwiftUI:
Don't worry, standard UIKit samples are also available (thanks to UIViewControllerRepresentable
)
Feel free to suggest improvements to the Demo app!
CoreStore now compiles using Xcode 12 and Swift 5.3!
⚠️ There was a bug in Swift 5.3 propertyWrappers
where Segmentation Faults happen during compile time. CoreStore was able to work around this issue through runtime fatalError
s, but the result is that missing required parameters for @Field
properties may not be caught during compile-time. The runtime checks crash if there are missing parameters, so please take care to debug your models!
One common mistake when assigning default values to CoreStoreObject
properties is to assign it a value and expect it to be evaluated whenever an object is created:
// ❌
class Person: CoreStoreObject {
@Field.Stored("identifier")
var identifier: UUID = UUID() // Wrong!
@Field.Stored("createdDate")
var createdDate: Date = Date() // Wrong!
}
This default value will be evaluated only when the DataStack
sets up the schema, and all instances will end up having the same values. This syntax for "default values" are usually used only for actual reasonable constant values, or sentinel values such as ""
or 0
.
For actual "initial values", @Field.Stored
and @Field.Coded
now supports dynamic evaluation during object creation via the dynamicInitialValue:
argument:
// ✅
class Person: CoreStoreObject {
@Field.Stored("identifier", dynamicInitialValue: { UUID() })
var identifier: UUID
@Field.Stored("createdDate", dynamicInitialValue: { Date() })
var createdDate: Date
}
When using this feature, a "default value" should not be assigned (i.e. no =
expression).
⚠️ These changes apply only to CoreStoreObject
subclasses, notNSManagedObject
s.
‼️ Please take note of the warnings below before migrating or else the model's hash might change.
If conversion is too risky, the current Value.Required
, Value.Optional
, Transformable.Required
, Transformable.Optional
, Relationship.ToOne
, Relationship.ToManyOrdered
, and Relationship.ToManyUnordered
will all be supported for while so you can opt to use them as is for now.
‼️ If you are confident about conversion, I cannot stress this enough, but please make sure to set your schema's VersionLock
before converting!
@Field.Stored
(replacement for non "transient" Value.Required
and Value.Optional
)class Person: CoreStoreObject {
@Field.Stored("title")
var title: String = "Mr."
@Field.Stored("nickname")
var nickname: String?
}
⚠️ Only Value.Required
and Value.Optional
that are NOT transient values can be converted to Field.Stored
.
⚠️ When converting, make sure that all parameters, including the default values, are exactly the same or else the model's hash might change.
@Field.Virtual
(replacement for "transient" versions of Value.Required
andValue.Optional
)class Animal: CoreStoreObject {
@Field.Virtual(
"pluralName",
customGetter: { (object, field) in
return object.$species.value + "s"
}
)
var pluralName: String
@Field.Stored("species")
var species: String = ""
}
⚠️ Only Value.Required
and Value.Optional
that ARE transient values can be converted to Field.Virtual
.
⚠️ When converting, make sure that all parameters, including the default values, are exactly the same or else the model's hash might change.
@Field.Coded
(replacement for Transformable.Required
andTransformable.Optional
, with additional support for custom encoders such as JSON)class Person: CoreStoreObject {
@Field.Coded(
"bloodType",
coder: {
encode: { $0.toData() },
decode: { BloodType(fromData: $0) }
}
)
var bloodType: BloodType?
}
‼️ The current Transformable.Required
and Transformable.Optional
mechanism have no safe conversion to @Field.Coded
. Please use @Field.Coded
only for newly added attributes.
@Field.Relationship
(replacement for Relationship.ToOne
, Relationship.ToManyOrdered
, and Relationship.ToManyUnordered
)class Pet: CoreStoreObject {
@Field.Relationship("master")
var master: Person?
}
class Person: CoreStoreObject {
@Field.Relationship("pets", inverse: \.$master)
var pets: Set<Pet>
}
⚠️ Relationship.ToOne<T>
maps to T?
, Relationship.ToManyOrdered
maps to Array<T>
, and Relationship.ToManyUnordered
maps to Set<T>
⚠️ When converting, make sure that all parameters, including the default values, are exactly the same or else the model's hash might change.
Before diving into the properties themselves, note that they will effectively force you to use a different syntax for queries:
From<Person>.where(\.title == "Mr.")
From<Person>.where(\.$title == "Mr.")
There are a several advantages to using these Property Wrappers:
@propertyWrapper
versions will be magnitudes performant and efficient than their current implementations. Currently Mirror
reflection is used a lot to inject the NSManagedObject
reference into the properties. With @propertyWrapper
s this will be synthesized by the compiler for us. (See https://github.com/apple/swift/pull/25884)@propertyWrapper
versions, being struct
s, will give the compiler a lot more room for optimizations which were not possible before due to the need for mutable classes.ObjectSnapshot
s and ObjectPublisher
s by declaring them as @Field.Virtual
. Note that for ObjectSnapshot
s, the computed values are evaluated only once during creation and are not recomputed afterwards.The only disadvantage will be:
@propertyWrapper
s
(But the legacy ones will remain available for quite a while, so while it is recommended to migrate soon, no need to panic)⚠️This update will break current code. Make sure to read the changes below:
Starting version 7.0.0
, CoreStore will be using a lot of Swift 5.1 features, both internally and in its public API. You can keep using the last 6.3.2
release if you still need Swift 5.0.
The CoreStore
-namespaced API has been deprecated in favor of DataStack
method calls. If you are using the global utilities such as CoreStore.defaultStack
and CoreStore.logger
, a new CoreStoreDefaults
namespace has been provided:
CoreStore.defaultStack
-> CoreStoreDefaults.dataStack
CoreStore.logger
-> CoreStoreDefaults.logger
CoreStore.addStorage(...)
-> CoreStoreDefaults.dataStack.addStorage(...)
CoreStore.fetchAll(...)
-> CoreStoreDefaults.dataStack.fetchAll(...)
If you have been using your own properties to store DataStack
references, then you should not be affected by this change.
UITableViews
and UICollectionViews
now have a new ally: ListPublisher
s provide diffable snapshots that make reloading animations very easy and very safe. Say goodbye to UITableViews
and UICollectionViews
reload errors!
self.dataSource = DiffableDataSource.CollectionView<Person>(
collectionView: self.collectionView,
dataStack: CoreStoreDefaults.dataStack,
cellProvider: { (collectionView, indexPath, person) in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PersonCell") as! PersonCell
cell.setPerson(person)
return cell
}
)
This is now the recommended method of reloading UITableView
s and UICollectionView
s because it uses list diffing to update your list views. This means that it is a lot less prone to cause layout errors.
ListPublisher
is a more lightweight counterpart of ListMonitor
. Unlike ListMonitor
, it does not keep track of minute inserts, deletes, moves, and updates. It simply updates its snapshot
property which is a struct
storing the list state at a specific point in time. This ListSnapshot
is then usable with the DiffableDataSource
utilities (See section above).
self.listPublisher = dataStack.listPublisher(
From<Person>()
.sectionBy(\.age") { "Age \($0)" } // sections are optional
.where(\.title == "Engineer")
.orderBy(.ascending(\.lastName))
)
self.listPublisher.addObserver(self) { [weak self] (listPublisher) in
self?.dataSource?.apply(
listPublisher.snapshot, animatingDifferences: true
)
}
ListSnapshot
s store only NSManagedObjectID
s and their sections.
ObjectPublisher
is a more lightweight counterpart of ObjectMonitor
. Unlike ObjectMonitor
, it does not keep track of per-property changes. You can create an ObjectPublisher
from the object directly:
let objectPublisher: ObjectPublisher<Person> = person.asPublisher(in: dataStack)
or by indexing a ListPublisher
's ListSnapshot
:
let objectPublisher = self.listPublisher.snapshot[indexPath]
The ObjectPublisher
exposes a snapshot
property which returns an ObjectSnapshot
, which is a lazily generated struct
containing fully-copied property values.
objectPublisher.addObserver(self) { [weak self] (objectPublisher) in
let snapshot: ObjectSnapshot<Person> = objectPublisher.snapshot
// handle changes
}
This snapshot is completely thread-safe, and any mutations to it will not affect the actual object.
CoreStore is slowly moving to abstract object utilities based on usage intent.
NSManageObject',
CoreStoreObject,
ObjectPublisher, and
ObjectSnapshotall conform to the
ObjectRepresentation` protocol, which allows conversion of each type to another:
public protocol ObjectRepresentation {
associatedtype ObjectType : CoreStore.DynamicObject
func objectID() -> ObjectType.ObjectID
func asPublisher(in dataStack: DataStack) -> ObjectPublisher<ObjectType>
func asReadOnly(in dataStack: DataStack) -> ObjectType?
func asEditable(in transaction: BaseDataTransaction) -> ObjectType?
func asSnapshot(in dataStack: DataStack) -> ObjectSnapshot<ObjectType>?
func asSnapshot(in transaction: BaseDataTransaction) -> ObjectSnapshot<ObjectType>?
}
ObjectMonitor
being excluded in this family was intentional; its initialization is complex enough to be an API of its own.
SetupResult<T>
, MigrationResult
, and AsynchronousDataTransaction.Result<T>
have all been converted into typealias
es for Swift.Result<T, CoreStoreError>
. The benefit is we can now use the utility methods on Swift.Result
such as map()
, mapError()
, etc. Their Objective-C counterparts (CSSetupResult
, etc.) remain available and can still be used as before.⚠️This update will break current code. Make sure to read the changes below:
iOS 10
, macOS 10.12
, tvOS 10
, watchOS 3
ICloudStore
and ICloudStoreObserver
s are now officially deprecated (iCloud Core Data had been deprecated quite a long time ago).throws
an error of type CoreStoreError.persistentStoreNotFound(DynamicObject.Type)
when the specified entity does not exist in any storage. This is to distinguish difference between non-existing objects versus non-existing stores.// Before
if let object = CoreStore.fetchOne(...) {
// ...
}
else {
// May be nil because `addStorage()` hasn't completed yet or because nothing really matches the query
}
// After
do {
if let object = try CoreStore.fetchOne(...) {
// ...
}
else {
// `addStorage()` has completed but nothing matches the query
}
}
catch {
// method was called before `addStorage()` completed
}
If you are sure you won't encounter cases where fetches happen before a storage is added to the DataStack
, simply add try!
to your fetch*()
and query*()
method calls.
CoreStoreObject
s (as well as their PartialObject
counterparts) now conform to CustomDebugStringConvertable
by default.CoreStoreObject
s now assert on property names that possibly collide with reserved names such as description
CoreStoreObject
properties can now be observed individually without the need for ObjectMonitor
s. The API is a bit similar to the standard KVO API:// NSManagedObject
let observer = person.observe(\.friends, options: [.new, .old]) { (person, change) in
// ...
}
// CoreStoreObject
let observer = person.friends.observe(options: [.new, .old]) { (person, change) in
// ...
}
You may still use ObjectMonitor
s especially for observing multiple changes at once.
pod try CoreStore
if you don't have it locally). You will need to build the CoreStore iOS
schema at least once to create the framework used by the Playgrounds.UnsafeDataTransaction
s (https://github.com/JohnEstropia/CoreStore/pull/275)ListMonitor
access performance boost (https://github.com/JohnEstropia/CoreStore/pull/287, https://github.com/JohnEstropia/CoreStore/pull/288)CoreStoreError.asynchronousMigrationRequired(URL)
for cases when addStorageAndWait()
is used together with .allowSynchronousLightweightMigration
but migrations are only allowed asynchronously (#277)InferredSchemaMappingProvider
not respecting renaming identifiers (https://github.com/JohnEstropia/CoreStore/pull/301)This release builds on Swift 4.
Most fetch and query clauses such as Where
, OrderBy
, etc. are now generic types. In effect, clauses using the previous fetching/querying calls need to indicate their generic type:
var people = CoreStore.fetchAll(
From<MyPersonEntity>(),
Where<MyPersonEntity>("%K > %d", "age", 30),
OrderBy<MyPersonEntity>(.ascending("name"))
)
This is quite repetitive. To make this even more convenient, CoreStore now has fetch/query builders.
var people = CoreStore.fetchAll(
From<MyPersonEntity>()
.where(format: "%K > %d", "age", 30)
.orderBy(.ascending("name"))
)
This way the generic type is propagated onto subsequent clauses. But we can improve this further.
Using Swift 4's type-safe keypaths, we can make our fetch/query expressions even more solid.
var people = CoreStore.fetchAll(
From<MyPersonEntity>()
.where(\.age > 30)
.orderBy(.ascending(\.name))
)
All CoreStore API that accepts a string keypath now accepts these Smart Keypaths. (If I missed some, please post a github issue)
typealias KeyPath = String
was changed to typealias RawKeyPath = String