Core Data Predicate Using KeyPath on a Collection - swift

I have the following code which returns all movies matching the actor name.
static func byActorName(name: String) -> [Movie] {
let request: NSFetchRequest<Movie> = Movie.fetchRequest()
request.predicate = NSPredicate(format: "actors.name CONTAINS %#", name)
do {
return try viewContext.fetch(request)
} catch {
print(error)
return []
}
}
The code above works, but how can I convert it to a modern Swift KeyPath approach?
I implemented the following code which works but I don't like the idea of writing %K.name syntax.
static func byActorName(name: String) -> [Movie] {
let request: NSFetchRequest<Movie> = Movie.fetchRequest()
request.predicate = NSPredicate(format: "%K.name CONTAINS %#", #keyPath(Movie.actors), name)
do {
return try viewContext.fetch(request)
} catch {
print(error)
return []
}
}
Any recommendations?

Solution:
request.predicate = NSPredicate(format: "%K.%K CONTAINS %#", #keyPath(Movie.actors), #keyPath(Actor.name), name)

Related

Swift Core Data - handling empty fetch result

I have a simple entity, a stationID and a type, both Strings
I use this method to search for a type for a given stationID,
func returnStationType(stationTypeId: String) -> PersistantStationType {
let context = container.viewContext
let request = PersistantStationType.fetchRequest() as NSFetchRequest<PersistantStationType>
request.predicate = NSPredicate(format: "%K == %#", #keyPath(PersistantStationType.stationId), stationTypeId as CVarArg)
do {
let result = try context.fetch(request)
if result.count != 0 {
let fetchedStationType = result.first!
return fetchedStationType
} else { print("4 Fetch result was empty for specified stationid: \(String(describing: stationTypeId))")
}
} catch { print("Fetch on goal id: \(String(describing: stationTypeId)) failed. \(error)") }
return PersistantStationType.init()
}
I call the method here:
let persistentStationType = persistenceController.returnStationType(stationTypeId: closestStation.id)
let stationType = persistentStationType.type?.lowercased().capitalized
let realName = helper.returnRealName(stationType: stationType ?? "None")
let imageName = helper.returnStationTypeImage(stationType: stationType ?? "None")
If a stationId is not found - so empty result I get a crash in the code(Bad Exec/ Access) where I call the method - not in the method itself. (I'm not surprised by this but i'm not sure how to handle it)
I can't use if let on the method .. .it's not an optional. Should I be returning an empty object and check for It when the fetch result is empty?
Thanks for any help.
I would declare the function to return an optional and return nil when nothing is found
func returnStationType(stationTypeId: String) -> PersistantStationType? {
let context = container.viewContext
let request: NSFetchRequest<PersistantStationType> = PersistantStationType.fetchRequest()
request.predicate = NSPredicate(format: "%K == %#", #keyPath(PersistantStationType.stationId), stationTypeId)
do {
let result = try context.fetch(request)
if result.count != 0 {
return result[0]
} else {
print("4 Fetch result was empty for specified stationid: \(String(describing: stationTypeId))")
return nil
}
} catch {
print("Fetch on goal id: \(String(describing: stationTypeId)) failed. \(error)")
return nil
}
}
If on the error hand it is considered an error if more than one objects (or none) exists for an id then it would be better to throw an error in those situations
func returnStationType(stationTypeId: String) throws -> PersistantStationType {
let context = container.viewContext
let request: NSFetchRequest<PersistantStationType> = PersistantStationType.fetchRequest()
request.predicate = NSPredicate(format: "%K == %#", #keyPath(PersistantStationType.stationId), stationTypeId)
do {
let result = try context.fetch(request)
switch result.count {
case 0:
throw FetchError("No station type found for id \(stationTypeId)")
case 1:
return result[0]
default:
throw FetchError("Multiple station types found for id \(stationTypeId)")
}
} catch let error as NSError {
throw FetchError(error.localizedDescription)
}
}
struct FetchError: LocalizedError {
private let message: String
var errorDescription: String? {
message
}
init(_ message: String) {
self.message = message
}
}

EXC_BAD_ACCESS when using NSPredicate in Swift

I'm trying to fetch all my items from Core Data that doesn't have "X" in its allergens attribute.
func doesNotContain(attribute: String = "allergens", text: String) {
let request: NSFetchRequest<Dinner> = Dinner.fetchRequest()
let predicate = NSPredicate(format:"NOT \(attribute) CONTAINS %#", text)
request.predicate = predicate
do { items = try context.fetch(request) }
catch let error { print("Error: \(error)") }
}
For some reason this crashes with "EXC_BAD_ACCESS"
But it works perfectly fine when I'm trying to fetch with a predicate using:
func containsSearchWithNSPredicate(attribute: String, text: String) {
let request: NSFetchRequest<Dinner> = Dinner.fetchRequest()
let predicate = NSPredicate(format: "\(attribute) CONTAINS[cd] %#", text)
request.predicate = predicate
do { items = try context.fetch(request) }
catch let error { print("Error: \(error)") }
}
The attribute here is set to "name" when the function is called, and it's set to "allergens" in the first example
I've made sure that the attribute "allergens" is not nil, and I've also tried using %d instead of %#. The allergens attribute is an array, and that's why I'm using NOT CONTAINS
Turns out that NSPredicate doesn't work with attributes marked as "Transformable" with a custom class of [String].
I made it work by instead of having an array, I made it to a String, separating each individual allergen with ";" to make it possible to divide into substrings later.
allergen example:
dinner.allergens = "Gluten;Dairy"
The NSPredicate:
let predicate = NSPredicate(format:"NOT (%K CONTAINS[cd] %#)",attribute, text)
The fetch request will now get all entities that does not have an allergen attribute containing text

Swift core data change bool stored

I am making and expanding an app for lists and adding extra data as I go. I have added a bool to mark the item as completed.
I want to change the value of a bool stored in core data. I can add and delete orders but now I'm looking to change properties. Please can I have help on the best way to change these?
I have made a func changeCompleated, but can't work out how to get it to work in my core data manager.
class CoreDataManager {
static let shared = CoreDataManager(moc: NSManagedObjectContext.current)
var moc: NSManagedObjectContext
private init(moc: NSManagedObjectContext) {
self.moc = moc
}
private func fetchOrder(name: String) -> Order? {
var orders = [Order]()
let request: NSFetchRequest<Order> = Order.fetchRequest()
request.predicate = NSPredicate(format: "name == %#", name)
do {
orders = try self.moc.fetch(request)
} catch let error as NSError {
print(error)
}
return orders.first
}
func changeCompleated(name:String, completed: Bool) {
do {
if let order = fetchOrder(name: name) {
self.moc.perform {
}
try self.moc.save()
}
} catch let error as NSError {
print(error)
}
}
func deleteOrder(name: String) {
do {
if let order = fetchOrder(name: name) {
self.moc.delete(order)
try self.moc.save()
}
} catch let error as NSError {
print(error)
}
}
func getAllOrders() -> [Order] {
var orders = [Order]()
let orderRequest: NSFetchRequest<Order> = Order.fetchRequest()
do {
orders = try self.moc.fetch(orderRequest)
} catch let error as NSError {
print(error)
}
return orders
}
func saveOrder(id: UUID, name: String, type: String, qty: Double, urgent: Bool, complete: Bool) {
let order = Order(context: self.moc)
order.id = id
order.name = name
order.type = type
order.qty = qty
order.urgent = urgent
order.complete = complete
do {
try self.moc.save()
} catch let error as NSError {
print(error)
}
}
}
Almost, just add a line to change the value, the perform block is not needed
func changeCompleated(name: String, completed: Bool) {
guard let order = fetchOrder(name: name) else { return }
do {
order.complete = completed
try self.moc.save()
} catch {
print(error)
}
}
And you can also shorten fetchOrder
private func fetchOrder(name: String) -> Order? {
let request: NSFetchRequest<Order> = Order.fetchRequest()
request.predicate = NSPredicate(format: "name == %#", name)
do {
return try self.moc.fetch(request).first
} catch {
print(error)
}
}

Get Core Data Entity relatives with a generic function

I'm designing a data manager for my Core Data model and I'd like to create a generic function to fetch relatives of a class.
I’ve created a protocol allowing to build managers for each data type. In this protocol I already defined two associated types T and K and several simple functions. Now I’m stuck with a class relatives fetching method — I need to indicate somehow that T has K relatives. I’ve tried in vain to create some protocol indicating this relationship thru mutual properties, so both classes could conform to this protocol. Any idea, is it even possible?
import Foundation
import CoreData
protocol DataManager {
associatedtype T: NSManagedObject, NSFetchRequestResult
associatedtype K: NSManagedObject, NSFetchRequestResult // Relative
static var sharedInstance: Self { get }
static func getAll(sorted: [NSSortDescriptor]?, context: NSManagedObjectContext) -> [T]?
static func insert(item: T)
static func update(item: T)
static func clean()
static func deleteById(id: String)
// Relatives
static func getRelatives(by: T) -> [K]?
static func get(byRelative: K) -> [T]?
}
extension DataManager {
static func getAll(sorted: [NSSortDescriptor]?, context: NSManagedObjectContext) -> [T]? {
guard let fetchRequest: NSFetchRequest<T> = T.fetchRequest() as? NSFetchRequest<T> else { return nil }
fetchRequest.sortDescriptors = sorted
var results: [T]? = nil
do {
results = try context.fetch(fetchRequest)
} catch {
assert(false, error.localizedDescription)
} //TODO: Handle Errors
return results
}
}
protocol Identifiable {
typealias Identity = String
var id: Identity? { get }
}
extension DataManager where Self.T: Identifiable {
static func get(by id: T.Identity, context: NSManagedObjectContext) -> T? {
guard let fetchRequest: NSFetchRequest<T> = T.fetchRequest() as? NSFetchRequest<T> else { return nil }
fetchRequest.predicate = NSPredicate(format: "%K == %#", "id", id)
var rawResults: [T]? = nil
do {
rawResults = try context.fetch(fetchRequest)
} catch {
assert(false, error.localizedDescription)
} //TODO: Handle Errors
if let result = rawResults?.first {
return result }
else { return nil }
}
}
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 :-)

How to apply the type to a NSFetchRequest instance?

In Swift 2 the following code was working:
let request = NSFetchRequest(entityName: String)
but in Swift 3 it gives error:
Generic parameter "ResultType" could not be inferred
because NSFetchRequest is now a generic type. In their documents they wrote this:
let request: NSFetchRequest<Animal> = Animal.fetchRequest
so if my result class is for example Level how should I request correctly?
Because this not working:
let request: NSFetchRequest<Level> = Level.fetchRequest
let request: NSFetchRequest<NSFetchRequestResult> = Level.fetchRequest()
or
let request: NSFetchRequest<Level> = Level.fetchRequest()
depending which version you want.
You have to specify the generic type because otherwise the method call is ambiguous.
The first version is defined for NSManagedObject, the second version is generated automatically for every object using an extension, e.g:
extension Level {
#nonobjc class func fetchRequest() -> NSFetchRequest<Level> {
return NSFetchRequest<Level>(entityName: "Level");
}
#NSManaged var timeStamp: NSDate?
}
The whole point is to remove the usage of String constants.
I think i got it working by doing this:
let request:NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Level")
at least it saves and loads data from DataBase.
But it feels like it is not a proper solution, but it works for now.
The simplest structure I found that works in 3.0 is as follows:
let request = NSFetchRequest<Country>(entityName: "Country")
where the data entity Type is Country.
When trying to create a Core Data BatchDeleteRequest, however, I found that this definition does not work and it seems that you'll need to go with the form:
let request: NSFetchRequest<NSFetchRequestResult> = Country.fetchRequest()
even though the ManagedObject and FetchRequestResult formats are supposed to be equivalent.
Here are some generic CoreData methods that might answer your question:
import Foundation
import Cocoa
func addRecord<T: NSManagedObject>(_ type : T.Type) -> T
{
let entityName = T.description()
let context = app.managedObjectContext
let entity = NSEntityDescription.entity(forEntityName: entityName, in: context)
let record = T(entity: entity!, insertInto: context)
return record
}
func recordsInTable<T: NSManagedObject>(_ type : T.Type) -> Int
{
let recs = allRecords(T.self)
return recs.count
}
func allRecords<T: NSManagedObject>(_ type : T.Type, sort: NSSortDescriptor? = nil) -> [T]
{
let context = app.managedObjectContext
let request = T.fetchRequest()
do
{
let results = try context.fetch(request)
return results as! [T]
}
catch
{
print("Error with request: \(error)")
return []
}
}
func query<T: NSManagedObject>(_ type : T.Type, search: NSPredicate?, sort: NSSortDescriptor? = nil, multiSort: [NSSortDescriptor]? = nil) -> [T]
{
let context = app.managedObjectContext
let request = T.fetchRequest()
if let predicate = search
{
request.predicate = predicate
}
if let sortDescriptors = multiSort
{
request.sortDescriptors = sortDescriptors
}
else if let sortDescriptor = sort
{
request.sortDescriptors = [sortDescriptor]
}
do
{
let results = try context.fetch(request)
return results as! [T]
}
catch
{
print("Error with request: \(error)")
return []
}
}
func deleteRecord(_ object: NSManagedObject)
{
let context = app.managedObjectContext
context.delete(object)
}
func deleteRecords<T: NSManagedObject>(_ type : T.Type, search: NSPredicate? = nil)
{
let context = app.managedObjectContext
let results = query(T.self, search: search)
for record in results
{
context.delete(record)
}
}
func saveDatabase()
{
let context = app.managedObjectContext
do
{
try context.save()
}
catch
{
print("Error saving database: \(error)")
}
}
Assuming that there is a NSManagedObject setup for Contact like this:
class Contact: NSManagedObject
{
#NSManaged var contactNo: Int
#NSManaged var contactName: String
}
These methods can be used in the following way:
let name = "John Appleseed"
let newContact = addRecord(Contact.self)
newContact.contactNo = 1
newContact.contactName = name
let contacts = query(Contact.self, search: NSPredicate(format: "contactName == %#", name))
for contact in contacts
{
print ("Contact name = \(contact.contactName), no = \(contact.contactNo)")
}
deleteRecords(Contact.self, search: NSPredicate(format: "contactName == %#", name))
recs = recordsInTable(Contact.self)
print ("Contacts table has \(recs) records")
saveDatabase()
This is the simplest way to migrate to Swift 3.0, just add <Country>
(tested and worked)
let request = NSFetchRequest<Country>(entityName: "Country")
Swift 3.0 This should work.
let request: NSFetchRequest<NSFetchRequestResult> = NSManagedObject.fetchRequest()
request.entity = entityDescription(context)
request.predicate = predicate
I also had "ResultType" could not be inferred errors. They cleared once I rebuilt the data model setting each entity's Codegen to "Class Definition". I did a brief writeup with step by step instructions here:
Looking for a clear tutorial on the revised NSPersistentContainer in Xcode 8 with Swift 3
By "rebuilt" I mean that I created a new model file with new entries and attributes. A little tedious, but it worked!
What worked best for me so far was:
let request = Level.fetchRequest() as! NSFetchRequest<Level>
I had the same issue and I solved it with the following steps:
Select your xcdatamodeld file and go to the Data Model Inspector
Select your first Entity and go to Section class
Make sure that Codegen "Class Definition" is selected.
Remove all your generated Entity files. You don't need them anymore.
After doing that I had to remove/rewrite all occurences of fetchRequest as XCode seem to somehow mix up with the codegenerated version.
HTH
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
func loadItemsCategory() {
let request: NSFetchRequest<Category> = Category.fetchRequest()
do {
categoryArray = try context.fetch(request)
} catch {
print(error)
}
tableView.reloadData()
}