diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 0000000..60cdc77 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,5 @@ +version: 1 +builder: + configs: + - documentation_targets: [ ManagedModels ] + diff --git a/Sources/ManagedModels/Documentation.docc/DifferencesToSwiftData.md b/Sources/ManagedModels/Documentation.docc/DifferencesToSwiftData.md new file mode 100644 index 0000000..8b9ba56 --- /dev/null +++ b/Sources/ManagedModels/Documentation.docc/DifferencesToSwiftData.md @@ -0,0 +1,150 @@ +# Differences to SwiftData + +ManagedObjects tries to provide an API that is very close to SwiftData, +but it is not exactly the same API. + +## Differences + +The key difference when converting SwiftData projects: +- Import `ManagedModels` instead of `SwiftData`. +- Let the models inherit from the CoreData + [`NSManagedObject`](https://developer.apple.com/documentation/coredata/nsmanagedobject). +- Use the CoreData + [`@FetchRequest`](https://developer.apple.com/documentation/swiftui/fetchrequest) + instead of SwiftData's + [`@Query`](https://developer.apple.com/documentation/swiftdata/query). + + +### Explicit Superclass + +ManagedModels classes must explicitly inherit from the CoreData +[`NSManagedObject`](https://developer.apple.com/documentation/coredata/nsmanagedobject). +Instead of just this in SwiftData: +```swift +@Model class Contact {} +``` +the superclass has to be specified w/ ManagedModels: +```swift +@Model class Contact: NSManagedObject {} +``` + +> That is due to a limitation of +> [Swift Macros](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/macros/). +> A macro can add protocol conformances, but it cannot add a superclass to a +> type. + + +### CoreData @FetchRequest instead of SwiftData @Query + +Instead of using the new SwiftUI +[`@Query`](https://developer.apple.com/documentation/swiftdata/query) +wrapper, the already available +[`@FetchRequest`](https://developer.apple.com/documentation/swiftui/fetchrequest) +property wrapper is used. + +SwiftData: +```swift +@Query var contacts : [ Contact ] +``` +ManagedModels: +```swift +@FetchRequest var contacts: FetchedResults +``` + +### Properties + +The properties work quite similar. + +Like SwiftData, ManagedModels provides implementations of the +[`@Attribute`](https://developer.apple.com/documentation/swiftdata/attribute(_:originalname:hashmodifier:)), +`@Relationship` and +[`@Transient`](https://developer.apple.com/documentation/swiftdata/transient()) +macros. + +#### Compound Properties + +More complex Swift types are always stored as JSON by ManagedModels, e.g. +```swift +@Model class Person: NSManagedObject { + + struct Address: Codable { + var street : String? + var city : String? + } + + var privateAddress : Address + var businessAddress : Address +} +``` + +SwiftData decomposes those structures in the database. + + +#### RawRepresentable Properties + +Those end up working the same like in SwiftData, but are implemented +differently. +If a type is RawRepresentable by a CoreData base type (like `Int` or `String`), +they get mapped to the same base type in the model. + +Example: +```swift +enum Color: String { + case red, green, blue +} + +enum Priority: Int { + case high = 5, medium = 3, low = 1 +} +``` + + +### Initializers + +A CoreData object has to be initialized through some +[very specific initializer](https://developer.apple.com/documentation/coredata/nsmanagedobject/1506357-init), +while a SwiftData model class _must have_ an explicit `init`, +but is otherwise pretty regular. + +The ManagedModels `@Model` macro generates a set of helper inits to deal with +that. +But the general recommendation is to use a `convenience init` like so: +```swift +convenience init(title: String, age: Int = 50) { + self.init() + title = title + age = age +} +``` +If the own init prefilles _all_ properties (i.e. can be called w/o arguments), +the default `init` helper is not generated anymore, another one has to be used: +```swift +convenience init(title: String = "", age: Int = 50) { + self.init(context: nil) + title = title + age = age +} +``` +The same `init(context:)` can be used to insert into a specific context. +Often necessary when setting up relationships (to make sure that they +live in the same +[`NSManagedObjectContext`](https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext)). + + +### Migration + +Regular CoreData migration mechanisms have to be used. +SwiftData specific migration APIs might be +[added later](https://github.com/Data-swift/ManagedModels/issues/6). + + +## Implementation Differences + +SwiftData completely wraps CoreData and doesn't expose the CoreData APIs. + +SwiftData relies on the +[Observation](https://developer.apple.com/documentation/observation) +framework (which requires iOS 17+). +ManagedModels uses CoreData, which makes models conform to +[ObservableObject](https://developer.apple.com/documentation/combine/observableobject) +to integrate w/ SwiftUI. diff --git a/Sources/ManagedModels/Documentation.docc/Documentation.md b/Sources/ManagedModels/Documentation.docc/Documentation.md new file mode 100644 index 0000000..a1b35ff --- /dev/null +++ b/Sources/ManagedModels/Documentation.docc/Documentation.md @@ -0,0 +1,69 @@ +# ``ManagedModels`` + +SwiftData like declarative schemas for CoreData. + +@Metadata { + @DisplayName("ManagedModels for CoreData") +} + +## Overview + +[ManagedModels](https://github.com/Data-swift/ManagedModels/) is a package +that provides a +[Swift 5.9](https://www.swift.org/blog/swift-5.9-released/) +macro similar to the SwiftData +[@Model](https://developer.apple.com/documentation/SwiftData/Model()). +It can generate CoreData +[ManagedObjectModel](https://developer.apple.com/library/archive/documentation/DataManagement/Devpedia-CoreData/managedObjectModel.html)'s +declaratively from Swift classes, +w/o having to use the Xcode "CoreData Modeler". + +Unlike SwiftData it doesn't require iOS 17+ and works directly w/ +[CoreData](https://developer.apple.com/documentation/coredata). +It is **not** a direct API replacement, but a look-a-like. +Example model class: +```swift +@Model +class ToDo: NSManagedObject { + var title: String + var isDone: Bool + var attachments: [ Attachment ] +} +``` +Setting up a store in SwiftUI: +```swift +ContentView() + .modelContainer(for: ToDo.self) +``` +Performing a query: +```swift +struct ToDoListView: View { + @FetchRequest(sort: \.isDone) + private var toDos: FetchedResults + + var body: some View { + ForEach(toDos) { todo in + Text("\(todo.title)") + .foregroundColor(todo.isDone ? .green : .red) + } + } +} +``` + +- Swift package: [https://github.com/Data-swift/ManagedModels.git](https://github.com/Data-swift/ManagedModels/) +- Example ToDo list app: [https://github.com/Data-swift/ManagedToDosApp.git](https://github.com/Data-swift/ManagedToDosApp/) + + + +## Topics + +### Getting Started + +- +- + +### Support + +- +- +- diff --git a/Sources/ManagedModels/Documentation.docc/FAQ.md b/Sources/ManagedModels/Documentation.docc/FAQ.md new file mode 100644 index 0000000..22ed173 --- /dev/null +++ b/Sources/ManagedModels/Documentation.docc/FAQ.md @@ -0,0 +1,77 @@ +# Frequently Asked Questions + +A collection of questions and possible answers. + +## Overview + +Any question we should add: [info@zeezide.de](mailto:info@zeezide.de), +file a GitHub [Issue](https://github.com/Data-swift/ManagedModels/issues). +or submit a GitHub PR w/ the answer. Thank you! + +## General + +### Is the API the same like in SwiftData? + +The API is very similar, but there are some significant differences: +. + +### Is ManagedObjects a replacement for SwiftData? + +It is not exactly the same but can be used as one, yes. +ManagedObjects allows deployment on earlier versions than iOS 17 or macOS 14 +while still providing many of the SwiftData benefits. + +It might be a sensible migration path towards using SwiftData directly in the +future. + +### Which deployment versions does ManagedObjects support? + +While it might be possible to backport further, ManagedObjects currently +supports: +- iOS 13+ +- macOS 11+ +- tvOS 13+ +- watchOS 6+ + +### Does this require SwiftUI or can I use it in UIKit as well? + +ManagedObjects works with both, SwiftUI and UIKit. + +In a UIKit environment the ``ModelContainer`` (aka ``NSPersistentContainer``) +needs to be setup manually, e.g. in the `ApplicationDelegate`. + +Example: +```swift +import ManagedModels + +let schema = Schema([ Item.self ]) +let container = try ModelContainer(for: schema, configurations: []) +``` + +### Are the `ModelContainer`, `Schema` classes subclasses of CoreData classes? + +No, most of the SwiftData-like types provided by ManagedObjects are just +"typealiases" to the corresponding CoreData types, e.g.: +- ``ModelContainer`` == ``NSPersistentContainer`` +- ``ModelContext`` == ``NSManagedObjectContext`` +- ``Schema`` == ``NSManagedObjectModel`` +- `Schema/Entity` == ``NSEntityDescription`` + +And so on. The CoreData type names can be used instead, but make a future +migration to SwiftData harder. + +### Is it possible to use ManagedModels in SwiftUI Previews? + +Yes! Attach an in-memory store to the preview-View, like so: +```swift +#Preview { + ContentView() + .modelContainer(for: Item.self, inMemory: true) +} +``` + +### Something isn't working right, how do I file a Radar? + +Please file a GitHub + [Issue](https://github.com/Data-swift/ManagedModels/issues). +Thank you very much. diff --git a/Sources/ManagedModels/Documentation.docc/GettingStarted.md b/Sources/ManagedModels/Documentation.docc/GettingStarted.md new file mode 100644 index 0000000..5c239be --- /dev/null +++ b/Sources/ManagedModels/Documentation.docc/GettingStarted.md @@ -0,0 +1,175 @@ +# Getting Started + +Setting up ManagedModels. + +## Introduction + +This article shows how to setup a small SwiftUI Xcode project to work with +ManagedModels. +It is a conversion of the Xcode template project for CoreData. + + +## Creating the Xcode Project and Adding ManagedModels + +1. Create a new SwiftUI project in Xcode, e.g. a Multiplatform/App project or an + iOS/App project. (it does work for UIKit as well!) +2. Choose "None" as the "Storage" (instead of "SwiftData" or "Core Data") +3. Select "Add Package Dependencies" in Xcode's "File" menu to add the + ManagedModels macro. +4. In the **search field** (yes!) of the packages panel, + paste in the URL of the ManagedModels package: + `https://github.com/Data-swift/ManagedModels.git`, + and press "Add Package" twice. + +> At some point Xcode will stop compilation and ask you to confirm that you +> want to use the `@Model` macro provided by ManagedModels. +> Confirm to continue, or review the source code of the macro first. + + +## Create a Model + +Create a new file for the model, say `Item.swift` (or just paste the code in +any other Swift file of the project). +It could look like this: +```swift +import ManagedModels + +@Model class Item: NSManagedObject { + var timestamp : Date +} +``` + +## Configure the App to use the Container + +Use the `View/modelContainer` modifier provided by ManagedModels to setup +the whole CoreData stack for the `Item` model: +```swift +import SwiftUI +import ManagedModels + +@main +struct MyApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + .modelContainer(for: Item.self, inMemory: true) + } +} +``` + +This is using `inMemory: true` during testing, i.e. a new in-memory database +will be create on each restart of the app. +Once the app is in better shape, this can be set to `false` (or the whole +argument removed). + + +## Write a SwiftUI View that works w/ the Model + +Replace the default SwiftUI ContentView with this setup. +The details are addressed below. + +```swift +import SwiftUI +import ManagedModels + +struct ContentView: View { + + @Environment(\.modelContext) + private var viewContext + + @FetchRequest(sort: \.timestamp, animation: .default) + private var items: FetchedResults + + private func addItem() { + withAnimation { + let newItem = Item() + newItem.timestamp = Date() + viewContext.insert(newItem) + } + } + + var body: some View { + NavigationStack { + List { + ForEach(items) { item in + Text("\(item.timestamp, format: .dateTime)") + } + } + .toolbar { + Button(action: addItem) { Label("Add Item", systemImage: "plus") } + } + } + } +} + +#Preview { + ContentView() + .modelContainer(for: Item.self, inMemory: true) +} +``` + +#### Access the ModelContext aka NSManagedObjectContext + +A ``ModelContext`` (``NSManagedObjectContext``) maintains the current state of +the models. It is used to insert and delete models. +A model object is always assigned to just one context. + +The context can be retrieved using the `@Environment` property wrapper: +```swift +@Environment(\.modelContext) private var viewContext +``` +And is then used in e.g. the `addItem` function to create a new model object: +```swift +let newItem = Item() +newItem.timestamp = Date() +viewContext.insert(newItem) +``` +One could also write a convenience initializer to streamline the process. + +#### Use the SwiftUI @FetchRequest to Fetch Models + +The +[`@FetchRequest`](https://developer.apple.com/documentation/swiftui/fetchrequest) +property wrapper is used to load models from CoreData: +```swift +@FetchRequest(sort: \.timestamp, animation: .default) +private var items: FetchedResults +``` +It can sort and filter and do various other things as documented in the +- [CoreData documentation](https://developer.apple.com/documentation/coredata). + +The value of the `items` can be directly used in SwiftUI +[List](https://developer.apple.com/documentation/swiftui/list)'s: +```swift +List { + ForEach(items) { + item in Text("\(item.timestamp)") + } +} +``` + +#### Test the View using SwiftUI Previews + +Using the `#Preview` macro the view can be directly tested in Xcode: +```swift +#Preview { + ContentView() + .modelContainer(for: Item.self, inMemory: true) +} +``` +Note how the in-memory store is used again. In a real setup, one might want to +pre-populate an in-memory store for testing. + + +## Full Example + +There is a small SwiftUI todo list example app, +demonstrating the use of +[ManagedModels](https://github.com/Data-swift/ManagedModels/). +It has two connected entities and shows the general setup: +[Managed ToDos](https://github.com/Data-swift/ManagedToDosApp/). + +It should be self-explanatory. Works on macOS 13+ and iOS 16+, due to the use +of the new SwiftUI navigation views. +Could be backported to earlier versions. diff --git a/Sources/ManagedModels/Documentation.docc/Links.md b/Sources/ManagedModels/Documentation.docc/Links.md new file mode 100644 index 0000000..a0f71a6 --- /dev/null +++ b/Sources/ManagedModels/Documentation.docc/Links.md @@ -0,0 +1,46 @@ +# Links related to CoreData & SwiftData + +A collection of links related to CoreData/SwiftData etc. + + +## Project Links + +Swift Package URL: `https://github.com/Data-swift/ManagedModels.git` + +- [ManagedModels](https://github.com/Data-swift/ManagedModels/) + - filing [GitHub Issues](https://github.com/Data-swift/ManagedModels/issues) +- [Managed ToDos](https://github.com/Data-swift/ManagedToDosApp/) example app + + +## Apple + +- [CoreData](https://developer.apple.com/documentation/coredata) documentation +- [SwiftData](https://developer.apple.com/documentation/swiftdata) documentation + - [@Model](https://developer.apple.com/documentation/SwiftData/Model()) macro + +### WWDC Sessions + +- WWDC 2023 + - [Meet SwiftData](https://developer.apple.com/videos/play/wwdc2023/10187), + Session 10187 + - [Build an App with SwiftData](https://developer.apple.com/videos/play/wwdc2023/10154), + Session 10154 + - [Model your Schema with SwiftData](https://developer.apple.com/videos/play/wwdc2023/10195), + Session 10195 + +### Misc + +- [Enterprise Objects Framework](https://en.wikipedia.org/wiki/Enterprise_Objects_Framework) / + aka EOF, NeXT's/Apple's old ORM CoreData is based on. + - EOF [Developer Guide](https://developer.apple.com/library/archive/documentation/LegacyTechnologies/WebObjects/WebObjects_4.5/System/Documentation/Developer/EnterpriseObjects/DevGuide/EOFDevGuide.pdf) +- Swift + [Macros](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/macros/) + + +## Related Technology + +- [Lighter.swift](https://github.com/Lighter-swift), typesafe and superfast + [SQLite](https://www.sqlite.org) Swift tooling +- [ZeeQL](http://zeeql.io). Prototype of an + [EOF](https://en.wikipedia.org/wiki/Enterprise_Objects_Framework) for Swift, + with many database backends diff --git a/Sources/ManagedModels/Documentation.docc/Who.md b/Sources/ManagedModels/Documentation.docc/Who.md new file mode 100644 index 0000000..a500b5f --- /dev/null +++ b/Sources/ManagedModels/Documentation.docc/Who.md @@ -0,0 +1,29 @@ +# Support + +Built by @helje5. + +## ZeeZide + +ManagedModels is brought to you by +[Helge Heß](https://github.com/helje5/) via [ZeeZide](https://zeezide.de). +We like feedback, GitHub stars, cool contract work, +presumably any form of praise you can think of. + +## Supporting the Project + +If you want to support the project, consider buying a copy of the +[“Code for SQLite3” application](https://apps.apple.com/us/app/code-for-sqlite3/id1638111010). +Or any other, you don't have to use them! 😀 + +Thank you! + +## Support for ManagedModels + +Commercial support for ManagedModels is available from [ZeeZide](https://zeezide.de). +Need help to apply that to your codebase, prepare the CoreData codebase for +SwiftData. +ZeeZide can help with that +[info@zeezide.de](mailto:info@zeezide.de). + +Issues with the software can be filed against the public GitHub repository: +[ManagedModels Issues](https://github.com/Data-swift/ManagedModels/issues). diff --git a/Sources/ManagedModels/ModelMacroDefinition.swift b/Sources/ManagedModels/ModelMacroDefinition.swift index 4a762f9..d72f881 100644 --- a/Sources/ManagedModels/ModelMacroDefinition.swift +++ b/Sources/ManagedModels/ModelMacroDefinition.swift @@ -3,13 +3,14 @@ // Copyright © 2023 ZeeZide GmbH. // +import CoreData + /** - * Tag an `NSManagedObject` class property as a ``Schema/Attribute`` - * (vs a ``Schema/Relationship``). + * Tag an `NSManagedObject` class property as an "Attribute" + * (`NSAttributeDescription`, vs a `NSRelationshipDescription`). * * - Parameters: - * - options: A set of ``Schema/Attribute/Option``s, - * e.g. ``Schema/Attribute/Option/unique``, or none. + * - options: A set of attribute `Option`s, e.g. `.unique`, or none. * - originalName: The peer to CoreData's `renamingIdentifier`. * - hashModifier: The peer to CoreData's `versionHashModifier`. * - defaultValue: The default value for the property. @@ -17,7 +18,7 @@ @available(swift 5.9) @attached(peer) public macro Attribute( - _ options: NSAttributeDescription.Option..., + _ options: CoreData.NSAttributeDescription.Option..., originalName: String? = nil, hashModifier: String? = nil, defaultValue: Any? = nil @@ -25,19 +26,19 @@ public macro Attribute( /** - * Tag an `NSManagedObject` class property as a ``Schema/Relationship`` - * (vs a ``Schema/Attribute``). + * Tag an `NSManagedObject` class property as a "Relationship" + * (`NSRelationshipDescription` vs a `NSAttributeDescription`). * * - Parameters: - * - options: A set of ``Schema/Relationship/Option``s. + * - options: A set of relationship `Option`s. * - originalName: The peer to CoreData's `renamingIdentifier`. * - hashModifier: The peer to CoreData's `versionHashModifier`. */ @available(swift 5.9) @attached(peer) public macro Relationship( - _ options: NSRelationshipDescription.Option..., - deleteRule: Schema.Relationship.DeleteRule = .nullify, + _ options: CoreData.NSRelationshipDescription.Option..., + deleteRule: CoreData.NSRelationshipDescription.DeleteRule = .nullify, minimumModelCount: Int? = 0, maximumModelCount: Int? = 0, originalName: String? = nil, inverse: AnyKeyPath? = nil, diff --git a/Sources/ManagedModels/PersistentModel/PropertyMetadata.swift b/Sources/ManagedModels/PersistentModel/PropertyMetadata.swift index 0aae5ed..d7692b0 100644 --- a/Sources/ManagedModels/PersistentModel/PropertyMetadata.swift +++ b/Sources/ManagedModels/PersistentModel/PropertyMetadata.swift @@ -25,14 +25,17 @@ public extension NSManagedObjectModel { /// The default value associated with the property, e.g. `""`. public let defaultValue : Any? - /// Either a ``Schema/Attribute`` or ``Schema/Relationship`` object (or nil - /// if the user didn't specify an `@Attribute` or `@Relationship` macro). - /// Note: This is never modified, it is treated as a template and gets - /// copied when the ``Schema/Entity`` is built. + /** + * Either a ``NSAttributeDescription`` or a ``NSRelationshipDescription`` + * object (or nil if the user didn't specify an `@Attribute` or + * `@Relationship` macro). + * Note: This is never modified, it is treated as a template and gets + * copied when the `NSEntityDescription` is built. + */ public let metadata : NSPropertyDescription? /** - * Create a new ``Schema/PropertyMetadata`` value. + * Create a new ``PropertyMetadata`` value. * * - Parameters: * - name: name of the property instance variable, e.g. `street`.