Swift - Sorting core data in sections - ios

I've managed to sort the data received from my core data alphabetically, and now I want put the usernames in sections for each letter. I understand that the easiest way to do this is by using NSFetchedResultsController, but I can't figure out how to use this (very few tutorials covering Swift).
So my code looks like this:
override func viewDidAppear(animated: Bool) {
let appDel:AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
let context:NSManagedObjectContext = appDel.managedObjectContext!
let freq = NSFetchRequest(entityName: "Message")
let en = NSEntityDescription.entityForName("Message", inManagedObjectContext: context)
let fetchRequest = NSFetchRequest(entityName: "Message")
let sortDescriptor = NSSortDescriptor(key: "username", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
myList = context.executeFetchRequest(fetchRequest, error: nil) as [Model]
tv.reloadData()
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
return myList.count
}
I was hoping someone could shed some light over the NSFetchedResultsController and help me get on my way.
If I'm not completely wrong, the initialization looks something like this, although I can't figure out the "cache name":
let resultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: myList, sectionNameKeyPath: "username", cacheName: <#String?#>)
Any suggestions on how to proceed would be appreciated.

You can set up the fetched results controller easily by copying and tweaking the code from the Xcode template. (Create a new project, Master-Detail, check "Use Core Data", copy from MasterViewController.)
The following is not the ideal solution, but I think it is appropriate for your level of experience. When you add the username attribute to the Message entity object, also add another attribute with the first letter. Then use the name of this new attribute as the sectionNameKeyPath parameter when creating the fetched results controller.
Don't worry too much about the cache parameter. You can just put any string there, such as "Root", or even pass nil to not use cache which is also fine in most use cases.

Related

Is performing fetch for NSFetchedResultsController on a background thread a good idea?

When a user runs the app, he first has to login. If the login is successful, a function called setUpSaving is triggered. In setUpSaving, as I wrote in the commented part of the code, ( "some firebase stuff happens, grabs all the messages, and for each message, one by one, it goes through createMessageWithText and inserts the message into core data, eventually saving it. "
So, my problem is here. Let's assume that a user has 20 000 messages associated with someone he is messaging. When he goes to the ChatLogController, all the messages must be present for the user. The way I did that is by using NSFetchedResultsController perform fetch to load all the messages that were saved in the login controller. My problem with that is that all of this is on the main queue, and sometimes, that has frozen the UI for quite some time. My code for chatLogController is below (not all of it, just the fetchedResultsController part and the collectionView), I was wondering how I could perform fetch on a background queue, yet still update the collectionView on the main queue.
func setUpSaving() {
///////some firebase stuff happens, grabs all the messages, and for each message, one by one, it goes through createMessageWithText and inserts the message into core data,
somewhere along the line context.save() gets triggered saving the message.
}
private func createMessageWithText(text: String, friend: Friend, context: NSManagedObjectContext, date: NSDate, isSender: Bool = false, sentStatus: String, fromID: String) -> Mesages {
let message = NSEntityDescription.insertNewObjectForEntityForName("Mesages", inManagedObjectContext: context) as! Mesages
message.user = friend
message.text = text
message.timestamp = date
message.isSender = isSender
message.fromID = fromID
message.status = sentStatus
return message
}
}
lazy var fetchedResultsControler: NSFetchedResultsController = {
let fetchRequest = NSFetchRequest(entityName: "Mesages")
fetchRequest.fetchBatchSize = 20
fetchRequest.includesPendingChanges = false
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "timestamp", ascending: true)]
fetchRequest.predicate = NSPredicate(format: "user.id = %#", self.friend!.id!)
let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: moc, sectionNameKeyPath: nil, cacheName: nil)
frc.delegate = self
return frc
}()
override func viewDidLoad() {
super.viewDidLoad()
NavigationItems()
revealStatus()
do {
try fetchedResultsControler.performFetch()
} catch let err {
print(err)
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) ->
Int {
if let count = fetchedResultsControler.sections?[0].numberOfObjects {
return count
}
return 0
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = super.collectionView(collectionView, cellForItemAtIndexPath: indexPath) as! JSQMessagesCollectionViewCell
return cell
}
No, you should not fetch an FRC that will drive the UI on a background thread.
Your issue is the idea behind loading 20,000 items after login. No-one can or will view that number of messages on a mobile device. You might want to get 20 after login, and the total count, but that's it.
When the user goes to view messages you should load pages, of perhaps 20 to 100 messages, on demand. In this way you don't use resources the user hasn't asked you to use or take any processing time to do it.
Search should also be done by a server.

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

CoreData FetchRequest problems

I'm teaching myself to programme and have thought up this project for myself to learn. I'm having trouble with my code, I was able to save it correctly and load the first state population TVC. However, I'm having problems with the state and number of animals per state TVC. I want to total it per a state. So I would be adding the dogs and cats population together and get the total per a state, but it brings Alabama separately with two different population, can someone help me with this please.
the model below shows how I want it, I'm able to output to State Population correctly but now the second one.
What my code is doing for the second one is that it's getting the data from coredata but I'm using sort descriptor because I don't know any other way to pull the data.
var totalEntries : Int = 0
let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
var frc : NSFetchedResultsController = NSFetchedResultsController()
func fetchRequest() -> NSFetchRequest {
let fetchRequest = NSFetchRequest(entityName: "Animals")
let sortDescriptor = NSSortDescriptor(key: "state", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
return fetchRequest
}
func getFRC() -> NSFetchedResultsController {
frc = NSFetchedResultsController(fetchRequest: fetchRequest(), managedObjectContext: moc, sectionNameKeyPath: nil, cacheName: nil)
return frc
}
override func viewDidLoad() {
super.viewDidLoad()
frc = getFRC()
frc.delegate = self
do {
try frc.performFetch()
} catch {
print("Failed to fetch data")
return
}
totalEntries = moc.countForFetchRequest(fetchRequest(), error: nil) as Int!
self.tableView.reloadData()
}
override func viewDidAppear(animated: Bool) {
frc = getFRC()
frc.delegate = self
do {
try frc.performFetch()
} catch {
print("Failed to fetch data")
return
}
totalEntries = moc.countForFetchRequest(fetchRequest(), error: nil) as Int!
self.tableView.reloadData()
}
Your problem is the fetched results controllers aren't designed to show aggregated fetch results like you desire, hence you see all the underlying data instead of the aggregate.
You could use the FRC if you cheat... Set the section name of the FRC to the state name, then you will have one section in the table per state. In the table delegate return 1 for the number of rows in each section. When configuring the cell use KVC to #sum the populations of all of the animals in that state (the rows for that section as understood by the FRC).
This is a memory and runtime inefficient solution... It could be improved by caching the calculated sums, but you're adding logic on top of bad design then.
The correct approach would be to abandon the FRC and use a simple array of dictionaries. This would be generated by changing your fetch request to return dictionary type and configuring it to calculate the sums for you. That's done with an expression something like:
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
expressionDescription.name = #"sumOfPopulations";
expressionDescription.expression = [NSExpression expressionForKeyPath:#"#sum.population"];
expressionDescription.expressionResultType = NSDecimalAttributeType;

How can I sort a UITableView using CoreData information in swift?

I'm new to core data and I'm trying to sort my UITableView from data fetched from CoreData. I know how to do this using an array, but I'm struggling to find out how to do it with CoreData. I've read similar questions but it's either in Objective-c -which I haven't learned- or it's not solving my specific problem. My CoreData entity is named numbers and it stores numbers of type Float that the user passes in. My code here is in my viewDidAppear method, the first line is my declaration of myNumbers array which is globally available in that class. ***Take note that my entity has more than one attribute, I have an attribute "number" and an attribute "name" which is a string.
var myNumbers: Array<AnyObject> = []
override func viewDidAppear(animated: Bool) {
let appDeleg: AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
let context: NSManagedObjectContext = appDeleg.managedObjectContext!
let fetchreq = NSFetchRequest(entityName: "Numbers")
//Would I be adding the sort descriptor here?
let sortDescriptor = NSSortDescriptor(key: "numbersAttribute", ascending: true)
fetchreq.sortDescriptors = [sortDescriptors]
myNumbers = context.executeFetchRequest(fetchreq, error: nil)!
tableView.reloadData()
}
Now this is the code in my cellForRowAtIndexPath method:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let CellID: NSString = "myCell"
var cell: CustomCell = tableView.dequeueReusableCellWithIdentifier(CellID) as CustomCell
if let indexpth = indexPath as NSIndexPath! {
var data: NSManagedObject = myNumbers[indexpth.row] as NSManagedObject
cell.theTextLabel?.text = data.valueForKeyPath("tName") as? String
cell.theDetailTextLabel?.text = data.valueForKeyPath("tNumber") as? String
}
I want to order the data in my table view based on the "tNumber" attribute in the Numbers entity.
You can do this using an NSSortDescriptor. Do the following before doing the fetch request to populate myNumbers.
// Note that the key is the attribute of your Numbers entity that you are sorting
let sortDescriptor = NSSortDescriptor(key: "tNumber", ascending: true)
fetchreq.sortDescriptors = [sortDescriptors]
This should work, but the best way to accomplish loading data from Core Data into a table is to use a NSFetchedResultsController. This is especially true if you're dealing with a very large or transient data set.

Re-order cells in UITableView with swift and fetchedResultsController

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?

Resources