Skip to content

Releases: JohnEstropia/CoreStore

9.3.0

31 Oct 08:09
Compare
Choose a tag to compare

I'm currently rethinking a lot of CoreStore's architectural design around Swift concurrency, and I'm planning 9.3.0 to be the last major version that will support unstructured concurrency. If you have ideas or suggestions for CoreStore's direction with regards to Swift concurrency (or other Swift 6 features), feel free to post a Github issue.

What's Changed

  • Swift's new "Typed Throws" are now fully supported in relevant methods
  • issue #509 | Xcode 16.0 beta 6 error - "'CATransaction' is unavailable in watchOS" by @DavidTiimo in #510

New Contributors

Full Changelog: 9.2.0...9.3.0

Swift 5.9 update

24 Jun 05:23
Compare
Choose a tag to compare

What's Changed

Full Changelog: 9.1.0...9.2.0

Swift 5.7 update

24 Jun 05:21
Compare
Choose a tag to compare

Xcode 14 (Swift 2.7), min iOS 13 support

29 Sep 01:07
Compare
Choose a tag to compare

Swift 2.7 is bundled with Xcode 14, and CoreStore 9.0.0 will be the officially supported version from here on out.

Breaking changes:

  • Removal of Objective-C support (which had been deprecated for a long time now)
  • Migration to Swift 2.7 from Swift 2.4
  • Bumped minimum supported version to iOS 13 from iOS 11. While this double jump had never been done in CoreStore before, we are aiming to fully utilize Combine utilities internally and to make the shift to Swift concurrency (which requires at least iOS 13) as smooth as possible.

Full Changelog: 8.1.0...9.0.0

Reactive Programming and SwiftUI

11 Apr 07:03
Compare
Choose a tag to compare

Reactive Programming

RxSwift

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 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

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):

DataExample
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
            
            // ...
        }
    }
}
...
Read more

New Demo app, Swift 5.3 / Xcode 12 / iOS 14 Support

19 Sep 06:38
Compare
Choose a tag to compare

New Demo app

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!

Swift 5.3 / Xcode 12 / iOS 14 Support

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 fatalErrors, 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!

dynamicInitialValue support for @Field.Stored and @Field.Coded properties

20 Jun 08:48
e720504
Compare
Choose a tag to compare

Default values vs. Initial values

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).

Swift 5.2 / Xcode 11.4, New `Field` PropertyWrappers

27 Mar 03:22
Compare
Choose a tag to compare

Maintenance updates

  • Xcode 11.4 and Swift 5.2 support

New Property Wrappers syntax

⚠️ These changes apply only to CoreStoreObject subclasses, notNSManagedObjects.

‼️ 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.

Usage

Before diving into the properties themselves, note that they will effectively force you to use a different syntax for queries:

  • Before: From<Person>.where(\.title == "Mr.")
  • After: From<Person>.where(\.$title == "Mr.")

There are a several advantages to using these Property Wrappers:

  • The @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 @propertyWrappers this will be synthesized by the compiler for us. (See swiftlang/swift#25884)
  • The @propertyWrapper versions, being structs, will give the compiler a lot more room for optimizations which were not possible before due to the need for mutable classes.
  • You can now add computed properties that are accessible to both ObjectSnapshots and ObjectPublishers by declaring them as @Field.Virtual. Note that for ObjectSnapshots, the computed values are evaluated only once during creation and are not recomputed afterwards.

The only disadvantage will be:

  • You need to update your code by hand to migrate to the new @propertyWrappers
    (But the legacy ones will remain available for quite a while, so while it is recommended to migrate soon, no need to panic)

Swift 5.1 (Breaking Changes), ListPublisher, ObjectPublisher, DiffableDataSources

22 Oct 08:37
Compare
Choose a tag to compare

⚠️This update will break current code. Make sure to read the changes below:

Breaking Changes

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.

Deprecations

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(...)
  • etc.

If you have been using your own properties to store DataStack references, then you should not be affected by this change.

New features

Backwards-portable DiffableDataSources implementation

UITableViews and UICollectionViews now have a new ally: ListPublishers provide diffable snapshots that make reloading animations very easy and very safe. Say goodbye to UITableViews and UICollectionViews reload errors!

DiffableDataSource.CollectionView (iOS and macOS) and DiffableDataSource.TableView (iOS)

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 UITableViews and UICollectionViews because it uses list diffing to update your list views. This means that it is a lot less prone to cause layout errors.

ListPublisher and ListSnapshot

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
   )
}

ListSnapshots store only NSManagedObjectIDs and their sections.

ObjectPublisher and ObjectSnapshot

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.

Intent-based Object representations

CoreStore is slowly moving to abstract object utilities based on usage intent.
NSManageObject', CoreStoreObject, ObjectPublisher, and ObjectSnapshotall conform to theObjectRepresentation` 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.

Swift 5 + Deprecation Cleanup

01 Apr 03:24
Compare
Choose a tag to compare
  • CoreStore now builds on Swift 5 and Xcode 10.2
  • SetupResult<T>, MigrationResult, and AsynchronousDataTransaction.Result<T> have all been converted into typealiases 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.
  • Bunch of deprecated/obsoleted stuff deleted
  • CoreData iCloud support had been deprecated for a while now and CoreStore finally removes its support in this version. If you wish to continue using it please continue to use the 6.2.x versions but it will be unlikely to get bugfixes from here on out so please try to migrate your app's data as soon as possible (iOS and macOS already had this deprecated for years)