Calling a Core Data attribute through a relationship - swift

I have two Entities as depicted in the image below:
Food and Restaurant.
I know the naming is a bit off for now, but basically, I'm building up a list of Food items. A user will add in a new entry with the name of the food and the name of the restaurant. I'm at the very early stages of development.
So in the AddViewController, and in the save method, I have:
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate) {
foodEntry = FoodManagedObject(context: appDelegate.persistentContainer.viewContext)
foodEntry.nameOfFood = foodNameTextField.text
foodEntry.restaurantName?.nameOfRestaurant = restaurantNameTextField.text
With a variable declared:
var foodEntry:FoodManagedObject!
In the TimelineView, using NSFetchedResultsController, I'm fetching for the FoodManagedObject and able to display the name of the food in the label. However, the name of the restaurant doesn't display.
So, I'm fetching appropriately:
let fetchRequest: NSFetchRequest<FoodManagedObject> = FoodManagedObject.fetchRequest()
let sortDescriptor = NSSortDescriptor(key: "nameOfFood", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate) {
let context = appDelegate.persistentContainer.viewContext
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
if let fetchedObjects = fetchedResultsController.fetchedObjects {
foods = fetchedObjects
}
} catch {
print(error)
}
}
and in the cellForRow:
cell.foodNameLabel.text = foods[indexPath.row].nameOfFood
cell.restaurantLabel.text = foods[indexPath.row].restaurantName?.nameOfRestaurant
I get no errors, but the name of the restaurant never displays.
Foods is:
var foods:[FoodManagedObject] = []
So I've tried adding in an attribute called theRestaurant into the Food Entity and that works, but calling through a relationship never seems to work.
Am I missing something obvious here?

You're creating relations between the objects, not of their values
It means, that you must assign already existing restaurant entity object or create the new one when you're saving the new food object.
You can't just assign object values without an initialisation of the restaurant object.
E.g.
foodEntry = FoodManagedObject(context: appDelegate.persistentContainer.viewContext)
foodEntry.nameOfFood = foodNameTextField.text
// Here you must to load existing Restaurant entity object from database or create the new one
let restaurant = RestaurantManagedObject(context: appDelegate.persistentContainer.viewContext)
restaurant.nameOfRestaurant = restaurantNameTextField.text
foodEntry.restaurantName = restaurant // Object instead of value
Or if you already have some list of restaurants, than just add the new food object to one of them

Related

How can I set NSFetchedResultsController's section sectionNameKeyPath to be the first letter of a attribute not just the attribute in Swift, NOT ObjC

I'm rewriting an old obj-c project in swift that has a tableView with a sectionIndex
I've set a predicate so it only returns objects with the same country attribute
I want to make the section index based on the first letter of the country attribute
in obj-c i created the fetchedResultsController like this
_fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:#"name.stringGroupByFirstInitial" cacheName:nil];
and i had an extension on NSString
#implementation NSString (Indexing)
- (NSString *)stringGroupByFirstInitial {
if (!self.length || self.length == 1)
return self;
return [self substringToIndex:1];
}
this worked fine in conjunction with the two methods
- (NSArray *) sectionIndexTitlesForTableView: (UITableView *) tableView{
return [self.fetchedResultsController sectionIndexTitles];
}
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index{
return [self.fetchedResultsController sectionForSectionIndexTitle:title atIndex:index];
}
In swift I tried to create a similar extension, with snappier name :)
extension String {
func firstCharacter()-> String {
let startIndex = self.startIndex
let first = self[...startIndex]
return String(first)
}
}
which works fine in a playground returning the string of the first character of any string you call it on.
but using a similar approach creating the fetchedResultsController, in swift, ...
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: (dataModel?.container.viewContext)!,
sectionNameKeyPath: "country.firstCharacter()",
cacheName: nil)
...causes an exception. heres the console log
2018-02-23 11:41:20.762650-0800 Splash[5287:459654] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[ valueForUndefinedKey:]: this class is not key value coding-compliant for the key firstCharacter().'
Any suggestions as the correct way to approach this would be appreciated
This is not a duplicate of the question suggested as this is specifically related to how to achieve this in Swift. I have added the simple correct way to achieve this in Obj -c to the other question
Fastest solution (works with for Swift 4)
The solution from pbasdf works. It's much faster than using a transient property.
I have a list of about 700 names to show in a tableview, with a relative complex fetch from CoreData.
Just doing the initial search:
With a transient property returning a capitalised first letter of 'name': Takes 2-3s to load. With some peaks of up to 4s !
With the solution from pbasdf ( #obj func nameFirstCharacter() -> String, this loading time goes down to 0.4 - 0.5s.
Still the fastest solution is adding a regular property 'nameFirstChar' to the model. And make sure it is indexed. In this case the loading time goes down to 0.01 - 0.02s for the same fetch query!
I have to agree that I agree with some remarks that it looks a bit dirty to add a property to a model, just for creating sections in a tableview. But looking at the performance gain I'll stick to that option!
For the fastest solution, this is the adapted implementation in Person+CoreDataProperties.swift so updating the namefirstchar is automatic when changes to the name property are made. Note that code autogeneration is turned off here. :
//#NSManaged public var name: String? //removed default
public var name: String? //rewritten to set namefirstchar correctly
{
set (new_name){
self.willChangeValue(forKey: "name")
self.setPrimitiveValue(new_name, forKey: "name")
self.didChangeValue(forKey: "name")
let namefirstchar_s:String = new_name!.substring(to: 1).capitalized
if self.namefirstchar ?? "" != namefirstchar_s {
self.namefirstchar = namefirstchar_s
}
}
get {
self.willAccessValue(forKey: "name")
let text = self.primitiveValue(forKey: "name") as? String
self.didAccessValue(forKey: "name")
return text
}
}
#NSManaged public var namefirstchar: String?
In case you don't like or can't add a new property or if you prefer to keep the code autogeneration on, this is my tweaked solution from pbasdf:
1) Extend NSString
extension NSString{
#objc func firstUpperCaseChar() -> String{ // #objc is needed to avoid crash
if self.length == 0 {
return ""
}
return self.substring(to: 1).capitalized
}
}
2) Do the sorting, no need to use sorting key name.firstUpperCaseChar
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Person")
let personNameSort = NSSortDescriptor(key: "name", ascending: true, selector: #selector(NSString.caseInsensitiveCompare))
let personFirstNameSort = NSSortDescriptor(key: "firstname", ascending: true, selector: #selector(NSString.caseInsensitiveCompare))
fetchRequest.sortDescriptors = [personNameSort, personFirstNameSort]
3) Then do the fetch with the right sectionNameKeyPath
fetchRequest.predicate = ... whatever you need to filter on ...
fetchRequest.fetchBatchSize = 50 //or 20 to speed things up
let fetchedResultsController = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: managedObjectContext,
sectionNameKeyPath: "name.firstUpperCaseChar", //note no ()
cacheName: nil)
fetchedResultsController.delegate = self
let start = DispatchTime.now() // Start time
do {
try fetchedResultsController.performFetch()
} catch {
fatalError("Failed to initialize FetchedResultsController: \(error)")
}
let end = DispatchTime.now() // End time
let nanoTime = end.uptimeNanoseconds - start.uptimeNanoseconds // time difference in nanosecs
let timeInterval = Double(nanoTime) / 1_000_000_000
print("Fetchtime: \(timeInterval) seconds")
Add a function to your DiveSite class to return the first letter of the country:
#objc func countryFirstCharacter() -> String {
let startIndex = country.startIndex
let first = country[...startIndex]
return String(first)
}
Then use that function name (without the ()) as the sectionNameKeyPath:
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: (dataModel?.container.viewContext)!,
sectionNameKeyPath: "countryFirstCharacter",
cacheName: nil)
Note that the #objc is necessary here in order to make the (Swift) function visible to the (Objective-C) FRC. (Sadly you can't just add #objc to your extension of String.)

Why can't I create a Fetched Results Controller using a closure?

I am trying to create a Fetched Results Controller following some tutorials. However in Swift 3 I get the error 'unable to infer complex closure type' when attempting to create one in the pattern you see below.
class FriendsController: UICollectionViewController {
lazy var fetchedResultsController: NSFetchedResultsController = {
let context = (UIApplication.shared.delegate as!
AppDelegate).persistentContainer.viewContext
let fetchRequest: NSFetchRequest = Friend.fetchRequest()
let fetchedResultsController =
NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: context, sectionNameKeyPath: nil, cacheName:
nil)
return fetchedResultsController
}()
I use this closure pattern when making other things such as views, buttons etc, however it doesn't work (even when I don't use lazy var). The error also isn't very clear to me either (stated above). Thank you.
NSFetchedResultsController is generic in Swift 3. You have to specify a concrete type because the compiler is unable to infer complex closure type :
lazy var fetchedResultsController: NSFetchedResultsController<Friend> = { ...
Because you should specify the generic type of the objects your FetchedResultsControllers holds
lazy var fetchedResultsController: NSFetchedResultsController<Friend> = {
let context = //your context
let req = // your request
let fetchedResultsController = NSFetchedResultsController(fetchRequest: req, managedObjectContext:context, sectionNameKeyPath: nil, cacheName: nil)
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
}
catch {
print("fetch error \(error)")
}
return fetchedResultsController
}()

TableView.reloadData() doesn't work after save data into core data entity

I'm trying to insert a default record into my core data entity while the tableview first-time loaded and checked there's no data in the entity.
The data inserted just fine , but the reloadData() didn't work, after navigate to other view and navigate back to the view the data appears. no matter the reloadData() in or out of the .save() method.
override func viewDidLoad() {
let cateContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let categoryRequest = NSFetchRequest(entityName: "Category")
categoryArray = (try! cateContext.executeFetchRequest(categoryRequest)) as! [Category]
if categoryArray.count == 0 {
let category = NSEntityDescription.insertNewObjectForEntityForName("Category", inManagedObjectContext: cateContext) as! Category
category.sno = "1"
category.category = "General"
category.locate = false
do {
try cateContext.save()
self.categoryTableView.reloadData()
} catch let saveError as NSError {
print("Saving Error : \(saveError.localizedDescription)")
}
}
//self.categoryTableView.reloadData()
}
If your are calling self.categoryTableView.reloadData() in viewDidLoad() method it will not reload your tableView twice. You need to call self.categoryTableView.reloadData() after you have checked if entity existed again.

Request just one attribute of a entity

So I am using one core data file with a single one entity named BookArray, inside that entity I have four different attributes, what I want to do is to request just one of those attributes from the entity not all. Is it possible?
var appDel: AppDelegate = (UIApplication.sharedApplication().delegate as! AppDelegate)
var context:NSManagedObjectContext = appDel.managedObjectContext!
var request = NSFetchRequest(entityName: "BookArray")
request.returnsObjectsAsFaults = false
bookArray = context.executeFetchRequest(request, error: nil)!
Suppose I have an attribute called sciFi and another named drama, how would I request just the drama attribute?
You can, by adding:
request.propertiesToFetch = ["drama"]
request.resultType = .DictionaryResultType
but, unless your other properties are big, it's unlikely to be worthwhile: your bookArray will then contain an array of dictionaries, from which you will need to unpack the relevant values: you might just as well do that directly from the array of NSManagedObjects returned by a normal fetch.

how would i skip displaying certain entries when populating cell rows from core data in a tableviewcontroller?

For an app I'm working on, i have saved several items to my coredata model.
My intention is to selectively print them out to a tableview based on whether certain attributes are true.
Example given the items in CoreData:
/////////////////////
1--name: Joe, display: true
2--name: Sally, display:false
3--name: Bob, display:false
4--name: Jess, display:true
/////////////////////
i would only want my table view to display to cells where display is true
so out of the 4 entries only Joe and Jess would be listed in my tableview
So far, I only know how to print out every single item. with the table view datasource protocol
thanks for helping out a noob!
I would do something like this:
// Your Entity here is called "Users"
var myUsers = [Users]()
var fetchRequest = NSFetchRequest(entityName: "Users")
let Predicate = NSPredicate(format: "display = %#", true)
fetchRequest.predicate = Predicate
// maybe Sort?
var sortDescriptor = NSSortDescriptor(key: "UserID", ascending: true)
var sortDescriptors = [sortDescriptor]
fetchRequest.sortDescriptors = sortDescriptors
myUsers = context!.executeFetchRequest(fetchRequest, error: nil) as [Users]
// use them in your TableView (cellForRowAtIndexPath)
var myuser = myUsers[indexPath.row] as Users
You should use a NSFetchedResultsController to display the Core Data objects in a
table view.
Restricting the displayed items is then simply done by setting a predicate
NSPredicate(format: "display == YES")
on the fetched results controller.