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

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
}()

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.)

Calling a Core Data attribute through a relationship

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

CoreData's performBackgroundTask method crashes

I wrote a function to fetch database in CoreData. this function will take a closure and run performBackgroundTask to fetch the data. Then, passing the result to the closure to run.
I wrote static properties in AppDelegate for me to access viewContext easily:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
static var persistentContainer: NSPersistentContainer {
return (UIApplication.shared.delegate as! AppDelegate).persistentContainer
}
static var viewContext: NSManagedObjectContext {
return persistentContainer.viewContext
}
// ...
}
The following is the function(not method) I wrote which crashed by using context:
func fetch<T>(fetchRequest: NSFetchRequest<T>, keyForOrder: String? = nil, format: String? = nil, keyword: String? = nil, handler: (([T]?)->Void)? = nil) where T:NSManagedObject, T: NSFetchRequestResult {
AppDelegate.persistentContainer.performBackgroundTask{(context: NSManagedObjectContext) in
if let format = format?.trimmingCharacters(in: .whitespacesAndNewlines),
!format.isEmpty,
let keyword = keyword?.trimmingCharacters(in: .whitespacesAndNewlines),
!keyword.isEmpty {
fetchRequest.predicate = NSPredicate(format: format, keyword)
}
if let keyForOrder = keyForOrder {
fetchRequest.sortDescriptors = [NSSortDescriptor(key: keyForOrder, ascending: true)]
}
guard let cats = try? context.fetch(fetchRequest) else { // crash
return
}
context.performAndWait(){ // crash
if let handler = handler {
handler(cats)
}
}
}
}
but if i replace context with AppDelegate.viewContext, the function won't crash:
func fetch<T>(fetchRequest: NSFetchRequest<T>, keyForOrder: String? = nil, format: String? = nil, keyword: String? = nil, handler: (([T]?)->Void)? = nil) where T:NSManagedObject, T: NSFetchRequestResult {
AppDelegate.persistentContainer.performBackgroundTask{(context: NSManagedObjectContext) in
if let format = format?.trimmingCharacters(in: .whitespacesAndNewlines),
!format.isEmpty,
let keyword = keyword?.trimmingCharacters(in: .whitespacesAndNewlines),
!keyword.isEmpty {
fetchRequest.predicate = NSPredicate(format: format, keyword)
}
if let keyForOrder = keyForOrder {
fetchRequest.sortDescriptors = [NSSortDescriptor(key: keyForOrder, ascending: true)]
}
guard let cats = try? AppDelegate.viewContext.fetch(fetchRequest) else { // crash
return
}
AppDelegate.viewContext.performAndWait(){ // crash
if let handler = handler {
handler(cats)
}
}
}
}
what is exactly going on?
thanks.
Here are some issues:
performBackgroundTask is already on the right thread for the context so there is no reason to call context.performAndWait and may lead to a deadlock or a crash.
The items fetched or created in a performBackgroundTask cannot leave that block under any circumstances. The context will be destroyed at the end of the block and the managedObjects will crash when it tries to access its context
Managing core-data thread safety can be difficult and I have found it a generally good practice to never pass or return managed objects to functions, unless the context of the object is explicit and clear. This is not an unbreakable rule, but I think it is a good rule of thumb when making your APIs.
performBackgroundTask is generally used for updates to core data. If you are only doing fetches you should use the viewContext. Doing a fetch on the background only to pass it to the main thread is generally a waste.
While in a performBackgroundTask block you cannot access the viewContext - neither for reading or for writing. If you do the app can crash any at time with confusing crash reports, even at a later time when you are not violating thread safety.
I don't know what the predicates that you are creating look like, but I have a strong feeling that they are wrong. This would cause a crash when fetching.
Overall I think that the function you created has little value. If all it is doing is a fetch then you should simply create the predicate and sort descriptors and fetch on the viewContext. If you insist on keeping the function, then remove the performBackgroundTask, fetch using the viewContext, return the results(instead of a callback) and only call it from the main thread.

Primary entity vs secondary entities about Core Data saving objects

I am working on a core data application and currently I have the methods setup correctly to save the primary object saves the name of the users deck but it doesn't save recall the secondary object even though the method used to save both is identical. The primary does save second though and I am wondering if it matters the order that objects are saved. I know it is a relational but I figured it wouldn't matter if the secondary was called to save prior to the primary. I am still new to core data so just a simple answer is enough. If I need to save the primary entity object first then I will build the app in such a way that such occurs, else I may have to relook at the code to figure out why it isn't recalling.
This is the code that is supposed to save prior to the name being saved in a relational manner:
#IBAction func buttonWarrior(sender: AnyObject) {
let entity = NSEntityDescription.entityForName("ClassSelection", inManagedObjectContext: classMOC!)
let newObject = ClassSelection(entity: entity!,insertIntoManagedObjectContext: classMOC)
newObject.classname = "Warrior"
var error: NSError?
classMOC?.save(&error)
if let err = error {
println(err)
} else {
self.performSegueWithIdentifier("popOver", sender: self)
}
}
This is the code used to store the primary object which is a different viewcontroller.swift file than the other one. This is presented as a popover box over the secondary object. This part works fine and recalls correctly :
#IBAction func enterButton(sender: AnyObject) {
let entityDescription = NSEntityDescription.entityForName("Deck",inManagedObjectContext: managedObjectContext!)
let storeDeck = Deck(entity: entityDescription!,insertIntoManagedObjectContext: managedObjectContext)
storeDeck.deckname = usersDeckName.text
var error: NSError?
managedObjectContext?.save(&error)
if let err = error {
status.text = err.localizedFailureReason
} else {
usersDeckName.text = ""
status.text = "Deck Saved"
self.performSegueWithIdentifier("showCardSelection", sender: self)
}
}
The recall method I am trying to use may not make sense in it's current iteration as I have been trying many different methods :
#IBOutlet weak var decksListed: UITableView!
let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
var savedDecksClass = [ClassSelection]()
var frc: NSFetchedResultsController = NSFetchedResultsController()
var frcClasses: NSFetchedResultsController = NSFetchedResultsController()
func getFetchedResultsController() -> NSFetchedResultsController {
frc = NSFetchedResultsController(fetchRequest: listFetchRequest(), managedObjectContext: managedObjectContext!, sectionNameKeyPath: nil, cacheName: nil)
return frc
}
func getClassesFetchedResultsController() -> NSFetchedResultsController {
frcClasses = NSFetchedResultsController(fetchRequest: classFetchRequest(), managedObjectContext: managedObjectContext!, sectionNameKeyPath: nil, cacheName: nil)
return frcClasses
}
func listFetchRequest() -> NSFetchRequest {
let fetchRequest = NSFetchRequest(entityName: "Deck")
let sortDescriptor = NSSortDescriptor(key: "deckname", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
return fetchRequest
}
func classFetchRequest() -> NSFetchRequest {
let fetchRequestClasses = NSFetchRequest(entityName: "Deck")
let classSortDescriptor = NSSortDescriptor(key: "classname", ascending: true)
fetchRequestClasses.sortDescriptors = [classSortDescriptor]
return fetchRequestClasses
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let numberofRowsInSection = frc.sections?[section].numberOfObjects
return numberofRowsInSection!
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("usersDeck", forIndexPath: indexPath) as! UITableViewCell
let listed = frc.objectAtIndexPath(indexPath) as! Deck
cell.textLabel?.text = listed.deckname
let listedClass = frcClasses.objectAtIndexPath(indexPath) as! ClassSelection
cell.detailTextLabel!.text = listedClass.classname
return cell
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
decksListed.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
frcClasses = getClassesFetchedResultsController()
frcClasses.delegate = self
frc.performFetch(nil)
frc = getFetchedResultsController()
frc.delegate = self
frc.performFetch(nil)
}
I hope this is enough to give you an idea. I checked the relationships out and they all seem to be correct in the model. I apologize in advanced for the way some of the code looks I plan on shrinking it down after all the editing is done and working.
Thanks to pbasdf for helping me with this one. The chat he opened actually contained exactly what was needed to be done. I just wasn't saving the relationship and passing the object from one view controller to the next. After showing me exactly how to do so with an example I figured out the rest! Basically it would never have been able to recall the object as it never knew that they were related....foolish me! Thanks again!

Filtering Core Data for UISearchBar with Swift

I have a UISearchBar thats hooked up to a table view thats populated by Core Data. I've been having a lot of trouble getting filtering to work. Most tutorials on this are extremely old and haven't worked for me. I was thinking about converting the entity to an array and doing the filtering on the array but I've read that that's inefficient. I'm thinking I should use NSPredicate, but I honestly don't know what to do. Any ideas? Thanks.
I've actually been working on a demo of this and I have just put it up on GitHub. It can be found here. As far as the implementation of it goes, you have to set up UITableViewController, NSFetchedResultsController, and UISearchDisplayDelegate for your class:
class ContactsViewController: UITableViewController, NSFetchedResultsController, UISearchDisplayDelegate {
}
And with this there will be two table views:
Your tableView from the tableViewController (self.tableView)
Your tableView from searchDisplayController (searchDisplayController?.searchResultsTableView)
You also make class variables for fetchedResultsController, searchResultsController, appDelegate, and managedObjectContext:
let appDelegate = UIApplication.sharedApplication().delegate as? AppDelegate
var managedObjectContext: NSManagedObjectContext? {
get {
if let delegate = appDelegate {
return delegate.managedObjectContext
}
return nil
}
}
var fetchedResultsController: NSFetchedResultsController?
var searchResultsController: NSFetchedResultsController?
In the viewDidLoad() you must register your cell for your searchResultsTableView because it does not exist in interface:
searchDisplayController?.searchResultsTableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "CellID")
It is also here that you setup your fetchRequest:
fetchedResultsController = NSFetchedResultsController(fetchRequest: someFetchRequest, managedObjectContext: managedObjectContext!, sectionNameKeyPath: nil, cacheName: nil)
fetchedResultsController?.delegate = self
fetchedResultsController?.performFetch(nil)
You will need a function that returns which FRC you will need depending on the tableView. You make this function return a NSFetchedResultsController and you will use this in all of the table view functions where you pull data from the FRC such as cellForRowAtIndexPath: because it will get you the correct set of data
func fetchedResultsControllerForTableView(tableView: UITableView) -> NSFetchedResultsController? {
return tableView == searchDisplayController?.searchResultsTableView ? searchResultsController? : fetchedResultsController?
}
Finally, you need to implement searchDisplayControllerWillUnloadSearchResults and searchDisplayControllerShouldReloadTableForSearchString:
func searchDisplayController(controller: UISearchDisplayController, willUnloadSearchResultsTableView tableView: UITableView) {
searchResultsController?.delegate = nil
searchResultsController = nil
}
func searchDisplayController(controller: UISearchDisplayController!, shouldReloadTableForSearchString searchString: String!) -> Bool {
let firstNamePredicate = NSPredicate(format: "nameFirst CONTAINS[cd] %#", searchString.lowercaseString)
let lastNamePredicate = NSPredicate(format: "nameLast CONTAINS[cd] %#", searchString.lowercaseString)
let predicate = NSCompoundPredicate.orPredicateWithSubpredicates([firstNamePredicate!, lastNamePredicate!])
searchResultsController = NSFetchedResultsController(fetchRequest: someOtherFetchRequestWithPredicate(predicate), managedObjectContext: managedObjectContext!, sectionNameKeyPath: nil, cacheName: nil)
searchResultsController?.performFetch(nil)
return true
}
If you have any trouble filling in the other stuff, such as the data model or the NSFetchedResultsControllerDelegate methods, check out the demo!
As of iOS 8, the searchDisplayController is deprecated.