Coder Social home page Coder Social logo

data-swift / managedmodels Goto Github PK

View Code? Open in Web Editor NEW
92.0 3.0 2.0 227 KB

A SwiftData like `@Model` infrastructure for CoreData.

Home Page: https://www.alwaysrightinstitute.com/managedmodels/

License: Apache License 2.0

Swift 100.00%
coredata orm-framework persistence swift swiftdata swiftmacro

managedmodels's Introduction

ManagedModels for CoreData

Instead of wrapping CoreData, use it directly :-)

The key thing ManagedModels provides is a @Model macro, that works similar (but not identical) to the SwiftData @Model macro. It generates an NSManagedObjectModel straight from the code. I.e. no CoreData modeler / data model file is necessary.

A small sample model:

@Model class Item: NSManagedObject {
  var timestamp : Date
  var title     : String?
}
The full CoreData template application converted to ManagedModels
import SwiftUI
import ManagedModels

@Model class Item: NSManagedObject {
  var timestamp : Date
}

struct ContentView: View {

  @Environment(\.modelContext) private var viewContext
  
  @FetchRequest(sort: \.timestamp, animation: .default)
  private var items: FetchedResults<Item>
  
  var body: some View {
    NavigationView {
      List {
        ForEach(items) { item in
          NavigationLink {
            Text("Item at \(item.timestamp!, format: .dateTime)")
          } label: {
            Text("\(item.timestamp!, format: .dateTime)")
          }
        }
        .onDelete(perform: deleteItems)
      }
      .toolbar {
        ToolbarItem(placement: .navigationBarTrailing) {
          EditButton()
        }
        ToolbarItem {
          Button(action: addItem) {
            Label("Add Item", systemImage: "plus")
          }
        }
      }
      Text("Select an item")
    }
  }
  
  private func addItem() {
    withAnimation {
      let newItem = Item(context: viewContext)
      newItem.timestamp = Date()
      try! viewContext.save()
    }
  }
  
  private func deleteItems(offsets: IndexSet) {
    withAnimation {
      offsets.map { items[$0] }.forEach(viewContext.delete)
      try! viewContext.save()
    }
  }
}

#Preview {
  ContentView()
    .modelContainer(for: Item.self, inMemory: true)
}

This is not intended as a replacement implementation of SwiftData. I.e. the API is kept similar to SwiftData, but not exactly the same. It doesn't try to hide CoreData, but rather provides utilities to work with CoreData in a similar way to SwiftData.

A full To-Do list application example: ManagedToDos.app.

Blog article describing the thing: @Model for CoreData.

Requirements

The macro implementation requires Xcode 15/Swift 5.9 for compilation. The generated code itself though should backport way back to iOS 10 / macOS 10.12 though (when NSPersistentContainer was introduced).

Package URL:

https://github.com/Data-swift/ManagedModels.git

ManagedModels has no other dependencies.

Differences to SwiftData

  • The model class must explicitly inherit from NSManagedObject (superclasses can't be added by macros), e.g. @Model class Person: NSManagedObject.
  • Uses the CoreData @FetchRequest property wrapper instead @Query.
  • Doesn't use the new Observation framework (which requires iOS 17+), but uses ObservableObject (which is directly supported by CoreData).

TODO

  • Figure out whether we can do ordered attributes: Issue #1.
  • Support for "autosave": Issue #3
  • Support transformable types, not sure they work right yet: Issue #4
  • Generate property initializers if the user didn't specify any inits: Issue #5
  • Support SchemaMigrationPlan/MigrationStage: Issue #6
  • Write more tests.
  • Write DocC docs: Issue #7, Issue #8
  • Support for entity inheritance: Issue #9
  • Add support for originalName/versionHash in @Model: Issue 10
  • Generate "To Many" accessor function prototypes (addItemToGroup etc): Issue 11
  • Foundation Predicate support (would require iOS 17+) - this seems actually supported by CoreData!
    • SwiftUI @Query property wrapper/macro?: Issue 12
  • Figure out all the cloud sync options SwiftData has and whether CoreData can do them: Issue 13
  • Archiving/Unarchiving, required for migration.
  • Figure out whether we can add support for array toMany properties: Issue #2
  • Generate fetchRequest() class function.
  • Figure out whether we can allow initialized properties (var title = "No Title"): Issue 14

Pull requests are very welcome! Even just DocC documentation or more tests would be welcome contributions.

Links

Disclaimer

SwiftData and SwiftUI are trademarks owned by Apple Inc. Software maintained as a part of the this project is not affiliated with Apple Inc.

Who

ManagedModels are brought to you by Helge Heß / ZeeZide. We like feedback, GitHub stars, cool contract work, presumably any form of praise you can think of.

managedmodels's People

Contributors

admkopec avatar helje5 avatar radianttap avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

managedmodels's Issues

`NSEntityDescription` can only be used in a single MOM

I guess it is OK, but one entity can only be used in a single MOM, otherwise it throws an internal inconsistency exception:

Can't use an entity in two models.

There is no really nice way around it, but internally MOMs are already uniqued based on the input types, so that is probably not a huge deal in practice.

Also, MOMs do not seem to go away when they are released. Or the entities keep a backref to them:

    try autoreleasepool { // doesn't help
      let schema1 = NSManagedObjectModel(
    }
    // Can't use an entity in two models. (NSInternalInconsistencyException)
    let schema2 = NSManagedObjectModel(
      [ Fixtures.PersonAddressSchema.Person.self ],
      schemaCache: cache
    )

Not sure there is a way around this. Entities could be copied, but they are likely still tied to the classes?

Auto generate property initialisers

If the dev didn't specify an own initialiser, the macro should generate one, similar to what Swift itself already does for structures.

E.g.:

@Model class Item: NSManagedObject {
  var date : Date
}

Should generate:

convenience init(date: Date) {
  self.init()
  self.date = date
}

The code for this is already prepared in the Model macro, needs to be finished.

Can we flatten structs?

Looks like SwiftData flattens Codable structure into own table columns vs storing them as JSON.
Presumably to be able to run queries against such.

E.g.

@Model class Person {
  struct Address: Codable {
    var street: String
    var city: String
  }
  var privateAddress : Address
}

Even though the property is just one in the model, I think it ends up in separate columns in SQLite. Presumably to allow this:

#Predicate {
  $0.privateAddress.street = "XYZ"
}

Which seems valuable?

Not sure how we would hook that up in Core Data yet, I'd guess the Entity would need to get attributes for those.

Check what has to be done for iCloud syncing

SwiftData seems to have more options that it passes to the ModelContainer. No idea what CoreData supports here and how those things should be mapped.

The related option types are already available.

Do not generate `init()` if the user specified an init w/ defaults

E.g. this conflicts w/ the plain init that the macro generates. The macro needs to check whether all parameters are initialized.

    convenience init(timestamp: Date = Date(), title: String? = nil, age: String? = nil) {
      self.init()
      self.timestamp = timestamp
      self.title = title
      self.age = age
    }

Optional Codable property issue

After dropping CodableBox a new issue arose when declaring entity attribute as an optional codable type. The NSAttributeDescription's isOptional value is set to false, which is invalid and results in "NSLocalizedDescription=%{PROPERTY}@ is a required value., NSValidationErrorKey=optionalSip, NSValidationErrorValue=null" error upon save when the attribute value is nil.

Usage Tagged (safe) types in CoreData

Hi @helje5
Thank you for quick solve previous issue

I use Tagged
Tagged structure also have Codable protocol realisation like RawValue type
Example

struct AccessTokenTagged {}
typealias AccessToken = Tagged<AccessTokenTagged, String>

@Model
final class StoredAccess: NSManagedObject {
    var token: AccessToken
    ...
}

When compilation as result we can see

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unacceptable type of value for attribute: property = "token"; desired type = NSString; given type = _TtGC13ManagedModels10CodableBoxGV6Tagged6TaggedV5WE_UC17AccessTokenTaggedSS__; value = <_TtGC13ManagedModels10CodableBoxGV6Tagged6TaggedV5WE_UC17AccessTokenTaggedSS__: 0x600000336460>.'

It's strange, because ManagedModels use CodableBox for wrap Codable types. How we can solve it issue in current realisation?

Fatal error when work with Codable type

Please, help me understand crash reason

@Model 
final class StoredAccess: NSManagedObject {
    var token: AccessToken
    var expires: Date
    var accessLevel: AccessLevel
    var mainAccount: AccountID
    var userID: UserID
    var sip: AccessSIP
    var deviceID: DeviceID
}

struct AccessSIP: Codable {
    var username: String
    var password: String
    var realm: String
}

Log in console

ManagedModels/NSAttributeDescription+Data.swift:97: Fatal error: Unsupported Attribute value type AccessSIP

Strange exception thrown when assigning a value to optional to-one relationship

I have a Track and Album entities, where track has optional to-one .album relationship.

@Model class Track: NSManagedObject, Decodable {
	@Relationship(inverse: \Album.tracks)
	var album: Album?
}

@Model class Album: NSManagedObject, Decodable {
	var tracks: Set<Track> = []
}

When I try to assign a value to this album property, I get very strange crash that [Album count] is "unrecognized selector sent to instance". But don't see what could possibly be calling count.

Here's some screenshot of stack frames from the moment exception happens (it's different threads because this is from different attempts - each attempt gives identical frame stack):

Screenshot 2023-10-01 at 14 03 26 Screenshot 2023-10-01 at 17 49 35 Screenshot 2023-10-01 at 17 48 51

Do you have any idea if this could be something in the PersistentModel implementation..?

Can we support Swift arrays (`[]`) for toMany properties?

This would be nice to have:

@Model class Groups: NSManagedObject {
  var items : [ Item ]
}

Related to issue #1.

Maybe there is some hackish solution which swizzles NSArray to react like a set (override isKindOfClass and implement NSSet methods to please CoreData).

Optional properties sometimes complain about `NSNull` (`desired type = NSString; given type = NSNull`)

I'm sometimes getting this:

Unacceptable type of value for attribute: property = "xyz"; desired type = NSString; given type = NSNull; value = .

In this:

  func setValue<T>(forKey key: String, to value: T)
    where T: Codable & CoreDataPrimitiveValue & AnyOptional
  {
    willChangeValue(forKey: key); defer { didChangeValue(forKey: key) }
    let attr = Self.entity().attributesByName[key]
    print("Attr:", attr?.name, attr?.isOptional, attr?.attributeValueClassName)
    setPrimitiveValue(value, forKey: key)
  }

The property is optional, the valueClassName is set to NSString. Not quite sure what is going on here.

In CoreData those work:

    let item = Item(entity: Item.entity(), insertInto: nil)
    assert(item.entity === entity)
    
    item.value = nil
    item.setPrimitiveValue(nil, forKey: "value")

but this fails as well:

    item.setPrimitiveValue(NSNull(), forKey: "value")

Maybe a bridging issue w/ nil? As far as I can see, nil is properly used.

It fails in coerceValueForKey.

Figure out whether property initialisers _might_ be possible

@NSManaged doesn't allow this:

@Model class Item: NSManagedObject {
  var date = Date()
}

This complains that the @NSManaged var item must not have initialization expressions.

I don't think a macro can remove the expression, but maybe the property could be shadowed somehow. Which would also potentially help w/ Issue #2 .
I.e. instead of generating:

@NSManaged var date : Date

something more like:

var date = Date() {
  didSet { _date = newValue }
}
@objc(date)
@NSManaged var _date : Date

Potentially possible, not quite sure. That the _date is renamed might pose the biggest issue here.

Circular reference resolving attached macro

I have a model with entities Album and Artist and many-to-many relationship between them.
Setting up inverse relationship yielded this error, not sure why it happens.

Screenshot 2023-10-01 at 13 12 38

Possibly related: I expanded the macro to see if I can figure something out and noticed this extra space in front of .self, seems like a typo..?

Screenshot 2023-10-01 at 13 13 25

Generate extra toMany accessors like CoreData does

The CoreData generator also generates such accessors for toMany properties:

extension GroupOfItems {

    @objc(insertObject:inItemsAtIndex:)
    @NSManaged public func insertIntoItems(_ value: Item, at idx: Int)

    @objc(removeObjectFromItemsAtIndex:)
    @NSManaged public func removeFromItems(at idx: Int)

    @objc(insertItems:atIndexes:)
    @NSManaged public func insertIntoItems(_ values: [Item], at indexes: NSIndexSet)

    @objc(removeItemsAtIndexes:)
    @NSManaged public func removeFromItems(at indexes: NSIndexSet)

    @objc(replaceObjectInItemsAtIndex:withObject:)
    @NSManaged public func replaceItems(at idx: Int, with value: Item)

    @objc(replaceItemsAtIndexes:withItems:)
    @NSManaged public func replaceItems(at indexes: NSIndexSet, with values: [Item])

    @objc(addItemsObject:)
    @NSManaged public func addToItems(_ value: Item)

    @objc(removeItemsObject:)
    @NSManaged public func removeFromItems(_ value: Item)

    @objc(addItems:)
    @NSManaged public func addToItems(_ values: NSOrderedSet)

    @objc(removeItems:)
    @NSManaged public func removeFromItems(_ values: NSOrderedSet)

Note that those are @NSManaged!

Should be easy?

Finish support for originalName/versionHash in `@Model` macro

Most of the infrastructure is in place, the sole thing missing should be the extraction of the parameters from the model macro.

E.g.:

@Model(originalName: "OldName", versionHash: "2828287")
class NewName: NSManagedObject {}

This already generate the original names into the class, but the values are not filled.

To do this, the parameter list of the model macro needs to be traverse, a matching name needs to be found and the associated expression used in the static variable of the class.

Shouldn't be very hard.

Ordered Relationships, how?

Some support for NSOrderedSet is prepared in the codebase, need to test this and check what actually works.

A problem w/ NSOrderedSet is the lack of an associated type, i.e. it isn't a NSOrderedSet<T>. It can be subclassed in Swift like:

final class OrderedSet<T>: NSOrderedSet

but then the OrderedSet<T> can't be used w/ @NSManaged? (something about OrderedSet can't be used in ObjC).

So the minimal thing would be support for such:

@Model class Group: NSManagedObject {
  @Relationship(inverse: \Item.group)
  var items : NSOrderedSet
}

The target model type could be provided by the inverse.

Check whether optional base types can be used (e.g. `Int?`)

Looks like CoreData itself doesn't really support them.

Another specific issue here is that Int? cannot be used in @NSManaged because Optional<Int> cannot be represented in ObjC (and we can't rewrite the type of a variable in a macro).

If we figure out shadowing like in Issue #14, that might be doable though.

How to specify default value of attribute?

If I do this in @Model class,

var followers: Int = 0

I get @NSManaged property cannot have an initial value.

I see in the expanded macro that schema metadata picks this up correctly but not sure how to force the compiler to allow this.

static let schemaMetadata : [ CoreData.NSManagedObjectModel.PropertyMetadata ] = [
    .init(name: "followers", keypath: \Artist.followers,
          defaultValue: 0,
          metadata: CoreData.NSAttributeDescription(name: "followers", valueType: Int .self)),

Always pre-fill the metadata property info slot

Sharing a whole entity hasn't been the best idea. But pre-creating the metadata info slots should always be possible. Of course inverse relationships won't usually be set as that requires the model resolution context.

E.g. this:

@Model class Person: NSManagedObject {
  var firstname : String
  var lastname : String
}

would generate sth like:

static let metadata = [
  ( "firstname", \Person.firstname, nil, nil ),
  ( "lastname", \Person.lastname, nil, nil )
]

(this is actually not true, this specific static type is detected by MM :-), but to illustrate the point).
Instead we should do:

static let metadata = [
  ( "firstname", \Person.firstname, nil, makePropertyDescription(for: \Person.firstname) ),
  ( "lastname", \Person.lastname, nil, makePropertyDescription(for: \Person.firstname) )
]

The prototypes in the metadata are always copied already, before they are added to an entity.

Backport Foundation `Predicate`s

The Foundation Predicate is available in the open source Foundation. I think it requires iOS 17+ because it uses variadic generics. But that's not actually necessary for the SwiftData APIs which only pass in a single model type.

This should probably go into an own package.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.