Re-order cells in UITableView with swift and fetchedResultsController - ios

I have a UITableView in swift where the app allows users to re-order the cells, but it keeps crashing during the re-order.
I have this data model:
class Person: NSManagedObject {
#NSManaged var name: String
#NSManaged var mood: String
}
I have setup a bool variable to check when the user hits the edit button to see if the tableview.setEdititing is set to true, because this indicates a change in order is about to happen:
var userDrivenDataChange : Bool = false
Then for each of the fetched results controller delegate methods before I go ahead I always check if there are user driven changees to the object - for example...
func controllerWillChangeContent(controller: NSFetchedResultsController) {
if userDrivenDataChange{
return
}
tblView.beginUpdates()
}
Now, this is the bit I'm really struggling with...
In the moveRowAtIndexPath function, my logic is to get the object the user has just moved, and then get the section that the user wants to move the line to. Then create a new Person NSManagedObject but set its "mood" field to the new section name, then delete the object at the old indexPath.
func tableView(tableView: UITableView, moveRowAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath) {
userDrivenDataChange = true
var context = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext!
var entity = NSEntityDescription.entityForName("Person", inManagedObjectContext: context)
var secInfo = fetchedResultController.sections![destinationIndexPath.section] as NSFetchedResultsSectionInfo
var personToAmend = fetchedResultController.objectAtIndexPath(sourceIndexPath) as Person
var copyOfPerson = Person(entity: entity!, insertIntoManagedObjectContext: context)
copyOfPerson.name = personToAmend.name
copyOfPerson.mood = secInfo.name!
context.deleteObject(personToAmend)
var err : NSError?
if !context.save(&err){
println(err)
}
userDrivenDataChange = false
}
So this doesn't work, honestly can't figure out why!
Any help would be greatly appreciated!
Thanks,
Jamie
p.s. Just incase you need to see it, my fetched results controller is:
lazy var fetchedResultController : NSFetchedResultsController = {
let fetchRequest = NSFetchRequest(entityName: "Person")
let context = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext!
let sort = NSSortDescriptor(key: "mood", ascending: true)
fetchRequest.sortDescriptors = [sort]
let frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: "mood", cacheName: nil)
frc.delegate = self
return frc
}()
----EDIT:
When a user reorders rows from one section to another, the fetchedResultsController always adds the row to the beginning of the section instead of the actual index path represented by destinationIndexPath.
So I guess my question becomes: Is there a function to either edit the index Path of the object I insert, or to add an object at a specific index path?

Related

NSFetchedResultsController not working with transient property for sectionNameKeyPath

Working fine in Swift 3 with Xcode8.3
I have a project ongoing which has core data for saving messages.
It sorts messages according to time and sections them according to day.
Here's how:
let request = NSFetchRequest(entityName: "Message")
let sortDiscriptor = NSSortDescriptor(key: "time", ascending: true)
request.sortDescriptors = [sortDiscriptor]
fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: mainThreadMOC, sectionNameKeyPath: "sectionTitle", cacheName: nil)
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
} catch {
fatalError("Failed to initialize FetchedResultsController: \(error)")
}
Here is transient property:
var sectionTitle: String? {
//this is **transient** property
//to set it as transient, check mark the box with same name in data model
return time!.getTimeStrWithDayPrecision()
}
Using it as:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let sectionInfo = fetchedResultsController.sections![section]
let n = sectionInfo.numberOfObjects
return n
}
It always gives 0 sections and sectionTitle property never getting called.
This setup was/is working correctly with Swift3 in Xcode8.3.
Even this is working with Swift3.2 in Xcode9-beta.
But if I switch to Swift4 in Xcode9-beta, it's not working.
Add #objc to the transient property, so:
#objc var sectionTitle: String? {
//this is **transient** property
//to set it as transient, check mark the box with same name in data model
return time!.getTimeStrWithDayPrecision()
}
I just switched 'Swift 3 #objc inference' in the build settings to 'on' and all works fine again.

NSFetchedResultsController Sort Descriptor for Last Character Of String

How do you set a NSSortDescriptor which will sort by an attribute (but the last character of it?)
For example, if I have the following barcodes...
0000000005353
0000000000224
0000000433355
It should sort using last character, in asc or desc order. So like 3,4,5 in this example. Which would create section headers 3,4,5.
The current code I have gives me an error, sayings the "fetched object at index 7 has an out of order section name '9'. Objects must be sorted by section name. Which tells me I messed up the sort. To understand more please look at the code as I'm using transient properties on the core data model.
The idea is that "numberendsection", should sort from the end of the number as I described previously.
The other two sorts I describe work perfectly right now.
Inventory+CoreDataProperties.swift
import Foundation
import CoreData
extension Inventory {
#NSManaged var addCount: NSNumber?
#NSManaged var barcode: String?
#NSManaged var currentCount: NSNumber?
#NSManaged var id: NSNumber?
#NSManaged var imageLargePath: String?
#NSManaged var imageSmallPath: String?
#NSManaged var name: String?
#NSManaged var negativeCount: NSNumber?
#NSManaged var newCount: NSNumber?
#NSManaged var store_id: NSNumber?
#NSManaged var store: Store?
//This is used for A,B,C ordering...
var lettersection: String? {
let characters = name!.characters.map { String($0) }
return characters.first?.uppercaseString
}
//This is used for 1,2,3 ordering... (using front of barcode)
var numbersection: String? {
let characters = barcode!.characters.map { String($0) }
return characters.first?.uppercaseString
}
//This is used for 0000000123 ordering...(uses back number of barcode)
var numberendsection: String? {
let characters = barcode!.characters.map { String($0) }
return characters.last?.uppercaseString
}
}
InventoryController.swift - (showing only relevant part)
import UIKit
import CoreData
import Foundation
class InventoryController: UIViewController, UISearchBarDelegate, UITableViewDataSource, UITableViewDelegate, NSFetchedResultsControllerDelegate {
//Create fetchedResultsController to handle Inventory Core Data Operations
lazy var fetchedResultsController: NSFetchedResultsController = {
return self.setFetchedResultsController()
}()
func setFetchedResultsController() -> NSFetchedResultsController{
let inventoryFetchRequest = NSFetchRequest(entityName: "Inventory")
var primarySortDescriptor = NSSortDescriptor(key: "name", ascending: true)//by default assume name.
if(g_appSettings[0].indextype=="numberfront"){
primarySortDescriptor = NSSortDescriptor(key: "barcode", ascending: true)
}else if(g_appSettings[0].indextype=="numberback"){
primarySortDescriptor = NSSortDescriptor(key: "barcode", ascending: true)
}
//let secondarySortDescriptor = NSSortDescriptor(key: "barcode", ascending: true)
inventoryFetchRequest.sortDescriptors = [primarySortDescriptor]
let storefilter = g_appSettings[0].selectedStore!
let predicate = NSPredicate(format: "store = %#", storefilter) //This will ensure correct data relating to store is showing
inventoryFetchRequest.predicate = predicate
//default assume letter section
var frc = NSFetchedResultsController(
fetchRequest: inventoryFetchRequest,
managedObjectContext: self.moc,
sectionNameKeyPath: "lettersection",
cacheName: nil)
if(g_appSettings[0].indextype=="numberfront"){
frc = NSFetchedResultsController(
fetchRequest: inventoryFetchRequest,
managedObjectContext: self.moc,
sectionNameKeyPath: "numbersection",
cacheName: nil)
}else if(g_appSettings[0].indextype=="numberback"){
frc = NSFetchedResultsController(
fetchRequest: inventoryFetchRequest,
managedObjectContext: self.moc,
sectionNameKeyPath: "numberendsection",
cacheName: nil)
}
frc.delegate = self
return frc
}
Entity Diagram
Entity + Core Data Screenshot
Screenshot of Error and Code where it occurs
Inventory.swift
** Inventory.swift Entire File **
import UIKit
import CoreData
import Foundation
class InventoryController: UIViewController, UISearchBarDelegate, UITableViewDataSource, UITableViewDelegate, NSFetchedResultsControllerDelegate {
//Create fetchedResultsController to handle Inventory Core Data Operations
lazy var fetchedResultsController: NSFetchedResultsController = {
return self.setFetchedResultsController()
}()
func setFetchedResultsController() -> NSFetchedResultsController{
let inventoryFetchRequest = NSFetchRequest(entityName: "Inventory")
var primarySortDescriptor = NSSortDescriptor(key: "name", ascending: true)//by default assume name.
print("primarySortDescriptor...")
if(g_appSettings[0].indextype=="numberfront"){
primarySortDescriptor = NSSortDescriptor(key: "barcode", ascending: true)
}else if(g_appSettings[0].indextype=="numberback"){
primarySortDescriptor = NSSortDescriptor(key: "barcode", ascending: true)
}
print("set primarySortDescriptor")
//let secondarySortDescriptor = NSSortDescriptor(key: "barcode", ascending: true)
inventoryFetchRequest.sortDescriptors = [primarySortDescriptor]
print("set sort descriptors to fetch request")
var storefilter : Store? = nil
if(g_appSettings[0].selectedStore != nil){
storefilter = g_appSettings[0].selectedStore
let predicate = NSPredicate(format: "store = %#", storefilter!) //This will ensure correct data relating to store is showing
inventoryFetchRequest.predicate = predicate
}
//default assume letter section
var frc = NSFetchedResultsController(
fetchRequest: inventoryFetchRequest,
managedObjectContext: self.moc,
sectionNameKeyPath: "lettersection",
cacheName: nil)
if(g_appSettings[0].indextype=="numberfront"){
frc = NSFetchedResultsController(
fetchRequest: inventoryFetchRequest,
managedObjectContext: self.moc,
sectionNameKeyPath: "numbersection",
cacheName: nil)
}else if(g_appSettings[0].indextype=="numberback"){
frc = NSFetchedResultsController(
fetchRequest: inventoryFetchRequest,
managedObjectContext: self.moc,
sectionNameKeyPath: "numbersection",
cacheName: nil)
}
print("set the frc")
frc.delegate = self
return frc
}
#IBOutlet weak var searchBar: UISearchBar!
#IBOutlet weak var inventoryTable: UITableView!
var moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext //convinience variable to access managed object context
// Start DEMO Related Code
var numberIndex = ["0","1","2","3","4","5","6","7","8","9"]
var letterIndex = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"]
var previousNumber = -1 //used so we show A,A, B,B, C,C etc for proper testing of sections
func createInventoryDummyData(number: Int) -> Inventory{
let tempInventory = NSEntityDescription.insertNewObjectForEntityForName("Inventory", inManagedObjectContext: moc) as! Inventory
if(number-1 == previousNumber){
tempInventory.name = "\(letterIndex[number-2])-Test Item # \(number)"
previousNumber = -1//reset it again
}else{
tempInventory.name = "\(letterIndex[number-1])-Test Item # \(number)"
previousNumber = number //set previous letter accordingly
}
tempInventory.barcode = "\(number)00000000\(number)"
tempInventory.currentCount = 0
tempInventory.id = number
tempInventory.imageLargePath = "http://website.tech//uploads/inventory/7d3fe5bfad38a3545e80c73c1453e380.png"
tempInventory.imageSmallPath = "http://website.tech//uploads/inventory/7d3fe5bfad38a3545e80c73c1453e380.png"
tempInventory.addCount = 0
tempInventory.negativeCount = 0
tempInventory.newCount = 0
tempInventory.store_id = 1 //belongs to same store for now
//Select a random store to belong to 0 through 2 since array starts at 0
let aRandomInt = Int.random(0...2)
tempInventory.setValue(g_storeList[aRandomInt], forKey: "store") //assigns inventory to one of the stores we created.
return tempInventory
}
func createStoreDummyData(number:Int) -> Store{
let tempStore = NSEntityDescription.insertNewObjectForEntityForName("Store", inManagedObjectContext: moc) as! Store
tempStore.address = "100\(number) lane, Miami, FL"
tempStore.email = "store\(number)#centraltire.com"
tempStore.id = number
tempStore.lat = 1.00000007
tempStore.lng = 1.00000008
tempStore.name = "Store #\(number)"
tempStore.phone = "123000000\(number)"
return tempStore
}
// End DEMO Related Code
override func viewDidLoad() {
super.viewDidLoad()
print("InventoryController -> ViewDidLoad -> ... starting inits")
// // Do any additional setup after loading the view, typically from a nib.
// print("InventoryController -> ViewDidLoad -> ... starting inits")
//
//First check to see if we have entities already. There MUST be entities, even if its DEMO data.
let inventoryFetchRequest = NSFetchRequest(entityName: "Inventory")
let storeFetchRequest = NSFetchRequest(entityName: "Store")
do {
let storeRecords = try moc.executeFetchRequest(storeFetchRequest) as? [Store]
//Maybe sort descriptor here? But how to organize into sectioned array?
if(storeRecords!.count<=0){
g_demoMode = true
print("No store entities found. Demo mode = True. Creating default store entities...")
var store : Store //define variable as Store type
for index in 1...3 {
store = createStoreDummyData(index)
g_storeList.append(store)
}
//save changes for the stores we added
do {
try moc.save()
print("saved to entity")
}catch{
fatalError("Failure to save context: \(error)")
}
}
let inventoryRecords = try moc.executeFetchRequest(inventoryFetchRequest) as? [Inventory]
//Maybe sort descriptor here? But how to organize into sectioned array?
if(inventoryRecords!.count<=0){
g_demoMode = true
print("No entities found for inventory. Demo mode = True. Creating default entities...")
var entity : Inventory //define variable as Inventory type
for index in 1...52 {
let indexFloat = Float(index/2)+1
let realIndex = Int(round(indexFloat))
entity = createInventoryDummyData(realIndex)
g_inventoryItems.append(entity)
}
//save changes for inventory we added
do {
try moc.save()
print("saved to entity")
}catch{
fatalError("Failure to save context: \(error)")
}
print("finished creating entities")
}
}catch{
fatalError("bad things happened \(error)")
}
//perform fetch we need to do.
do {
try fetchedResultsController.performFetch()
} catch {
print("An error occurred")
}
print("InventoryController -> viewDidload -> ... finished inits!")
}
override func viewWillAppear(animated: Bool) {
print("view appearing")
//When the view appears its important that the table is updated.
//Look at the selected Store & Use the LIST of Inventory Under it.
//Perform another fetch again to get correct data~
do {
//fetchedResultsController. //this will force setter code to run again.
print("attempting fetch again, reset to use lazy init")
fetchedResultsController = setFetchedResultsController() //sets it again so its correct.
try fetchedResultsController.performFetch()
} catch {
print("An error occurred")
}
inventoryTable.reloadData()//this is important to update correctly for changes that might have been made
}
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
print("inventoryItemControllerPrepareForSegueCalled")
if segue.identifier == "inventoryInfoSegue" {
let vc = segue.destinationViewController as! InventoryItemController
if let cell = sender as? InventoryTableViewCell{
vc.inventoryItem = cell.inventoryItem! //sets the inventory item accordingly, passing its reference along.
}else{
print("sender was something else")
}
}
}
func tableView(tableView: UITableView, sectionForSectionIndexTitle title: String, atIndex index: Int) -> Int {
//This scrolls to correct section based on title of what was pressed.
return letterIndex.indexOf(title)!
}
func sectionIndexTitlesForTableView(tableView: UITableView) -> [String]? {
//This is smart and takes the first letter of known sections to create the Index Titles
return self.fetchedResultsController.sectionIndexTitles
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let sections = fetchedResultsController.sections {
let currentSection = sections[section]
return currentSection.numberOfObjects
}
return 0
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("InventoryTableCell", forIndexPath: indexPath) as! InventoryTableViewCell
let inventory = fetchedResultsController.objectAtIndexPath(indexPath) as! Inventory
cell.inventoryItem = inventory
cell.drawCell() //uses passed inventoryItem to draw it's self accordingly.
return cell
}
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if let sections = fetchedResultsController.sections {
let currentSection = sections[section]
return currentSection.name
}
return nil
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if let sections = fetchedResultsController.sections {
return sections.count
}
return 0
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
//dispatch_async(dispatch_get_main_queue()) {
//[unowned self] in
print("didSelectRowAtIndexPath")//does not recognize first time pressed item for some reason?
let selectedCell = self.tableView(tableView, cellForRowAtIndexPath: indexPath) as? InventoryTableViewCell
self.performSegueWithIdentifier("inventoryInfoSegue", sender: selectedCell)
//}
}
#IBAction func BarcodeScanBarItemAction(sender: UIBarButtonItem) {
print("test of baritem")
}
#IBAction func SetStoreBarItemAction(sender: UIBarButtonItem) {
print("change store interface")
}
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
print("text is changing")
}
func searchBarCancelButtonClicked(searchBar: UISearchBar) {
print("ended by cancel")
searchBar.text = ""
searchBar.resignFirstResponder()
}
func searchBarSearchButtonClicked(searchBar: UISearchBar) {
print("ended by search")
searchBar.resignFirstResponder()
}
func searchBarTextDidEndEditing(searchBar: UISearchBar) {
print("ended by end editing")
searchBar.resignFirstResponder()
}
#IBAction func unwindBackToInventory(segue: UIStoryboardSegue) {
print("unwind attempt")
let barcode = (segue.sourceViewController as? ScannerViewController)?.barcode
searchBar.text = barcode!
print("barcode="+barcode!)
inventoryTable.reloadData()//reload the data to be safe.
}
}
//Extention to INT to create random number in range.
extension Int
{
static func random(range: Range<Int> ) -> Int
{
var offset = 0
if range.startIndex < 0 // allow negative ranges
{
offset = abs(range.startIndex)
}
let mini = UInt32(range.startIndex + offset)
let maxi = UInt32(range.endIndex + offset)
return Int(mini + arc4random_uniform(maxi - mini)) - offset
}
}
NOTE::
I've cleared phone database also, just in case it was old database by deleting the app (holding down till it wiggles and deleting).
When your persistent store for Core Data is stored in SQLite (which I am assuming here otherwise the other answers would have worked already) you can't use computed properties or transient properties.
However, you can alter your data model so that you are storing the last digit of that bar code in its own property (known as denormalizing) and then sort on that new property. That is the right answer.
You can also do a secondary sort after you have done a fetch. However that means that you are holding a sorted array outside of the NSFetchedResultsController and you will then need to maintain the order of that array as you receive delegate callbacks from the NSFetchedResultsController. This is the second best answer.
If you can change the data model, then add a sort property. Otherwise your view controller code will be more complex because of the second sort.
You can add a comparator to your NSSortDescriptor
example
NSSortDescriptor *sortStates = [NSSortDescriptor sortDescriptorWithKey:#"barcode"
ascending:NO
comparator:^(id obj1, id obj2) {
[obj1 substringFromIndex:[obj1 length] - 1];
[obj2 substringFromIndex:[obj2 length] - 1];
return [obj1 compare: obj2])
}];
I think that you can use transient property in order to achieve what you want:
In order for it to work properly you have to provide implementation of this property in Inventory class.
var lastCharacter: String? {
let characters = barcode!.characters.map { String($0) }
return characters.last?.uppercaseString
}
Having 'lastCharacter' property set up correctly you can create sort descriptor that will allow you to achieve what you want:
NSSortDescriptor(key: "lastCharacter", ascending: true)
So it turns out that my method of trying to sort on a transient property does not work with NSSortDescriptors, the value has to be a real persisted one in the database.
Therefore, my solution was to create a new variable called barcodeReverse in the entity and at the time I enter data into the database for the barcode I also enter a reversed version using this code.
String(tempInventory.barcode!.characters.reverse())
tempInventory is an instance of my coreData class, and barcode a property on it. Simply just use characters.reverse() on the string.
Then you simply do the following:
primarySortDescriptor = NSSortDescriptor(key: "barcodeReverse", ascending: true)
and set frc like so...
frc = NSFetchedResultsController(
fetchRequest: inventoryFetchRequest,
managedObjectContext: self.moc,
sectionNameKeyPath: "numberendsection",
cacheName: nil)
and finally the inventory extension should look like this.
//This is used for 0000000123 ordering...(uses back number of barcode)
var numberendsection: String? {
let characters = barcodeReverse!.characters.map { String($0) }
return characters.first?.uppercaseString
}
This will create the sections and order correctly using the last digit of the barcode.

How to take Core Data and Filter it with NSPredicate Swift

I am currently learn and creating a basic decision app. The basics of the app is to take user input for a category they would like to do and then all the things that want to fill that category with.
Now I am wanting to display the results on a table view which works but I also what to click on each individual category that they recently used and be able to see the things that they placed under ever category. I was getting everything that was being save to the Core Data but now I am trying to use NSPredicate to filter out what I need. When I run the App there is nothing in the table view.
mainName I have passed in from a different view controller to capture and set what the name of the category was to help filter the data. I was trying to use it in the predicate as a filter.
I don't know if what I am doing is right but help would be great. This is independent study project I am doing to help finish my degree and everything I know is self taught so far. If what I have is completely wrong please tell me. This is just one of the hundreds of different ways I have tried to get this right.
#IBOutlet weak var tableview: UITableView!
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return whats.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell")! as UITableViewCell
let Doing = whats[indexPath.row]
cell.textLabel!.text = Doing.valueForKey("what") as? String
return cell
}
func loadData(){
let appDelegate = UIApplication.sharedApplication().delegate as? AppDelegate
if let context = appDelegate?.managedObjectContext{
let fetchRequest = NSFetchRequest(entityName: "Doing")
let namePredicate = NSPredicate(format: "namesR.noun = '\(mainName)'")
(whats as NSArray).filteredArrayUsingPredicate(namePredicate)
fetchRequest.predicate = namePredicate
do {
let results =
try context.executeFetchRequest(fetchRequest)
whats = results as! [NSManagedObject]
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
}
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
}
override func viewDidLoad() {
super.viewDidLoad()
loadData()
}
If you want use Core Data in your tableView based app? You can use NSFetchedResultsController class and their delegate protocol methods. This class specially designed for a tableView!
Official Documentation for NSFetchedResultsController class
From my understanding, you're saying you have a Category entity and an Item entity with many Item for each Category. In that case, in the view controller where you want to display the Items, you need to have a Category variable to use in your predicate so that you only get back the Items associated with that Category.
class ItemsTableVC {
var category: Category!
lazy var fetchedResultsController: NSFetchedResultsController = {
let fetchRequest = NSFetchRequest(entityName: "Item")
fetchRequest.sortDescriptors = []
fetchRequest.predicate = NSPredicate(format: "category == %#", self.category)
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.sharedContext, sectionNameKeyPath: nil, cacheName: nil)
fetchedResultsController.delegate = self
return fetchedResultsController
}()
}

Displaying tableView items by CoreData attribute

I am trying to group section 0 my tableView by the "category" attribute of an item.
Example:
Drinks: (item.category = header)
Dr. Prepper
Coke
Pepsi
Kitchen: (item.category = header)
Pots
Pans...etc.
CrossOff(header)
items
I still want section1 to be the item.slcross (or the last section if each group has to be their own section...and it doesn't have to be grouped).
When I change the secondarySortDescriptor key from "slitem" to "slcategory" and use the sectionHeader code below, it returns "nil". I also tried using
let sectionHeader2 = "\(item.valueForKeyPath("slcategory"))" but still had the same effect with both "slitem" and "slcategory".
Do I have to use a sort descriptor for each category or is there a way to make it pull the category attribute for the item and group the like categories together?
FRC set up:
let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
var frc : NSFetchedResultsController = NSFetchedResultsController()
var selectedItem : List?
func itemFetchRequest() -> NSFetchRequest{
let fetchRequest = NSFetchRequest(entityName: "List")
let primarySortDescription = NSSortDescriptor(key: "slcross", ascending: true)
let secondarySortDescription = NSSortDescriptor(key: "slitem", ascending: true)
fetchRequest.sortDescriptors = [primarySortDescription, secondarySortDescription]
fetchRequest.predicate = NSPredicate(format:"slist == true")
return fetchRequest
}
func getFetchRequetController() ->NSFetchedResultsController{
frc = NSFetchedResultsController(fetchRequest: itemFetchRequest(), managedObjectContext: moc, sectionNameKeyPath: "slcross", cacheName: nil)
return frc
}
TableViewHeaders:
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String?{
let entityDescription = NSEntityDescription.entityForName("List", inManagedObjectContext: moc)
let item = List(entity: entityDescription!, insertIntoManagedObjectContext: moc)
let sectionHeader = "\(item.slcategory)"
let sectionHeader1 = "Items in Cart - #\(frc.sections![section].numberOfObjects)"
if (frc.sections!.count > 0) {
let sectionInfo = frc.sections![section]
if (sectionInfo.name == "0") {
return sectionHeader2
} else {
return sectionHeader1
}
} else {
return nil
}
}
There are a few ways to do this, but probably the easiest is to add a new method to your NSManagedObject subclass. The method returns a string which will be used as the title for the section; so if slcross is false, it returns the value of slcategory, and if slcross is true it returns "True":
func sectionIdentifier() -> String {
if (self.slcross) {
return "True"
} else {
return "\(self.slcategory)"
}
}
(Note this code goes in your List class definition, not your view controller).
In the view controller, use this sectionIdentifier as the sectionNameKeyPath for your FRC:
frc = NSFetchedResultsController(fetchRequest: itemFetchRequest(), managedObjectContext: moc, sectionNameKeyPath: "sectionIdentifier", cacheName: nil)
For that to work, it is imperative that the objects are sorted correctly: first by slcross, then by slcategory:
let primarySortDescription = NSSortDescriptor(key: "slcross", ascending: true)
let secondarySortDescription = NSSortDescriptor(key: "slcategory", ascending: true)
Finally, amend your titleForHeaderInSection to use the section name (which the FRC gets from sectionIdentifier), but replacing the "True" with your computed string:
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String?{
if (frc.sections!.count > 0) {
let sectionInfo = frc.sections![section]
if (sectionInfo.name == "True") {
return "Items in Cart - #\(sectionInfo.numberOfObjects)"
} else {
return sectionInfo.name
}
} else {
return nil
}
}
From Apple Docs...
When you initialize the fetch results controller, you provide four parameters: .....
....Optionally, a key path on result objects that returns the section name. The controller uses the key path to split the results into sections (passing nil indicates that the controller should generate a single section).....
After creating an instance, you invoke performFetch: to actually execute the fetch.
If you want to sort the sections by category then you need to make the sectionNameKeyPath argument in the NSFetchedResultsController init to be your "category" property instead of your "slcross" property.
When I've used this in the past I've also included that same property that I've set as the sectionNameKeyPath in my sort descriptors, but not sure if that is actually needed or not.
Hope I have answered your question?

Swift Using NSFetchedResultsController and UISearchBarDelegate

I am looking for a decent solution to this problem. I am wanting to implement some simple search functionality on a TableView that I have.
All the examples I have found either use the deprecated UISearchDisplayController or use the new UISearchController but without NSFetchedResultsController
Currently this is populated using Core Data / NSFetchedResultsController
So far I have managed to get it to a point where I can gather the users' search string (woo!). I am aware that I may need a separate FRC to perform the search on, but as mentioned above all attempts up to now have failed.
My class is conforming to the following protocols:
class JobListController: UIViewController, UITableViewDelegate, UITableViewDataSource, NSFetchedResultsControllerDelegate, UISearchBarDelegate{
I can't use UITableViewController as I have already written loads of existing functionality that relies on this class being a UIViewController
I have my two IBOutlets:
#IBOutlet var tblJobs : UITableView!
#IBOutlet weak var searchBar: UISearchBar!
and my empty arrays to hold my various Core Data bits and bobs:
var workItems = [Work]()
var filteredWorkItems = [Work]()
Here is how I am initialising my FRC, along with my MOC and I've left in my empty second FRC as I am quite sure it will be needed at some point:
let managedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
lazy var fetchedResultsController: NSFetchedResultsController = {
let workFetchRequest = NSFetchRequest(entityName: "Work")
let primarySortDescriptor = NSSortDescriptor(key: "createdDate", ascending: true)
let secondarySortDescriptor = NSSortDescriptor(key: "town", ascending: true)
workFetchRequest.sortDescriptors = [primarySortDescriptor, secondarySortDescriptor]
let frc = NSFetchedResultsController(
fetchRequest: workFetchRequest,
managedObjectContext: self.managedObjectContext!,
sectionNameKeyPath: "createdDate",
cacheName: nil)
frc.delegate = self
return frc
}()
var searchResultsController: NSFetchedResultsController?
In my viewDidLoad function I am setting up the delegates / data source for my table and the searchBar:
tblJobs.delegate = self
tblJobs.dataSource = self
searchBar.delegate = self
and here is the searchBar function which is where I am up to. The stringMatch variable is leftover from a previous attempt, I am hoping to be able to search by a multitude of different parameters here, but if I can get just one working it will be a solid start.
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
println("Search text is \(searchText)")
self.filteredWorkItems = self.workItems.filter({( work: Work) -> Bool in
//
let stringMatch = work.postcode.rangeOfString(searchText)
return stringMatch != nil
})
if(filteredWorkItems.count == 0){
searchActive = false;
} else {
searchActive = true;
}
self.tblJobs.reloadData()
}
Here is my cellForRowAtIndexPath function to show how I am pulling data from the fetchedResultsController
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
let cell = self.tblJobs.dequeueReusableCellWithIdentifier(
"JobCell", forIndexPath: indexPath)
as! JobTableViewCell
let workItem = fetchedResultsController.objectAtIndexPath(indexPath) as! Work
//...
return cell
}
So you can see I've got a few things going on here, ultimately I am wanting to figure out how I use my newly gotten searchText string to query against my FRC, and then for the results to filter properly in the View.
Update:
I have attempted to add the search string to my NSPredicate in the FRC like so:
lazy var fetchedResultsController: NSFetchedResultsController = {
../
workFetchRequest.predicate = NSPredicate(format:"title contains[cd] %#", savedSearchTerm!)
//...
return frc
}()
Which results in 'JobListController.Type' does not have a member named 'savedSearchTerm'
At the top of my class I have set it up like this:
var savedSearchTerm: NSString?
So not sure what I'm doing wrong?
From your code, I assume you want to use the same table view to display the results. So you just need to update your FRC with a new filter based on the search term.
Store the search term in a variable. In the FRC factory function, include the predicate, something like this:
request.predicate = searchText?.characters.count > 0 ?
NSPredicate(format:"title contains[cd] %#", searchText!) : nil
When the text changes, reset the FRC and reload.
fetchedResultsController = nil
tableView.reloadData()
If you have additional filters, such as scope buttons, add additional terms to the predicate.
Swift 4.2
This is a working solutions from one of my app. I have trimmed it down to make it simple to show how it works.
I have a database of around 6000 rows in which I search via 3 different scopes: Notes, Author and Keywords. I call initializeFetchedResultsController in the viewDidLoad function with default values. And later when user starts typing in the Search field, start calling it again with the required value.
The Fetch part:
let EMPTY_STRING = "" // I don't like string literals in my code so define them as static variables separately
// Giving two default values
func initializeFetchedResultsController(_ text: String: EMPTY_STRING, _ scope: Int = 0) {
fetchRequest.sortDescriptors = [NSSortDescriptor(key: NotesAttributes.author.rawValue, ascending: true)]
if searchedStringTemp != EMPTY_STRING { // Whatever conditions you want to pass on
let p0 = NSPredicate(format: NotesAttributes.scope.rawValue + " != \(scope)")
let p1 = NSPredicate(format: "\(column) CONTAINS[cd] %#", "\(text)")
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [p0, p1])
}
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: AppDelegate().sharedInstance().persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
fetchedResultsController.delegate = self
do {
try self.fetchedResultsController.performFetch()
} catch {
let fetchError = error as NSError
print("Error 12312: Unable to Perform Fetch Request")
print("\(fetchError), \(fetchError.localizedDescription)")
}
}
The search controller:
// MARK: - UISearchBar Delegate
extension AllNotesVC: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
// This is my function which I call to start search
notesSearched(searchBar.text!, searchBar.scopeButtonTitles![selectedScope])
}
}
// MARK: - UISearchResultsUpdating Delegate
extension AllNotesVC: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
let text = searchController.searchBar.text!
let searchBar = searchController.searchBar
let scope = searchBar.scopeButtonTitles![searchBar.selectedScopeButtonIndex]
// This is my function which I call to start search
notesSearched(text, scope)
}
}
And this in my notesSearched method which re-initialize the fetch results controller and reload the table every time.
// MARK: - Private instance methods
private func notesSearched(_ text: String, _ scope: Int) {
initializeFetchedResultsController(text, scope)
tableView.reloadData()
}
While calling doing so many table reloads might not be the most efficient way to do this, but it is lightening fast, and since this updates the table in real-time as user is typing it provides a wonderful user experience.

Resources