Filtering Core Data for UISearchBar with Swift - iphone

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.

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

How can I implement swipe to delete for UITableView containing filtered data from Core Data?

I have two VC with table views, the first showing categories, and the second showing items of selected category (recipes). I am able to get the RecipeTableVC to display filtered data using NSPredicate, but I haven't quite figured out how to delete the recipe from Core Data since the data displayed is a variable containing only the predicated data.
Here is my fetch:
func attemptRecipeFetch() {
let fetchRecipeRequest = NSFetchRequest(entityName: "Recipe")
let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
fetchRecipeRequest.sortDescriptors = [sortDescriptor]
let controller = NSFetchedResultsController(fetchRequest: fetchRecipeRequest, managedObjectContext: ad.managedObjectContext, sectionNameKeyPath: nil, cacheName: nil)
fetchedRecipeController = controller
do {
try self.fetchedRecipeController.performFetch()
let allRecipes = fetchedRecipeController.fetchedObjects as! [Recipe]
recipesOfCategory = allRecipes.filter { NSPredicate(format: "category = %#", selectedCategory!).evaluateWithObject($0) }
} catch {
let error = error as NSError
print("\(error), \(error.userInfo)")
}
}
So what's populating my table is the recipesOfCategory array.
Here is my attempt to delete so far:
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
recipesOfCategory.removeAtIndex(indexPath.row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
ad.managedObjectContext.delete(recipesOfCategory[indexPath.row])
}
}
This crashes and I understand why, but still haven't come up with a solution. Is there a way to implement swipe to delete where it deletes the recipe from Core Data? Am I using the correct methodology to populate the table with filtered data?
I used the following code to 'Swipe to delete from core data' in a table view for an App I recently did. I may work for you.
In your "tableView:commitEditingStyle",
1. set up CoreData access with ...
let appDel: AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let context: NSManagedObjectContext = appDel.managedObjectContext
2. Delete the desired row + incl. from Core Data...
if editingStyle == UITableViewCellEditingStyle.Delete {
context.deleteObject(self.resultsList[indexPath.row]) // Always before
as CoreD
self.resultsList.removeAtIndex(indexPath.row)
do {
try context.save()
} catch {
print("Error unable to save Deletion")
}
} // end IF EditingStyle
self.tableView.reloadData()
In your tableView:commitEditingStyle: you need to just delete the underlying object from Core Data, not from the table view. The NSFetchedResultsController delegate methods will tell you when to remove it from the table view.

How to reload TableView in other View?

I have some CoreData base wich I'm used in my TableView.
When I'm tried to clear those base in other View I have a message in my console log.
CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:.
Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (0) must be equal to the number of rows contained in that section before the update (3), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)
for deleting CoreData Array I'm used this code
self.historyArray.removeAll()
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "History")
fetchRequest.returnsObjectsAsFaults = false
do {
let results = try managedContext.executeFetchRequest(fetchRequest)
for managedObject in results
{
let managedObjectData:NSManagedObject = managedObject as! NSManagedObject
managedContext.deleteObject(managedObjectData)
}
} catch {
print("Detele all data")
}
I know I need to reload TableView, but how can I do this in other View?
ill tried this, but this code don't work.
var tableViewHistoryClass = HistoryView()
self.tableViewHistoryClass.tableView.reloadData()
Please help me to fix this message.
You can achieve this by using notification.
create observer in viewDidLoad method where you can display your table view data.
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector:"refreshTableView", name: "reloadTable", object: nil)
}
func refreshTableView () {
self.tableView.reloadData()
}
Second view controller
-> In this view controller you can change your data( if you want to do) or send data object
NSNotificationCenter.defaultCenter().postNotificationName("reloadTable", object: nil)
so like this it will reload your table view.
One solution is to notify your tableview when data is removed.
When data is removed your code post notifications :
do {
let results = try managedContext.executeFetchRequest(fetchRequest)
for managedObject in results
{
let managedObjectData:NSManagedObject = managedObject as! NSManagedObject
managedContext.deleteObject(managedObjectData)
NSNotificationCenter
.defaultCenter()
.postNotificationName("dataDeleted", object: self)
}
}
And in controller where is your tableview add an observer for this notification:
override func viewDidLoad() {
NSNotificationCenter
.defaultCenter()
.addObserver(
self,
selector: #selector(viewController.reloadTableView),
name: "dataDeleted",
object: nil)
}
func reloadTableView() {
self.tableview.reloadData
}
Thanks all for answers!
I'm created new method, all my Clear CoreData function i added to my View in which i have TableView for showing all data from CoreData :P
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector:"clearCoreDataArray", name: "clearAllData", object: nil)
}
func clearCoreDataArray() {
historyArray.removeAll()
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "History")
fetchRequest.returnsObjectsAsFaults = false
do
{
let results = try managedContext.executeFetchRequest(fetchRequest)
for managedObject in results
{
let managedObjectData:NSManagedObject = managedObject as! NSManagedObject
managedContext.deleteObject(managedObjectData)
}
} catch {
print("Detele all data")
}
self.tableView.reloadData()
}
and in View when I'm need to use this method i use this code
NSNotificationCenter.defaultCenter().postNotificationName("clearAllData", object: self)
now i don't have any CoreData warnings

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!