Create a Generic Swift Function to Return an Array of Core Data Entities

Create a generic Swift function to return an array of Core Data entities

You were almost there. You only have to change the parameter
type from T to T.Type:

func fetchEntities<T: NSManagedObject>(entity: T.Type) -> [T]?

so that you can pass a type instead of an instance:

let stores = fetchEntities(entity: Store.self)

How to make return type of function generic in Swift

This isn't possible.

What generics are for

Suppose you have this function:

func identity(_ value: Any) -> Any {
return value
}

It doesn't actually work:

let i = 5
assert(identity(i) == i) // ❌ binary operator '==' cannot be applied to operands of type 'Any' and 'Int'

Any causes type information to be lost. Even though we see that the type of the parameter and the return value will always be the same, we haven't expressed that to the type system. This is a perfect use-case for a generic type parameter. It allows us to express the relationships between the types of the parameter and the return value.

func identity<T>(_ value: T) -> T {
return value
}

let i = 5
assert(identity(i) == i) // ✅

What generics are not for

Looking back at your problem, you see that there are no type relationships here to be expressed.

  • ClassA.validateModel() always returns [String]
  • ClassB.validateModel() always returns [Int]
  • ClassC.validateModel() always returns [MyCustomEnum]

That's not generic.

How would it even work?

Suppose you had an object of type ElementData. That object could be an instance of ElementData, or of ClassA, or ClassB, or ClassC. Given that all four of these types are possible, and assuming there exists some concoction to do what you want, how would this code work?

let elementData = someElementData()
let validatedModel = elementData.validateModel() // What type is `someValue` supposed to be?

Since we (nor the compiler) knows what concrete type the value of elementData will be (we only know that it's an ElementData, or one of its subclasses), how is the compiler supposed to determine the type of validatedModel ?

Furthermore, your code would be breaking the Liskov Substitution Principle. ClassA needs to support being substituted where ElementData is expected. One of the things that ElementData.validateModel()can do is return a Something. Thus, ClassA.validateModel() needs to either return a Something, or a subclass (strangely, it seems that only inheritence relationships work, not protocol subtype relationships. For example, returning Int where Any is expected doesn't work). Since ClassA.validateModel() returns Array<String>, and Array isn't a class (thus, can't have a superclass), there's no possible type Something that could be used to make the code not violate LSP and compile.

Here's an illustration of LSP, and how Covariance works in return types of overridden methods, and not in parameter types of overridden methods.

// https://www.mikeash.com/pyblog/friday-qa-2015-11-20-covariance-and-contravariance.html

class Animal {}
class Cat: Animal {}

class Person {
func purchaseAnimal() -> Animal {
return Animal()
}
}

class CrazyCatLady: Person {
// Totally legal. `Person` has to be able to return an `Animal`.
// A `Cat` is an animal, so returning a `Cat` where an `Animal` is required is totally valid
override func purchaseAnimal() -> Cat {
return Cat()
}

// This method definition wouldn't be legal, because it violates the Liskov Substitution Principle (LSP).
// A `CrazyCatLady` needs to be able to stand in anywhere a `Person` can be used. One of the things a
// `Person` can do is to `pet(animal: Animal)`. But a `CrazyCatLady` can't, because she can only pet cats.
//
// If this were allowed to compile, this could would be undefined behaviour:
//
// let person: Person = getAPerson()
// let animal: Animal = getAnAnimal()
// person.pet(animal)
//
// override func pet(animal: Cat) { // ❌ method does not override any method from its superclass
//
// }
}

One approach to a solution

First of all, we need to establish what's in common between these return types. If we could do that, then the compiler has an answer to the question of "what type should someModel be?" above.

There are two tools available:

  1. Class inheritance (subclasses are subtypes of their superclasses)
  2. Protocol conformance (protocol conforming types are subtypes of the protocols they conform to)

Both have advantages/disadvantages. Protocols force you in the painful road of dispair that is associated-type, whereas Classes are less flexible (since they can't be subclassed by enums or structs). In this case, the answer lies with what you want this code to do. Fundamentally, you're trying to hook this data up to a table cell. So make a protocol for that:

protocol CellViewDataSource {
func populate(cellView: UICellView) {
// adjust the cell as necessary.
}
}

Now, update your methods to return this type:

class ElementData {
func validateModel() -> CellViewDataSource {
fatalError()
}
}

class ClassA {
func validateModel() -> CellViewDataSource {
fatalError()
}
}

To implement these methods, you'll have to extend Array to conform to CellViewDataSource. However, that's a pretty terrible idea. I suggest instead that you create a new type (probably a struct) that stores the data you need.

struct ModelA {
let name: String
let points: Int
let description: String
let image: UIImage
}

extension ModelA: CellViewDataSource {
func populate(cellView: UICellView) {
// Populate the cell view with my `name`, `points`, `description` and `image`.
}
}

class ElementData {
func validateModel() -> CellViewDataSource {
fatalError("Abstract method.")
}
}

class ClassA {
func validateModel() -> CellViewDataSource {
return ModelA(
name: "Bob Smith",
points: 123,
description: "A dummy model.",
image: someImage()
)
}
}

How to write a simple generic Core Data function for all Entities

You can pass the managed object class instead of NSFetchRequestResult
to the generic function:

func countAll<T: NSManagedObject>(entity: T.Type) -> Int {
let entityName = String(describing: entity)
let request = NSFetchRequest<T>(entityName: entityName)
do {
return try thePersistentContainer.viewContext.count(for: request)
} catch {
print(error.localizedDescription)
return 0
}
}

let count = countAll(entity: YourEntity.self)

An alternative is to define an extension method on NSManagedObject
(similarly as in How can I create instances of managed object subclasses in a NSManagedObject Swift extension?):

extension NSManagedObject {

class func countAll() -> Int {
let eName = String(describing: self)
let request = NSFetchRequest<NSFetchRequestResult>(entityName: eName)
do {
return try thePersistentContainer.viewContext.count(for: request)
} catch {
print(error.localizedDescription)
return 0
}
}
}

let count = YourEntity.countAll()

Get Core Data Entity relatives with a generic function

Well, I've created one solution.
We can identify all relations with a particular class:

let relationships = T.entity().relationships(forDestination: K.entity())

It allows us to find all IDs of an item for each relationship (we can have many relationships for the same relative Entity):

let relativesIDs = item.objectIDs(forRelationshipNamed: relationship.name)

So, we can use these IDs to fetch records from another class.

static func getRelatives(of item: T, context:NSManagedObjectContext) -> [K]? {

guard let fetchRequest: NSFetchRequest<K> = K.fetchRequest() as? NSFetchRequest<K> else { return nil }
fetchRequest.fetchBatchSize = 100

var results: [K]? = nil

var resultSet: Set<K> = [] // doesn't allow duplicates

let relationships = T.entity().relationships(forDestination: K.entity())

for relationship in relationships {

let relativesIDs = item.objectIDs(forRelationshipNamed: relationship.name)
let predicate = NSPredicate(format: "self IN %@", relativesIDs)
fetchRequest.predicate = predicate

var batchResults: [K] = []

do {
batchResults = try context.fetch(fetchRequest)
} catch {
assert(false, error.localizedDescription)
} //TODO: Handle Errors

if batchResults.count > 0 { resultSet = resultSet.union(Set(batchResults)) }
}

if resultSet.count > 0 { results = Array(resultSet) }

return results
}

I'm not sure that this is the most elegant solution, but it works :-)

Is there a proper way to make generic crud functions for Core Data in Swift?

The issue is, that users and movies are saved in their respected tables, but I want to have user's ID in the foreign key of movies as well.

The first thing you need to do is to stop thinking of Core Data as if it were a SQL database. It's not designed to work the same way, and trying to use it like SQL will make things difficult for you. Core Data uses SQLite internally, but its API is not SQL-like.

You don't use foreign keys in Core Data, you use relationships. You already have the relationship you need, so that's what you would normally use with Core Data. If you have a Movies instance and you want to know what User goes with that movie, you use the user relationship that you already have. To make that work, you need to assign a value for the relationship when you create an instance. When you create a Movies, assign a value to the user relationship. A foreign key isn't necessary, because the relationship exists.

If you prefer to use a SQL-like approach, there are several open source SQLite wrappers available, for example FMDB and PLDatabase.

Generic core data fetch request

Problem here is fetchRequest() is a auto-generated helper function and it's only available for your NSManagedObject sub classes. It's not available for your generic type. Therefore, instead of using that function you have to use it's implementation with your generic type like this,

replace this line,

let fetchRequest: NSFetchRequest<T> = T.fetchRequest()

with this,

let fetchRequest: NSFetchRequest<T> = NSFetchRequest<T>(entityName: String(describing: T.self))

Swift: Creating array of objects with generic type parameter

Change

class Setting<T> 

to

class Setting<T:SettingProtocol>

and try compiling.

Saving array to CoreData Swift

This is a question for a tutorial rather than a straight forward answer. I suggest you give some time to read about CoreData. Having said that, your question sounds generic, "Saving array to CoreData in Swift", so I guess it doesn't hurt to explain a simple implementation step by step:

Step 1: Create your model file (.xcdatamodeld)

In Xcode, file - new - file - iOS and choose Data Model

Step 2: Add entities

Select the file in Xcode, find and click on Add Entity, name your entity (CryptosMO to follow along), click on Add Attribute and add the fields you like to store. (name, code, symbol... all of type String in this case). I'll ignore everything else but name for ease.

Step 3 Generate Object representation of those entities (NSManagedObject)

In Xcode, Editor - Create NSManagedObject subclass and follow the steps.

Step 4 Lets create a clone of this subclass

NSManagedObject is not thread-safe so lets create a struct that can be passed around safely:

struct Cryptos {
var reference: NSManagedObjectID! // ID on the other-hand is thread safe.

var name: String // and the rest of your properties
}

Step 5: CoreDataStore

Lets create a store that gives us access to NSManagedObjectContexts:

class Store {
private init() {}
private static let shared: Store = Store()

lazy var container: NSPersistentContainer = {

// The name of your .xcdatamodeld file.
guard let url = Bundle().url(forResource: "ModelFile", withExtension: "momd") else {
fatalError("Create the .xcdatamodeld file with the correct name !!!")
// If you're setting up this container in a different bundle than the app,
// Use Bundle(for: Store.self) assuming `CoreDataStore` is in that bundle.
}
let container = NSPersistentContainer(name: "ModelFile")
container.loadPersistentStores { _, _ in }
container.viewContext.automaticallyMergesChangesFromParent = true

return container
}()

// MARK: APIs

/// For main queue use only, simple rule is don't access it from any queue other than main!!!
static var viewContext: NSManagedObjectContext { return shared.container.viewContext }

/// Context for use in background.
static var newContext: NSManagedObjectContext { return shared.container.newBackgroundContext() }
}

Store sets up a persistent container using your .xcdatamodeld file.

Step 6: Data source to fetch these entities

Core Data comes with NSFetchedResultsController to fetch entities from a context that allows extensive configuration, here is a simple implementation of a data source support using this controller.

class CryptosDataSource {

let controller: NSFetchedResultsController<NSFetchRequestResult>
let request: NSFetchRequest<NSFetchRequestResult> = CryptosMO.fetchRequest()

let defaultSort: NSSortDescriptor = NSSortDescriptor(key: #keyPath(CryptosMO.name), ascending: false)

init(context: NSManagedObjectContext, sortDescriptors: [NSSortDescriptor] = []) {
var sort: [NSSortDescriptor] = sortDescriptors
if sort.isEmpty { sort = [defaultSort] }

request.sortDescriptors = sort

controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
}

// MARK: DataSource APIs

func fetch(completion: ((Result) -> ())?) {
do {
try controller.performFetch()
completion?(.success)
} catch let error {
completion?(.fail(error))
}
}

var count: Int { return controller.fetchedObjects?.count ?? 0 }

func anyCryptos(at indexPath: IndexPath) -> Cryptos {
let c: CryptosMO = controller.object(at: indexPath) as! CryptosMO
return Cryptos(reference: c.objectID, name: c.name)
}
}

All we need from an instance of this class is, number of objects, count and item at a given indexPath. Note that the data source returns the struct Cryptos and not an instance of NSManagedObject.

Step 7: APIs for add, edit and delete

Lets add this apis as an extension to NSManagedObjectContext:
But before that, these actions may succeed or fail so lets create an enum to reflect that:

enum Result {
case success, fail(Error)
}

The APIs:

extension NSManagedObjectContext {

// MARK: Load data

var dataSource: CryptosDataSource { return CryptosDataSource(context: self) }

// MARK: Data manupulation

func add(cryptos: Cryptos, completion: ((Result) -> ())?) {
perform {
let entity: CryptosMO = CryptosMO(context: self)
entity.name = cryptos.name
self.save(completion: completion)
}
}

func edit(cryptos: Cryptos, completion: ((Result) -> ())?) {
guard cryptos.reference != nil else {
print("No reference")
return
}
perform {
let entity: CryptosMO? = self.object(with: cryptos.reference) as? CryptosMO
entity?.name = cryptos.name
self.save(completion: completion)
}
}

func delete(cryptos: Cryptos, completion: ((Result) -> ())?) {
guard cryptos.reference != nil else {
print("No reference")
return
}
perform {
let entity: CryptosMO? = self.object(with: cryptos.reference) as? CryptosMO
self.delete(entity!)
self.save(completion: completion)
}
}

func save(completion: ((Result) -> ())?) {
do {
try self.save()
completion?(.success)
} catch let error {
self.rollback()
completion?(.fail(error))
}
}
}

Step 8: Last step, use case

To fetch the stored data in main queue, use Store.viewContext.dataSource.
To add, edit or delete an item, decide if you'd like to do on main queue using viewContext, or from any arbitrary queue (even main queue) using newContext or a temporary background context provided by Store container using Store.container.performInBackground... which will expose a context.
e.g. adding a cryptos:

let cryptos: Cryptos = Cryptos(reference: nil, name: "SomeName")
Store.viewContext.add(cryptos: cryptos) { result in
switch result {
case .fail(let error): print("Error: ", error)
case .success: print("Saved successfully")
}
}

Simple UITableViewController that uses the cryptos data source:

class ViewController: UITableViewController {

let dataSource: CryptosDataSource = Store.viewContext.dataSource

// MARK: UITableViewDataSource

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return tableView.dequeueReusableCell(withIdentifier: "YourCellId", for: indexPath)
}
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let cryptos: Cryptos = dataSource.anyCryptos(at: indexPath)
// TODO: Configure your cell with cryptos values.
}
}


Related Topics



Leave a reply



Submit