Generic function for adding convenience constructors into the core data base - swift

I would like to create a function that would handle any type of object which have overriden init functions. As far I've got working with only overriden init function but if i want to do smth else with these objects every time i have to write extra code with some logic parts. To avoid that and makes code clean i have to make generic functions witch going to have as an argument T.Type.
This is my sample code which shows working parts :
import Foundation
import CoreData
extension Sample {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Sample> {
return NSFetchRequest<Sample>(entityName: "Sample");
}
#NSManaged public var smth: String
#NSManaged public var smth1: Double
convenience init(smth: String, smth1: Double, insertIntoManagedObjectContext _context: NSManagedObjectContext!) {
let _enitity = NSEntityDescription.entity(forEntityName: "Sample", in: _context)
self.init(entity: _entity, insertInto: _context)
self.smth = smth
self.smth1 = smth1
}
}
And then I initialize the object like this :
let _context = DataBaseController.getContext()
let _sample: Sample = Sample(smth: smth, smth1: smth1 insertIntoManagedObjectContext: _context)
DataBaseController.saveContext()
By following exapmle from here : Example
I've implemented these functions :
func addRecord<T: NSManagedObject>(_ type : T.Type) -> T {
let _entityName = T.description()
let _context = DataBaseController.persistentContainer.viewContext
let _entity = NSEntityDescription.entity(forEntityName: _entityName, in: _context)
let _record = T(entity: _entity!, insertInto: _context)
return _record
}
func recordsInDataBase<T: NSManagedObject>(_ type : T.Type) -> Int {
let _records = allRecords(T.self)
return _records.count
}
func allRecords<T: NSManagedObject>(_ type : T.Type, sort: NSSortDescriptor? = nil) -> [T] {
let _context = DataBaseController.persistentContainer.viewContext
let _request = T.fetchRequest()
do {
let _results = try _context.fetch(_request)
return _results as! [T]
} catch {
print("Error : \(error)")
return []
}
}
My question is : How could I invoke my overriden init function from the class with passing also these 2 extra arguments which is smth and smth1?
let _sample = DataBaseController.Instance.addRecord(...)
Thanks in advance!
EDIT :
Is it going to be like this ? :
let _sample = DataBaseController.Instance.addRecord(Sample.self.init(smth: smth, smth1: smth1, insertIntoManagedObjectContext: _context))

You can't do that.
In the example you linked, it is showing how to save and query the database using neat little functions. You can't magically pass extra parameters to the functions and expect them to be accepted and used to create the new "record".
The answerer actually showed you how to use the functions if you scroll down a bit:
let name = "John Appleseed"
let newContact = addRecord(Contact.self)
newContact.contactNo = 1
newContact.contactName = name
That is how you use addRecord. You call addRecord, pass in WhateverType.self and assign the return value to a variable. After that, you set the variable's property directly, not by using the initializer.
I have come up with this (not very elegant) solution:
extension NSManagedObject {
func initialize(with properties: [String: Any]) {
}
}
class Sample : NSManagedObject {
override func initialize(with properties: [String : Any]) {
self.smth = properties["smth"] as! String
self.smth1 = properties["smth1"] as! Double
}
#NSManaged var smth: String
#NSManaged var smth1: Double
convenience init(properties: [String: Any], insertIntoManagedObjectContext _context: NSManagedObjectContext!) {
let _enitity = NSEntityDescription.entity(forEntityName: "Sample", in: _context)
self.init(entity: _enitity!, insertInto: _context)
}
}
Now all NSManagedObject has this initialize(with:) method. So in the addRecord method, you can call that to initialize a new managed object with desired properties:
func addRecord<T: NSManagedObject>(_ type : T.Type, properties: [String: Any]) -> T {
let _entityName = T.description()
let _context = DataBaseController.persistentContainer.viewContext
let _entity = NSEntityDescription.entity(forEntityName: _entityName, in: _context)
let _record = T(entity: _entity!, insertInto: _context)
_record.initialize(with: properties)
return _record
}
Example usage:
addRecord(Sample.self, properties: ["smth": "Hello", "smth1": 1])
The bad thing about this is of course that there is no type safety. Every property you pass to addRecord is of type Any.

Related

save core data into boolean value

In my swift code below the goal is to save a boolean value into core data. I am getting a compile error stating Thread 1: "Unacceptable type of value for attribute: property = "bool"; desired type = NSNumber; given type = __SwiftValue; value = true.". I don't know what to do next. I also added a photo of my core data attributes. I have 2 classes a base class and a helper class.
pic
class ViewController: UIViewController {
var user : [User]? = nil
CoredataHandler.saveObject(username: "Yahs", password: "213", bool: true)
}
class CoredataHandler : NSManagedObject {
private class func getContext() -> NSManagedObjectContext {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
return appDelegate.persistentContainer.viewContext
}
class func saveObject(username: String,password: String,bool: DarwinBoolean) -> Bool{
let context = getContext()
let entity = NSEntityDescription.entity(forEntityName: "User", in: context)
let managedObject = NSManagedObject(entity: entity!, insertInto: context)
managedObject.setValue(username, forKey: "username")
managedObject.setValue(password, forKey: "password")
managedObject.setValue(bool, forKey: "bool")
do {
try context.save()
return true
} catch{
return false
}
}
}

Core Data: Generic class function to return object

I have several classes of base type NSManagedObject and each of them contains a class function to return the first object of a given context. Here's an example:
public class Car: NSManagedObject {
class func first(context: NSManagedObjectContext) -> Car? {
let fetchRequest = Car.fetchRequest()
fetchRequest.fetchLimit = 1
let results = try? context.fetch(fetchRequest)
return results?.first
}
}
Instead of writing this function for every subclass I'd like to put a generic version as an extension to NSManagedObject. I've tried this:
extension NSManagedObject {
class func first(context: NSManagedObjectContext) -> Self? {
let fetchRequest = Self.fetchRequest()
fetchRequest.fetchLimit = 1
let results = try? context.fetch(fetchRequest)
return results?.first
}
}
But this gives a "Type of expression is ambigous without more context" error. How can this be done?
context.fetch() returns [Any], but you can conditionally cast it to the expected type [Self]:
extension NSManagedObject {
class func first(context: NSManagedObjectContext) -> Self? {
let fetchRequest = Self.fetchRequest()
fetchRequest.fetchLimit = 1
let results = try? context.fetch(fetchRequest) as? [Self]
return results?.first
}
}
Or with a real do/try/catch for better diagnostics in the error case:
extension NSManagedObject {
class func first(context: NSManagedObjectContext) -> Self? {
let fetchRequest = Self.fetchRequest()
fetchRequest.fetchLimit = 1
do {
let results = try context.fetch(fetchRequest) as? [Self]
return results?.first
} catch {
print(error)
return nil
}
}
}

Generic Function: Type of expression is ambiguous without more context

I have a static function in a class that takes a generic type that must conform to decodable, however when I call this function I get the following error: "Type of expression is ambiguous without more context". The Tour class (which is the type I'm passing to the function) conforms to Decodable and inherits from the CoreDataModel class.
This is occurring on the new Xcode 12.0 Beta in DashboardNetworkAdapter when I call CoreDataModel.create (line 7 on the snippet I've shared for that class).
Edit minimal reproducible example:
DashboardNetworkAdapter:
class DashboardNetworkAdapter {
// MARK: - GET
public func syncTours(page: Int, completion: #escaping(Bool, Error, Bool) -> Void) {
if let path = Bundle.main.path(forResource: "Tours", ofType: "json") {
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
CoreDataModel.create(Array<Tour>.self, from: data) { success, error in
completion(success, error, false)
}
} catch let error {
completion(false, error, false)
}
}
}
}
CoreDataModel:
public class CoreDataModel: NSManagedObject {
// MARK: - Variables
#NSManaged public var id: Int64
#NSManaged public var createdAt: Date?
#NSManaged public var updatedAt: Date?
// MARK: - CRUD
static func create<T>(_ type: T.Type, from data: Data, completion: #escaping (Bool, Error?) -> Void) where T : Decodable {
DataCoordinator.performBackgroundTask { context in
do {
let _ = try DataDecoder(context: context).decode(type, from: data, completion: {
completion(true, nil)
})
} catch let error {
completion(false, error)
}
}
}
}
Tour:
#objc(Tour)
class Tour: CoreDataModel, Decodable {
// MARK: - Variables
#NSManaged public var name: String?
#NSManaged public var image: URL?
#NSManaged public var owned: Bool
#NSManaged public var price: Double
// MARK: - Coding Keys
enum CodingKeys: String, CodingKey {
case id = "id"
case name = "name"
case image = "image"
case owned = "owned"
case price = "price"
case updatedAt = "updated_at"
case createdAt = "created_at"
}
// MARK: - Initializer
required convenience init(from decoder: Decoder) throws {
guard let context = decoder.userInfo[.context] as? NSManagedObjectContext else { fatalError("Cannot find decoding context")}
self.init(context: context)
let topLevel = try decoder.container(keyedBy: PaginatedResponseCodingKeys.self)
let values = try topLevel.nestedContainer(keyedBy: CodingKeys.self, forKey: .data)
let id = try values.decode(Int64.self, forKey: .id)
let name = try values.decode(String.self, forKey: .name)
let image = try values.decode(String.self, forKey: .image)
let owned = try values.decode(Int.self, forKey: .owned)
let price = try values.decode(Double.self, forKey: .price)
let updatedAt = try values.decode(Date.self, forKey: .updatedAt)
let createdAt = try values.decode(Date.self, forKey: .createdAt)
self.id = id
self.name = name
self.image = URL(string: image)
self.owned = owned == 1
self.price = price
self.updatedAt = updatedAt
self.createdAt = createdAt
}
init(context: NSManagedObjectContext) {
guard let entity = NSEntityDescription.entity(forEntityName: "Tour", in: context) else { fatalError() }
super.init(entity: entity, insertInto: context)
}
}
DataDecoder:
class DataDecoder: JSONDecoder {
// MARK: - Variables
var context: NSManagedObjectContext!
private var persistent: Bool = true
// MARK: - Initializers
public init(persistent: Bool = true, context: NSManagedObjectContext) {
super.init()
self.dateDecodingStrategy = .custom({ (decoder) -> Date in
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
let dateFormatter = DateFormatter()
locale = .autoupdatingCurrent
dateFormat = "yyyy-MM-dd HH:mm:ss"
return dateFormatter.date(from: string)!
})
self.context = context
userInfo[.context] = context
}
// MARK: - Utilities
public func decode<T>(_ type: T.Type, from data: Data, completion: (() -> Void)? = nil) throws -> T where T : Decodable {
let result = try super.decode(type, from: data)
if(persistent) {
saveContext(completion: completion)
}
return result
}
private func saveContext(completion: (() -> Void)? = nil) {
guard let context = context else { fatalError("Cannot Find Decoding Context") }
context.performAndWait {
try? context.save()
completion?()
context.reset()
}
}
}
CodingUserInfoKey Extension:
extension CodingUserInfoKey {
static let context = CodingUserInfoKey(rawValue: "context")!
}
There's a type mismatch between what completion closure expects for its Error parameter, and what it gets from the inferred type of the CoreDataModel.create completion closure, which is Error?:
public func syncTours(page: Int, completion: #escaping(Bool, Error, Bool) -> Void) {
...
CoreDataModel.create(Array<Tour>.self, from: data) { success, error in
// error is of type Error?, but completion expects Error
completion(success, error, false)
}
...
}
In general, whenever you face type inference issues or errors, make each type explicit, and you'll see exactly exactly where the mismatch is. For example, below you could be explicit about the inner closure signature:
CoreDataModel.create([Tour].self, from: data) { (success: Bool, err: Error?) -> Void in
}

Dynamic protocol conformance in Swift

Hi I am struggle to solve the problem dynamic protocol conformance in swift language. Please see code.
Protocol:
protocol Object {
init(by object: [String: Any])
}
Custom structs with protocol object conformance:
struct Tree: Object {
let treeName: String
init(by object: [String: Any]) {
self.treeName = object["tree"] as? String ?? "Notree"
}
}
struct Plant: Object {
let plantName: String
init(by object: [String : Any]) {
self.plantName = object["tree"] as? String ?? ""
}
}
The above code just fine until the object is [String: Any]. I can't use [[String: Any]] like below.
let coconut = ["tree":"Coconut"] // => This fine
let allTrees = [["tree":"Apple"],["tree":"Orange"],["tree":"Jakfruit"]] //=> Here is the problem
let aTree = Tree(by: coconut)
let bTree = Tree(by: ["data":allTrees])
let cTree = Plant(by: ["data":allTrees])
I can't use array of objects. So, I used to store objects in to key "data". Now I used extension: Array confirm protocol object.
extension Array: Object where Element == Object{
init(by object: [String : Any]) {
if let data = object["data"] as? [[String: Any]]{
self = data.map({ (object) -> Object in
// return Plant.init(by: object) // => Works, But I need dynamic confirmance
// return Tree.init(by: object) // => Works, But I need dynamic confirmance
return Object.init(by: object) //=> How can I do?
})
}else{
self = []
}
}
}
The return Object shows error Protocol type 'Object' cannot be instantiated. I tried lot to solve but not able.
Can someone suggest better idea or solution for this problem? Thank you in advance...
First, you should not use the constraint == Object. You want to say that not only [Object] is an Object, but also [Plant] and [Tree] are Objects too, right? For that, you should use the : Object constraint. Second, you can use Element.init to initialise a new Element of the array. Because of the constraint Element : Object, we know that a init(by:) initialiser exists:
extension Array: Object where Element: Object{
init(by object: [String : Any]) {
if let data = object["data"] as? [[String: Any]]{
self = data.map({ (object) in
return Element.init(by: object)
})
}else{
self = []
}
}
}
Usage:
let trees = [Tree](by: ["data": allTrees])
Here's what I think a more Swifty version of your code, making use of failable initialisers - initialisers that return nil when they fail to initialise the object:
protocol Object {
init?(by object: [String: Any])
}
struct Tree: Object {
let treeName: String
init?(by object: [String: Any]) {
if let treeName = object["tree"] as? String {
self.treeName = treeName
} else {
return nil
}
}
}
struct Plant: Object {
let plantName: String
init?(by object: [String : Any]) {
if let plantName = object["tree"] as? String {
self.plantName = plantName
} else {
return nil
}
}
}
extension Array: Object where Element: Object{
init?(by object: [String : Any]) {
if let data = object["data"] as? [[String: Any]]{
self = data.compactMap(Element.init)
}else{
return nil
}
}
}

Swift can't infer generic type when generic type is being passed through a parameter

I'm writing a generic wrapper class for core data.
Here are some of my basic types. Nothing special.
typealias CoreDataSuccessLoad = (_: NSManagedObject) -> Void
typealias CoreDataFailureLoad = (_: CoreDataResponseError?) -> Void
typealias ID = String
enum CoreDataResult<Value> {
case success(Value)
case failure(Error)
}
enum CoreDataResponseError : Error {
typealias Minute = Int
typealias Key = String
case idDoesNotExist
case keyDoesNotExist(key: Key)
case fetch(entityName: String)
}
I've abstracted my coredata writes in a protocol. I'd appreciate if you let me know of your comments about the abstraction I'm trying to pull off.
Yet in the extension I run into the following error:
Cannot convert value of type 'NSFetchRequest' to
expected argument type 'NSFetchRequest<_>'
Not sure exactly how I can fix it. I've tried variations of changing my code but didn't find success...
protocol CoreDataWriteManagerProtocol {
associatedtype ManagedObject : NSManagedObject
var persistentContainer : NSPersistentContainer {get}
var idName : String {get}
func loadFromDB(storableClass : ManagedObject.Type, id: ID) throws -> CoreDataResult<ManagedObject>
func update(storableClass : ManagedObject.Type, id: ID, fields: [String : Any]) throws
func fetch(request: NSFetchRequest<ManagedObject>, from context: NSManagedObjectContext)
init(persistentContainer : NSPersistentContainer)
}
extension CoreDataWriteManagerProtocol {
private func loadFromDB(storableClass : ManagedObject.Type, id: ID) -> CoreDataResult<ManagedObject>{
let predicate = NSPredicate(format: "%# == %#", idName, id)
let fetchRequest : NSFetchRequest = storableClass.fetchRequest()
fetchRequest.predicate = predicate
// ERROR at below line!
return fetch(request: fetchRequest, from: persistentContainer.viewContext)
}
func fetch<ManagedObject: NSManagedObject>(request: NSFetchRequest<ManagedObject>, from context: NSManagedObjectContext) -> CoreDataResult<ManagedObject>{
guard let results = try? context.fetch(request) else {
return .failure(CoreDataResponseError.fetch(entityName: request.entityName ?? "Empty Entity Name")) // #TODO not sure if entityName gets passed or not.
}
if let result = results.first {
return .success(result)
}else{
return .failure(CoreDataResponseError.idDoesNotExist)
}
}
}
Additionally if I change the line:
let fetchRequest : NSFetchRequest = storableClass.fetchRequest()
to:
let fetchRequest : NSFetchRequest<storableClass> = storableClass.fetchRequest()
I get the following error:
Use of undeclared type 'storableClass'`
My intuition tells me that the compiler can't map 'parameters that are types' ie it doesn't understand that storableClass is actually a type. Instead it can only map generics parameters or actual types. Hence this doesn't work.
EDIT:
I used static approach Vadian and wrote this:
private func create(_ entityName: String, json : [String : Any]) throws -> ManagedObject {
guard let entityDescription = NSEntityDescription.entity(forEntityName: entityName, in: Self.persistentContainer.viewContext) else {
print("entityName: \(entityName) doesn't exist!")
throw CoreDataError.entityNotDeclared(name: entityName)
}
let _ = entityDescription.relationships(forDestination: NSEntityDescription.entity(forEntityName: "CountryEntity", in: Self.persistentContainer.viewContext)!)
let relationshipsByName = entityDescription.relationshipsByName
let propertiesByName = entityDescription.propertiesByName
guard let managedObj = NSEntityDescription.insertNewObject(forEntityName: entityName, into: Self.persistentContainer.viewContext) as? ManagedObject else {
throw CoreDataError.entityNotDeclared(name: entityName)
}
for (propertyName,_) in propertiesByName {
if let value = json[propertyName] {
managedObj.setValue(value, forKey: propertyName)
}
}
// set all the relationships
guard !relationshipsByName.isEmpty else {
return managedObj
}
for (relationshipName, _ ) in relationshipsByName {
if let object = json[relationshipName], let objectDict = object as? [String : Any] {
let entity = try create(relationshipName, json: objectDict)
managedObj.setValue(entity, forKey: relationshipName)
}
}
return managedObj
}
But the following piece of it is not generic as in I'm casting it with as? ManagedObject. Basically it's not Swifty as Vadian puts it:
guard let managedObj = NSEntityDescription.insertNewObject(forEntityName: entityName, into: Self.persistentContainer.viewContext) as? ManagedObject else {
throw CoreDataError.entityNotDeclared(name: entityName)
}
Is there any way around that?
My suggestion is a bit different. It uses static methods
Call loadFromDB and fetch on the NSManagedObject subclass. The benefit is that always the associated type is returned without any further type cast.
Another change is throwing errors. As the Core Data API relies widely on throwing errors my suggestion is to drop CoreDataResult<Value>. All errors are passed through. On success the object is returned, on failure an error is thrown.
I left out the id related code and the update method. You can add a static func predicate(for id : ID)
protocol CoreDataWriteManagerProtocol {
associatedtype ManagedObject : NSManagedObject = Self
static var persistentContainer : NSPersistentContainer { get }
static var entityName : String { get }
static func loadFromDB(predicate: NSPredicate?) throws -> ManagedObject
static func fetch(request: NSFetchRequest<ManagedObject>) throws -> ManagedObject
static func insertNewObject() -> ManagedObject
}
extension CoreDataWriteManagerProtocol where Self : NSManagedObject {
static var persistentContainer : NSPersistentContainer {
return (UIApplication.delegate as! AppDelegate).persistentContainer
}
static var entityName : String {
return String(describing:self)
}
static func loadFromDB(predicate: NSPredicate?) throws -> ManagedObject {
let request = NSFetchRequest<ManagedObject>(entityName: entityName)
request.predicate = predicate
return try fetch(request: request)
}
static func fetch(request: NSFetchRequest<ManagedObject>) throws -> ManagedObject {
guard let results = try? persistentContainer.viewContext.fetch(request) else {
throw CoreDataResponseError.fetch(entityName: entityName)
}
if let result = results.first {
return result
} else {
throw CoreDataResponseError.idDoesNotExist
}
}
static func insertNewObject() -> ManagedObject {
return NSEntityDescription.insertNewObject(forEntityName: entityName, into: persistentContainer.viewContext) as! ManagedObject
}
}
The issue is that NSManagedObject.fetchRequest() has a return type of NSFetchRequest<NSFetchRequestResult>, which is non-generic. You need to update the definition of your fetch function to account for this. Btw the function signatures of the default implementations in the protocol extension didn't actually match the function signatures in the protocol definition, so those also need to be updated.
You also need to change the implementation of fetch(request:,from:), since NSManagedObjectContext.fetch() returns a value of type [Any], so you need to cast that to [ManagedObject] to match the type signature of your own fetch method.
protocol CoreDataWriteManagerProtocol {
associatedtype ManagedObject : NSManagedObject
var persistentContainer : NSPersistentContainer {get}
var idName : String {get}
func loadFromDB(storableClass : ManagedObject.Type, id: ID) throws -> CoreDataResult<ManagedObject>
func update(storableClass : ManagedObject.Type, id: ID, fields: [String : Any]) throws
func fetch(request: NSFetchRequest<NSFetchRequestResult>, from: NSManagedObjectContext) -> CoreDataResult<ManagedObject>
init(persistentContainer : NSPersistentContainer)
}
extension CoreDataWriteManagerProtocol {
private func loadFromDB(storableClass : ManagedObject.Type, id: ID) -> CoreDataResult<ManagedObject>{
let predicate = NSPredicate(format: "%# == %#", idName, id)
let fetchRequest = storableClass.fetchRequest()
fetchRequest.predicate = predicate
return fetch(request: fetchRequest, from: persistentContainer.viewContext)
}
func fetch(request: NSFetchRequest<NSFetchRequestResult>, from context: NSManagedObjectContext) -> CoreDataResult<ManagedObject> {
guard let results = (try? context.fetch(request)) as? [ManagedObject] else {
return .failure(CoreDataResponseError.fetch(entityName: request.entityName ?? "Empty Entity Name")) // #TODO not sure if entityName gets passed or not.
}
if let result = results.first {
return .success(result)
}else{
return .failure(CoreDataResponseError.idDoesNotExist)
}
}
}