Sorting sections issue in NSFetchedResultsController - ios

I'm doing a chat app, where I'm storing chat time for each chat, now I need to display latest chats first, grouped in section, for eg: If I have 10 chats of today it should be under section named July 10, 2016 with last send chat at last. For this I'm sorting the list by chatTimestamp and I have another sectionDate field which stores the corresponding date. Below is a sample format of data that displays in core data.
<MyChat.ChatData: 0x7f8b71cdd190> (entity: ChatData; id: 0xd000000000140002 <x-coredata:….> ; data: {
chatDeviceType = ipad;
chatId = 3557;
chatOwnerName = “John Mathew”;
chatReadStatus = 1;
chatStatus = Received;
chatText = “Hi how are you?“;
chatTimestamp = "2015-09-21 10:41:37 +0000";
chatType = Mine;
imageData = nil;
imageUrl = nil;
sectionDate = "Sep 21, 2015";
users = "0xd000000000080000 <x-coredata:…>”;
})
This is a portion of my code so far
var fetchedResultsController: NSFetchedResultsController = NSFetchedResultsController()
override func viewDidLoad() {
super.viewDidLoad()
setupFRC(limit: LIMIT)
......
}
func setupFRC(limit limit:Int) {
messageMaxLimit += limit
let objectsCount = self.stack.mainContext.countForFetchRequest(fetchRequest(self.stack.mainContext), error: nil)
NSFetchedResultsController.deleteCacheWithName("Root")
let request = self.fetchRequest(self.stack.mainContext)
self.fetchedResultsController = NSFetchedResultsController(fetchRequest: request,
managedObjectContext: self.stack.mainContext,
sectionNameKeyPath: "sectionDate",
cacheName: "Root")
self.fetchedResultsController.delegate = self
self.messageMaxLimit = self.messageMaxLimit > objectsCount ? objectsCount : self.messageMaxLimit
if objectsCount > self.messageMaxLimit
{
self.fetchedResultsController.fetchRequest.fetchOffset = objectsCount - self.messageMaxLimit
}
self.fetchData()
}
//To fetch data in FRC
func fetchData() {
do {
try self.fetchedResultsController.performFetch()
chatCollectionView.reloadData()
} catch {
assertionFailure("Failed to fetch: \(error)")
}
}
func fetchRequest(context: NSManagedObjectContext) -> FetchRequest<ChatData> {
let e = entity(name: "ChatData", context: context)
let fetch = FetchRequest<ChatData>(entity: e)
fetch.predicate = NSPredicate(format: "(SELF.users == %#)", currentUser!)
//Sort by chatTimeStamp
let sortDescriptor = NSSortDescriptor(key: "chatTimestamp", ascending: true)
fetch.sortDescriptors = [sortDescriptor]
return fetch
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
do {
try self.fetchedResultsController.performFetch()
} catch {
assertionFailure("Failed to fetch: \(error)")
}
chatCollectionView.reloadData()
}
Now the problem is that the chats seems to have order correctly, but the sections are in alphabetic order. But If I enter a chat and send it and when it reloads from controllerDidChangeContent it gets corrected.
I can't figure out why it doesn't load in correct order initially.I'm using a collection view for this. Am I doing anything wrong here?

I fixed the issue by removing the NSManaged attribute for sectionDate and added it like below and made the property transient in the core data model and it worked.
var sectionDate: String {
get {
self.willAccessValueForKey("sectionDate")
var ddtmp = self.primitiveValueForKey("sectionDate") as! String?
self.didAccessValueForKey("sectionDate")
if (ddtmp == nil)
{
ddtmp = Utilities.stringFromDate(self.chatTimestamp!, dateFormat: "MMM dd, yyyy")
self.setPrimitiveValue(ddtmp, forKey: "sectionDate")
}
return ddtmp!
}
}

Related

How to use a predicate for one-to-many managed objects in Core Data

I currently have two managed objects for Core Data that has one-to-many relationship.
Goal
extension Goal {
#nonobjc public class func createFetchRequest() -> NSFetchRequest<Goal> {
return NSFetchRequest<Goal>(entityName: "Goal")
}
#NSManaged public var title: String
#NSManaged public var date: Date
#NSManaged public var progress: NSSet?
}
Progress
extension Progress {
#nonobjc public class func createFetchRequest() -> NSFetchRequest<Progress> {
return NSFetchRequest<Progress>(entityName: "Progress")
}
#NSManaged public var date: Date
#NSManaged public var comment: String?
#NSManaged public var goal: Goal
}
For every goal, you can have multiple Progress objects. The problem is when I request a fetch for Progress with a particular Goal as the predicate, nothing is being returned. I have a suspicion that I'm not using the predicate properly.
This is how I request them.
First, I fetch Goal for a table view controller:
var fetchedResultsController: NSFetchedResultsController<Goal>!
if fetchedResultsController == nil {
let request = Goal.createFetchRequest()
let sort = NSSortDescriptor(key: "date", ascending: false)
request.sortDescriptors = [sort]
request.fetchBatchSize = 20
fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: self.context, sectionNameKeyPath: "title", cacheName: nil)
fetchedResultsController.delegate = self
}
fetchedResultsController.fetchRequest.predicate = goalPredicate
do {
try fetchedResultsController.performFetch()
} catch {
print("Fetch failed")
}
And pass the result to the next screen, Detail view controller:
if let vc = storyboard?.instantiateViewController(withIdentifier: "Detail") as? DetailViewController {
vc.goal = fetchedResultsController.object(at: indexPath)
navigationController?.pushViewController(vc, animated: true)
}
Finally, I fetch Progress using the Goal as the predicate from Detail view controller:
var goal: Goal!
let progressRequest = Progress.createFetchRequest()
progressRequest.predicate = NSPredicate(format: "goal == %#", goal)
if let progress = try? self.context.fetch(progressRequest) {
print("progress: \(progress)")
if progress.count > 0 {
fetchedResult = progress[0]
print("fetchedResult: \(fetchedResult)")
}
}
Goal is being returned properly, but I get nothing back for Progress. I've tried:
progressRequest.predicate = NSPredicate(format: "goal.title == %#", goal.title)
or
progressRequest.predicate = NSPredicate(format: "ANY goal == %#", goal)
but still the same result.
Following is how I set up the relationship:
// input for Progress from the user
let progress = Progress(context: self.context)
progress.date = Date()
progress.comment = commentTextView.text
// fetch the related Goal
var goalForProgress: Goal!
let goalRequest = Goal.createFetchRequest()
goalRequest.predicate = NSPredicate(format: "title == %#", titleLabel.text!)
if let goal = try? self.context.fetch(goalRequest) {
if goal.count > 0 {
goalForProgress = goal[0]
}
}
// establish the relationship between Goal and Progress
goalForProgress.progress.insert(progress)
// save
if self.context.hasChanges {
do {
try self.context.save()
} catch {
print("An error occurred while saving: \(error.localizedDescription)")
}
}
Actually you don't need to refetch the data. You can get the progress from the relationship
Declare progress as native Set
#NSManaged public var progress: Set<Progress>
In DetailViewController delete the fetch code in viewDidLoad and declare
var progress: Progress!
In the first view controller filter the progress
let goal = fetchedResultsController.object(at: indexPath)
if let vc = storyboard?.instantiateViewController(withIdentifier: "Detail") as? DetailViewController,
let progress = goal.progress.first(where: {$0.goal.title == goal.title}) {
vc.progress = progress
navigationController?.pushViewController(vc, animated: true)
}
And consider to name the to-many relationship in plural form (progresses)
I figured out that it's due to Core Data Fault where Core Data lazy loads the data and unless you explicitly access the data, the value will not be displayed.
You can either do something like the following:
let goal = fetchedResultsController.object(at: indexPath)
if let vc = storyboard?.instantiateViewController(withIdentifier: "Detail") as? DetailViewController,
let progress = goal.progress.first(where: {$0.goal.title == goal.title}) {
vc.goalTitle = goal.title
vc.date = progress.date
if let comment = progress.comment {
vc.comment = comment
}
navigationController?.pushViewController(vc, animated: true)
}
or setreturnsObjectsAsFaults to false.
Here's a good article on the topic.

Core Data Migration: Could not cast value of type 'NSManagedObject_MyType' to 'MyModule.MyType'

I'm doing a 'medium-weight' Core Data Migration. I'm using a Mapping Model to migrate from one legacy store / data model to a different store and different model (i.e. completely different .xcdatamodeld) files, and using custom NSEntityMigrationPolicy objects where applicable.
Where previously I had all sorts of objects unrelated on the object graph, I now want to have a master object Library that would enable me to easily wipe out all of the associated data (using the Cascade delete rule).
I've run into problems during the migration because of a custom method in my NSEntityMigrationPolicy subclass:
class LegacyToModernPolicy: NSEntityMigrationPolicy {
func libraryForManager(_ manager: NSMigrationManager) -> Library {
let fetchRequest: NSFetchRequest<Library> = NSFetchRequest(entityName: Library.entity().name!)
fetchRequest.predicate = nil
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "filename", ascending: true)]
fetchRequest.fetchLimit = 1
do {
// will fail here if NSFetchRequest<Library>
let results = try manager.destinationContext.fetch(fetchRequest)
log.info("results: \(results)")
if results.count == 1 {
// can fail here if NSFetchRequest<NSManagedObject>
return results.first! as! Library
} else {
let newLib = Library(context: manager.destinationContext)
return newLib
}
} catch {
log.error("Error fetching: \(error.localizedDescription)")
}
let newLib = Library(context: manager.destinationContext)
return newLib
}
}
An exception will be thrown, and the error message is:
Could not cast value of type 'NSManagedObject_Library_' (0x6100000504d0) to 'SongbookSimple.Library' (0x101679180).
The question is, why is that happening, and does it matter? Because a migration is happening, perhaps it's enough to return the NSManagedObject with the correct entity description ?
The reason is that during migration, you should not be using instances of NSManagedObject subclasses. You need to express all of these in the form of NSManagedObject. So the code above must become:
class LegacyToModernPolicy: NSEntityMigrationPolicy {
static func find(entityName: String,
in context: NSManagedObjectContext,
sortDescriptors: [NSSortDescriptor],
with predicate: NSPredicate? = nil,
limit: Int? = nil) throws -> [NSManagedObject] {
let fetchRequest: NSFetchRequest<NSManagedObject> = NSFetchRequest(entityName: entityName)
fetchRequest.predicate = predicate
fetchRequest.sortDescriptors = sortDescriptors
if let limit = limit {
fetchRequest.fetchLimit = limit
}
do {
let results = try context.fetch(fetchRequest)
return results
} catch {
log.error("Error fetching: \(error.localizedDescription)")
throw error
}
}
func libraryForManager(_ manager: NSMigrationManager) -> NSManagedObject {
do {
var library: NSManagedObject? = try LegacyToModernPolicy.find(entityName: Library.entity().name!,
in: manager.destinationContext,
sortDescriptors: [NSSortDescriptor(key: "filename", ascending: true)],
with: nil,
limit: 1).first
if library == nil {
let dInstance = NSEntityDescription.insertNewObject(forEntityName: Library.entity().name!, into: manager.destinationContext)
// awakeFromInsert is not called, so I have to do the things I did there, here:
dInstance.setValue(Library.libraryFilename, forKey: #keyPath(Library.filename))
dInstance.setValue(NSDate(timeIntervalSince1970: 0), forKey: #keyPath(Library.updatedAt))
library = dInstance
}
return library!
} catch {
fatalError("Not sure why this is failing!")
}
}}
You can read more about my less-than-fun experiences with Core Data Migrations here.

How to handle empty objects with Swift?

I have an app that keeps track of monthly expenses. I have an Expense entity with a month attribute that keeps track of current month expense is created on. I would then display the expenses for each month in a table view as shown here. The user can only switch left and right only if there are expenses within the next or the last month
#IBAction func backMonthButtonPressed(sender: AnyObject) {
print("Back Button Pressed")
currentMonth = NSCalendar.currentCalendar().dateByAddingUnit(.Month, value: -1, toDate: currentMonth, options: [])!
if checkMonth(currentMonth) {
updateFetch()
setMonth()
} else {
currentMonth = NSCalendar.currentCalendar().dateByAddingUnit(.Month, value: 1, toDate: currentMonth, options: [])!
}
tableView.reloadData()
}
#IBAction func nextMonthButtonPressed(sender: AnyObject) {
print("Next Button Pressed")
currentMonth = NSCalendar.currentCalendar().dateByAddingUnit(.Month, value: 1, toDate: currentMonth, options: [])!
if checkMonth(currentMonth) {
updateFetch()
setMonth()
} else {
currentMonth = NSCalendar.currentCalendar().dateByAddingUnit(.Month, value: -1, toDate: currentMonth, options: [])!
}
tableView.reloadData()
}
func checkMonth(month : NSDate) -> Bool {
let app = UIApplication.sharedApplication().delegate as! AppDelegate
let context = app.managedObjectContext //scratch pad
let fetchRequest = NSFetchRequest(entityName: "Expense")
fetchRequest.predicate = NSPredicate(format: "month == %#", month.MonthYearDateFormatter())
let count = context.countForFetchRequest(fetchRequest, error: nil)
if count > 0 {
print("There are expenses for this month \(month.MonthYearDateFormatter()). Show expenses")
return true
} else {
print("There are no expenses for this month \(month.MonthYearDateFormatter()). Do Nothing")
return false
}
}
My problem is this, in the unlikely scenario that the user created an expense back in June and didn't create an expense in August. How can I let the user still see his/her expense back in August without skipping it. Any ideas?
I made some optimisation before elaboration:
#IBAction func backMonthButtonPressed(sender: AnyObject) {
self.processMonth(step: -1)
}
#IBAction func nextMonthButtonPressed(sender: AnyObject) {
self.processMonth(step: 1)
}
// a method uses almost the same code for both cases, so it was merged
func processMonth(step: Int) {
let direction = (step < 1 ? "Back" : "Next")
print("\(direction) Button Pressed")
currentMonth = NSCalendar.currentCalendar().dateByAddingUnit(.Month, value: step, toDate: currentMonth, options: [])!
//if checkMonth(currentMonth) {
// I wouldn't test this because it locks you out from seeing empty month.
updateFetch()
setMonth()
//}
tableView.reloadData()
}
An answer to what you've exactly asked:
If your data source for your UITableView is set properly, you should be able to go through empty months though
// changed return type from `Bool` to `void` as I suggested in the method not to test empty month, as it could be useless
func checkMonth(month : NSDate) {
let app = UIApplication.sharedApplication().delegate as! AppDelegate
let context = app.managedObjectContext //scratch pad
let fetchRequest = NSFetchRequest(entityName: "Expense")
fetchRequest.predicate = NSPredicate(format: "month == %#", month.MonthYearDateFormatter())
let count = context.countForFetchRequest(fetchRequest, error: nil)
if count > 0 {
print("There are expenses for this month \(month.MonthYearDateFormatter()). Show expenses")
} else {
print("There are no expenses for this month \(month.MonthYearDateFormatter()). Do Nothing")
// here you can make some additional actions, like seeting the empty table with "no expenses this month"
}
}
Anyway, as #Paulw11 noted, if your data source is not size-exhausting, you could rather fetch the data from your data-model at viewDidLoad/viewDidAppear for example and then to render each month according to the currentMonth variable (regarding what month a user currently see).
So as a result, you would call setMonth() method only in the load of your controller and each time a user changes a current month view.
With the help of #Paulw11 and #pedrouan, I was able to do what I wanted.
Fetch all expenses
func fetchExpenses() {
let app = UIApplication.sharedApplication().delegate as! AppDelegate
let context = app.managedObjectContext //scratch pad
let fetchRequest = NSFetchRequest(entityName: "Expense")
//Always Sort Budget by the date it's created
let sortDescriptor = NSSortDescriptor(key: "created", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
do {
let results = try context.executeFetchRequest(fetchRequest)
self.expenses = results as! [Expense]
} catch let err as NSError {
print(err.debugDescription)
}
}
Set constraints to first and last month to determine how far a user can navigate through months in viewDidLoad
//If array is not empty, set first and last month
if expenses.count > 0 {
guard let first = expenses.first?.month else {
return
}
firstMonth = first
guard let last = expenses.last?.month else {
return
}
lastMonth = last
}else {
//Set current month to be first and last
firstMonth = currentMonth.MonthYearDateFormatter()
lastMonth = currentMonth.MonthYearDateFormatter()
}
Limit user from going past first and last month
#IBAction func backMonthButtonPressed(sender: AnyObject) {
print("Back Button Pressed")
if currentMonth.MonthYearDateFormatter() != firstMonth {
self.processMonth(step: -1)
}
}
#IBAction func nextMonthButtonPressed(sender: AnyObject) {
print("Next Button Pressed")
if currentMonth.MonthYearDateFormatter() != lastMonth {
self.processMonth(step: 1)
}
}
Return all the expenses in current month and if there's none clear fetch and return empty table.
func updateFetch() {
setFetchResults()
do {
try self.fetchedResultsController.performFetch()
} catch {
let error = error as NSError
print("\(error), \(error.userInfo)")
}
}
func setFetchResults() {
let app = UIApplication.sharedApplication().delegate as! AppDelegate
let context = app.managedObjectContext //scratch pad
//Fetch Request
let fetchRequest = NSFetchRequest(entityName: "Expense")
let sortDescriptor = NSSortDescriptor(key: "created", ascending: false)
fetchRequest.predicate = NSPredicate(format: "month == %#", currentMonth.MonthYearDateFormatter())
fetchRequest.sortDescriptors = [sortDescriptor]
let count = context.countForFetchRequest(fetchRequest, error: nil)
let controller = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: ad.managedObjectContext , sectionNameKeyPath: section, cacheName: nil)
//If there is none, empty fetch request
if count == 0 {
print("There are no expenses for this month. Show nothing")
controller.fetchRequest.predicate = NSPredicate(value: false)
} else {
print("There are expenses for this month. Show expenses")
}
fetchedResultsController = controller
controller.delegate = self
}
With pedroruan's processMonth(), I was able to navigate through empty tableViews so that user can see that August is empty while still being able to go to June's month. Thanks guys.

MVC - NSFetchRequest in model file without UIKit

In my ViewController, I have a function for fetching all the 'headingNumbers' (an attribute of the entity 'Vocabulary'). I understood that this is not good MVC practice.
So I created a new swift file.
I did not import UIKit. Youtube Stanford - Developing iOS 9 Apps with Swift - 2, 22:53: "Never import UI KIT in a model file because the model is UI independent"
(https://www.youtube.com/watch?v=j50mPzDMWVQ)
However, now I get these messages like
"Use of unresolved identifier 'UIApplication'".
Which makes sense, as I did not import the UIKit.
The question is: how do I now execute a fetch request in my new swift file.
(As you probably now by now, I am a beginner)
import Foundation
import CoreData
class QueryData {
private var selectedHeadingNumber:String = "-123456789"
private var setOfHeadingNumbers:[String] = [String]()
func getHeadingNumbers2() -> [String] {
if let managedObjectContext = (UIApplication.sharedApplication().delegate as? AppDelegate)?.managedObjectContext {
// Create Fetch Request
let fetchRequest = NSFetchRequest(entityName: "Vocabulary")
// Add Sort Descriptor
let sortDescriptor = NSSortDescriptor(key: "headingNumber", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
// Execute Fetch Request
do {
let result = try managedObjectContext.executeFetchRequest(fetchRequest)
for managedObject in result {
if let foundHeadingNumber = managedObject.valueForKey("headingNumber") {
if let result_number = foundHeadingNumber as? NSNumber
{
let result_string = "\(result_number)"
if !setOfHeadingNumbers.contains(result_string) {
print("Headingnumber: \(foundHeadingNumber) ")
setOfHeadingNumbers.append(result_string)
print("updated selectedHeadingNumber: ", selectedHeadingNumber)
selectedHeadingNumber = result_string
// set the default lessonnumber to the first lesson
if selectedHeadingNumber == "-123456789" {
selectedHeadingNumber = result_string
print("updated selectedHeadingNumber: ", selectedHeadingNumber)
}
}
}
//setOfHeadingNumbers.append(first)
}
}
} catch {
let fetchError = error as NSError
print(fetchError)
}
} // end of if statement
return setOfHeadingNumbers
} // end of func
} // end of class
What you actually need is NSManagedObjectContex. Pass it as an argument to method:
func getHeadingNumbers2(inContext context: NSManagedObjectContext) -> [String]
{
...
}
Or inject it as dependency in initializer
class QueryData
{
let context: NSManagedObjectContext
init(context: NSManagedObjectContext)
{
self.context = context
}
func getHeadingNumbers2() -> [String]
{
let fetchRequest = NSFetchRequest(entityName: "Vocabulary")
let result = try self.context.executeFetchRequest(fetchRequest)
...
}
}

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.

Resources