Using 'didMoveCellFromIndexPathToIndexPathBlock' with Core Data - ios

I have a custom UITableView that takes care of animations for rearranging cells and works perfectly with standard test array which doesn't use core data.
When trying with an application that uses core data with two entities 'Folder' and 'Item' with a To Many relationships I receive an error.
[(Item)] does not have a member called exchangeObjectAtIndex
for;
tableView.didMoveCellFromIndexPathToIndexPathBlock = {(fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) -> Void in
let delegate:AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let context: NSManagedObjectContext = delegate.managedObjectContext!
self.itemsArray.exchangeObjectAtIndex(toIndexPath.row, withObjectAtIndex: fromIndexPath.row)
var error: NSError? = nil
if !context.save(&error) { abort() }
}
This is because:
The NSSet, NSMutableSet, and NSCountedSet classes declare the
programmatic interface to an unordered collection of objects.
So I tried converting the NSSet to an NSMutableArray to manage the objects order.
func itemsMutableArray() -> NSMutableArray {
return NSMutableArray(array: (item.allObjects as! [Item]).sorted{ $0.date.compare($1.date) == NSComparisonResult.OrderedAscending } )
}
But then I get the following error in my tableview; this was because the mutable array is an AnyObject so swift doesn't believe it has a title property.
cell.textLabel?.text = folderMutableArray[indexPath.row].title
So then I go back to where I started. I am just trying to create a simple list and rearrange the order of objects.
Here is my Folder Class:
class Folder: NSManagedObject {
#NSManaged var date: NSDate
#NSManaged var title: String
#NSManaged var item: NSSet
/// Method 1 - Converting to a NSMutableArray
// func itemsMutableArray() -> NSMutableArray {
// return NSMutableArray(array: (item.allObjects as! [Item]).sorted{ $0.date.compare($1.date) == NSComparisonResult.OrderedAscending } )
// }
// Method 2 - creating an array of type Item
func itemArray() -> [Item] {
let sortByDate = NSSortDescriptor(key: "Date", ascending: true)
return item.sortedArrayUsingDescriptors([sortByDate]) as! [Item]
}
}
Productivity apps do this all the time so I know it's possible but unsure how, Does anyone know where I am going wrong or have any ideas or suggestions ?

This is very easy to achieve. You need to add a property called index to your Item. Set it to an integer type and whenever you add a new Item set this value to the index under which you want an item to appear. This property should be added both in Core Data Model and in your Item's class as NSManaged property.
Next you need to add a transient property to your Folder class called arrayOfItems (you can rename this of course to whatever you want). In Core Data Model set it to Transient and Transformable.
In your Folder class do the following:
class Folder: NSManagedObject {
#NSManaged var date: NSDate
#NSManaged var title: String
#NSManaged var item: NSSet
#NSManaged var arrayOfItems: [Items]
override func awakeFromFetch() {
super.awakeFromFetch()
self.regenerateItems()
}
func regenerateItems() {
let desc = NSSortDescriptor(key: "index", ascending: true)
if let array = item.sortedArrayUsingDescriptors([desc]) as? [Item] {
self.arrayOfItems = array
}
}
}
Now whenever you fetch any instance of Folder you will get a correct sorted and mutable array of Items. There are only two other cases you need to consider.
And they result from the fact that awakeFromFetch is only getting called when you fetch your data from Core Data. So, you have to consider other scenarios.
Adding new Item
When you add new Item you need to either manually append the new Item to arrayOfItems or you need to call regenerateItems() once you are finished adding the new Item. For example, lets assume that somewhere in your code you create your initial data:
var folder = NSEntityDescription.insertNewObjectForEntityForName("Folder", inManagedObjectContext: self.managedObjectContext!) as! Folder
var firstItem = NSEntityDescription.insertNewObjectForEntityForName("Item", inManagedObjectContext: self.managedObjectContext!) as! Item
// assuming that you have an inverse relationship from Item to Folder
// line below will assign it and will add your firstItem to Folder's
// NSSet of items
firstItem.folder = folder
// at this point your firstItem is added to your Folder but
// arrayOfItems is empty. So, you should call
folder.arrayOfItems = [firstItem]
// or you can call folder.regenerateItems()
Code above refers to the situation when you create your initial data. If somewhere in your code you add a new Item to the folder which already has some Items you have the following:
var newItem = NSEntityDescription.insertNewObjectForEntityForName("Item", inManagedObjectContext: self.managedObjectContext!) as! Item
// assuming that you have an inverse relationship from Item to Folder
// line below will assign it and will add your newItem to Folder's
// NSSet of items
newItem.folder = someExistingFolder
// at this point your newItem is added to your Folder but
// arrayOfItems is not yep updated and does not include newItem
// so you should either call
folder.arrayOfItems.append(newItem)
// or you can call folder.regenerateItems()
Rearranging Items
Also, when you move Items in your table view you will need to change their index and order in the arrayOfItems. The easiest way to achieve this would probably be to change the order of items in the arrayOfItems and then to iterate through this array assigning correct new indexes to all items within it:
func tableView(tableView: UITableView, moveRowAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath) {
let itemThatMoved = currentFolder.arrayOfItems[sourceIndexPath.row]
currentFolder.arrayOfItems.removeAtIndex(sourceIndexPath.row)
currentFolder.arrayOfItems.insert(itemThatMoved, atIndex:destinationIndexPath.row )
var currentIndex = 0
for item in currentFolder.arrayOfItems {
item.index=currentIndex
currentIndex++
}
}
Let me know if this helps or if you have any other questions.
P.S. I strongly encourage you to change the name of your relationship from item to items. Plural name items is much more logical for NSSet.

Related

How to declare a variable in swift whose datatype is an array of entities without knowing which entity?

I am building an iOS app with Xcode and using CoreData. In the data model there are a few entities, let's say: A, B, C, D, E.
In the homeViewController there are five buttons and each button perform the segue to the detailTableViewController for each entity.
Depending on which button is pressed, you should fetch the information for the corresponding entity. For example, if I press button "B" I should get the Data for the "B" entity in the detailTableViewController.
And here comes the question: how can I declare the variable "entitiesArray" to store the fetch request result if I don't know which entity is going to be pushed until the button is pushed? I have no idea of its data type until the button is pushed.
If there were only one entity "A" I would write:
let entitiesArray = [A]()
let request: NSFetchRequest<A> = A.fetchRequest()
entitiesArray = try context.fetch(request)
...
However, I don't know the entity that will be pushed.
And using a switch statement in viewDidLoad doesn't solve the problem because I need the entitiesArray to be a global variable to use it inside other functions like numberOfRowsInSection and cellForRowAt indexPath.
Add this to your context via extension:
func fetchMOs (_ entityName: String, sortBy: [NSSortDescriptor]? = nil, predicate: NSPredicate? = nil) throws -> [NSManagedObject] {
let request = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
request.returnsObjectsAsFaults = false //as I need to access value
request.predicate = predicate
request.sortDescriptors = sortBy
return try! self.fetch(request) as! [NSManagedObject]
}
then you simply call it like this:
let mos = context.fetchMOs(String(describing: yourClassofAorBorCorD))
The point is using NSFetchRequest's convenience init(:entityName) and struct NSFetchRequestResultType as result type.
An alternative to using the superclass NSManagedObject could be to create a protocol and have your array declared to contain objects of that protocol. This could make sense if want to access data the same way for all entities in your UI like getting a name, identifier etc and preferably if it is immutable
Here is a quick example using the built-in protocol CustomStringConvertible
let entitesArray = [CustomStringConvertible]()
implement the protocol in an extension
extension A: CustomStringConvertible {
var description: String {
return "\(someAttribute), \(someOtherAttribute)"
}
}
You can have a simple protocol then with title and url and let all entities implement this protocol, you do need to have different names in the protocol do for the attributes so that they do not clash with the atributes in the entities. An example
protocol LabelSupport {
var titleLabel: String { get }
var urlLabel: String { get }
}
And let A implement it
extension A: LabelSupport {
var titleLabel: String {
return title
}
var urlLabel: String {
return url
// or perhaps url.path or similar
}
}

Fetching and displaying data from core data

Aim :
To be able to display the days selected and the time picked by the user in the same row of the table view. The time should appear at the top and the days selected should appear at the bottom, both in the same row, just like an alarm clock.
Work :
This is the relationship I've got setup :
and this is how I save the days that are selected from a UITable and the time from a UIDatepicker when the save button is tapped :
#IBAction func saveButnTapped(_ sender: AnyObject)
{
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext //creates an object of a property in AppDelegate.swift so we can access it
let bob = Bob(context: context)
//save the time from UIDatePicker to core data
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm"
bob.timeToPing = dateFormatter.string(from: timePicked.date)
// save the days selected to core data
for weekday in filteredWeekdays
{
var day = Days(context: context) //create new Days object
day.daysSelected = weekday as NSObject? //append selected weekday
bob.addToTimeAndDaysLink(day) //for every loop add day object to bob object
}
//Save the data to core data
(UIApplication.shared.delegate as! AppDelegate).saveContext()
//after saving data, show the first view controller
navigationController!.popViewController(animated: true)
}
Now that the data is once saved, I get the data :
func getData()
{
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
do
{
bobs = try context.fetch(Bob.fetchRequest())
}
catch
{
print("Fetching failed")
}
}
Attempt to get the days selected :
I tried to follow this, the below comments and a formerly deleted answer to this question to do this :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = UITableViewCell()
let bob = bobs[indexPath.row]
cell.textLabel?.text = bob.timeToPing?.description
// retrieve the days that are selected
var daysArray: [Days] = []
daysArray = bob.timeAndDaysLink?.allObjects as! [Days]
for days in daysArray
{
print (days.daysSelected?.description)
cell.textLabel?.text = days.daysSelected! as! String
}
return cell
}
EDIT :
print(daysArray) gives this :
[<Days: 0x6080000a5880> (entity: Days; id: 0xd000000000040000 <x-coredata://30B28771-0569-41D3-8BFB-D2E07A261BF4/Days/p1> ; data: <fault>)]
print(daysArray[0]) gives this :
<Days: 0x6080000a5880> (entity: Days; id: 0xd000000000040000 <x-coredata://30B28771-0569-41D3-8BFB-D2E07A261BF4/Days/p1> ; data: <fault>)
How to save days
let weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
var filteredWeekdays: [String] = []
#NSManaged public var daysSelectedbyUser: NSSet
And then
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
selectedWeekdays()
}
override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath)
{
selectedWeekdays()
}
func selectedWeekdays()
{
if let selectedRows = tableView.indexPathsForSelectedRows
{
let rows = selectedRows.filter {$0.section == 0}.map{ $0.row}
filteredWeekdays = rows.map{ weekdays[$0] }
print(filteredWeekdays)
}
}
Many thanks!
OK based on your latest comment that the crash occur on this line:
cell.textLabel?.text = days.value(forKey: "daySelected") as! String
It's clearly pointing to the typo you've made in key name. You have: daySelected and should be (based on your core data model) daysSelected, but nevertheless it's not very good approach to use values for your core data entity and also force type like that. To make it better I suggest replacing this line with:
cell.textLabel?.text = days.daysSelected!
This should be already a String since this is a String in CoreData. In case it's optional (should be an optional), you shouldn't force it. I will assume that whenever data will be not there you will just display empty cell, so even better it will be:
cell.textLabel?.text = days.daysSelected ?? ""
This will produce empty string to text, whenever (for some reason) data will be not there.
EDIT
So for additional piece of code you put in your question:
In your CoreData field daysSelected is type of String?, right?
Then you assign timeAndDateLink to NSSet<String>, right? But expected value here should be NSSet<Days>.
So let's edit your input code a bit ( i will put comment on every line):
let bob = Bob(context: context) /* create new Bob object */
for weekday in filteredWeekdays {
var day = Days(context: context) /* create new Days object */
day.daysSelected = weekday /* append selected weekday */
bob.addToTimeAndDaysLink(day) /* for every loop add day object to bob object */
}
I hope everything is clear in above example. You may have a problem with a compiler in that case, because if you choose generate class for entities you will endup with two func with the same name but different parameter (in Swift this should be two different functions, but Xcode sometimes pointing to the wrong one). If you hit that problem try:
let bob = Bob(context: context) /* create new Bob object */
var output: NSMutableSet<Days> = NSMutableSet()
for weekday in filteredWeekdays {
var day = Days(context: context) /* create new Days object */
day.daysSelected = weekday /* append selected weekday */
output.add(day)
}
bob.addToTimeAndDaysLink(output) /* this will assign output set to bob object */
You should also rename your Days entity to Day to avoid future confusion that we have right now, days as array will only be in relation from other entities to this not entity itself.
I don't know why no one uses FetchedResultsController, which is made for fetching NSManagedObjects into tableView, but it doesn't matter I guess...
Problem in this question is that you didn't post here your NSManagedObject class for the variable, so I cannot see which type you set there (Should be Transformable in CoreData model and [String] in NSManagedObject class...)
Ignoring all force unwraps and force casting and that mess (which you should pretty damn well fix as first, then it won't crash at least but just don't display any data...)
Days selected by user is NSSet, which it sure shouldn't be.
Please provide you NSManagedObjectClass in here so I can edit this answer and solve your problem...

CoreData One-To-Many, sorting the many

I have a small app that I'm working on to learn CoreData. These are the entities for CoreData.
extension Person {
#NSManaged var firstName: String?
#NSManaged var lastName: String?
#NSManaged var age: NSNumber?
#NSManaged var personToBook: NSSet?
}
extension Books {
#NSManaged var bookName: String?
#NSManaged var bookISBN: String?
#NSManaged var bookToPerson: Person?
}
The app is a list of people. Belonging to each person in the list can be one more more books.
I am easily able to sort the people.
let fetchRequest = NSFetchRequest(entityName: "Person")
let fetchSort = NSSortDescriptor(key: "lastName", ascending: true)
fetchRequest.sortDescriptors = [fetchSort]
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
}
catch let error as NSError {
print("Unable to perform fetch: \(error.localizedDescription)")
}
What I am having a tough time working out is how to sort the Books in alphabetical order, like I am the people. When I tap on a person in the people list, I segue to a UITableView that contains a list of books. In the prepareForSegue, I pass in the selected person, the context, and the NSFetchedResultsController that I am using in the person list.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let vc = segue.destinationViewController as! BooksController
vc.person = fetchedResultsController.objectAtIndexPath(selectedIndex) as! Person
vc.context = self.context
vc.fetchedResultsController = self.fetchedResultsController
}
The UITableView is populated by person.personToBook.
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return (person.personToBook?.count)!
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("BooksCell", forIndexPath: indexPath)
let book = person.personToBook?.allObjects[indexPath.row] as! Books
cell.textLabel?.text = book.bookName!
return cell
}
Books are added to the correct person in this manner:
let entity = NSEntityDescription.entityForName("Books", inManagedObjectContext: self.context)
let bookInstance = Books(entity:entity!, insertIntoManagedObjectContext: self.context)
bookInstance.bookName = alertController.textFields?[0].text
bookInstance.bookISBN = alertController.textFields?[1].text
self.person.mutableSetValueForKey("personToBook").addObject(bookInstance)
do {
try self.context.save()
self.tableView.reloadData()
}
catch let error as NSError {
print("Error saving a book \(error.localizedDescription)")
}
personToBook is an NSSet, and this is not sorted. I thought about using another NSFetchedResultsController to pull the list of books out, but there is nothing unique that is identifying the books as belonging to a specific user. For instance, I could have two people with the same name of John Doe. Because of this, I'd have no way to set theNSPredicate` so that it would only pull in books for one of the John Does.
One though that I had is that I could add some sort of UniqueID to the Person entity, like I'd do in SQL. This would ensure that I could get the Books records for only the correct person. I've been told that I should not do this in CoreData. That leads me to believe that there must be some way to sort the items in the to-many part of this.
What are the ways that I can sort my Books data?
An NSSet is unordered. The set won't return the books in the same order that you added them.
Here are three different approaches you could use for the books to be in alphabetical order.
Change the model's to-many personToBook to an ordered relationship. This will use an NSOrderedSet instead of an NSSet. Add the books to the ordered set in alphabetical order. (Note one disadvantage of this approach is that iCloud Core Data synchronisation does not support ordered relationships.)
Convert the NSSet of books to an array and sort that array.
person.personToBook?.allObjects.sort({ $0.bookName < $1.bookName })
Use a Book fetchedResultsController to fetch and order a person's books. Add a predicate specifying the person entity, so only the books for that person are returned. Since you're comparing against an entity, not a name, it wouldn't matter if there is more than one author with that name.
let predicate = NSPredicate(format: "%K == %#", "bookToPerson", person)
As an aside, if an entity's attributes or relationships are not optional, those #NSManaged properties wouldn't need to be optional. This would save you from having to unwrap values that would never be nil. You can also specify the type of object in the set, so Swift knows that it's dealing with a Book, instead of an AnyObject.
#NSManaged var personToBook: Set<Book> // Convention would be Book, not Books
You also can avoid passing the context or fetchedResultsController (of people). You've passed a Person managed object which has a managedObjectContext property, and you're directly accessing the person's to-many Book relationship to get their books. The Book view controller has no need to know about any other people that a master view controller happened to fetch.

Error: Could not find overload for 'title' that accepts the supplied arguments

Currently I have a subclass of NSManaged object called Folder with property called item that is of type NSSet. I have a function to return a NSMutableArray and I am trying to display the data in a tableview (and then rearrange the order displayed in the table).
class Folder: NSManagedObject {
#NSManaged var title: String
#NSManaged var date: NSDate
#NSManaged var item: NSSet
func itemMutableArray() -> NSMutableArray {
return NSMutableArray(array: (item.allObjects as! [Checklist]).sorted{ $0.date.compare($1.date) == NSComparisonResult.OrderedAscending } )
}
TableViewController:
var itemMutableArray: NSMutableArray!
itemMutableArray = folder.itemMutableArray()
cell.textLabel?.text = itemMutableArray[indexPath.row].title //Error
Unfortunately this returns an error in my tableview when using this function.
Error could not find overload for 'title' that accepts the supplied arguments
Ultimately what I am trying to achieve is to display the data and move the cells around to change the order of the NSSet.
table.didMoveCellFromIndexPathToIndexPathBlock = {(fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) -> Void in
let delegate:AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let context: NSManagedObjectContext = delegate.managedObjectContext!
context.deleteObject(self.itemArray[indexPath!.row] as NSManagedObject )
self.itemArray.exchangeObjectAtIndex(toIndexPath.row, withObjectAtIndex: fromIndexPath.row)
}
PS: Originally I had a function to return an array of the object but I was unable to modify the order of the NSSet as they are not ordered.
func itemArray() -> [Item] {
let sortDescriptor = NSSortDescriptor(key: "date", ascending: true)
return item.sortedArrayUsingDescriptors([sortDescriptor]) as! [Item]
}
Does anybody have any suggestions with where I am currently going wrong ?
The problem with the expression itemMutableArray[indexPath.row].title is that, because you have typed itemMutableArray as an NSMutableArray, itemMutableArray[indexPath.row] is an AnyObject. Thus, Swift has no reason to believe that this thing has a title property. You need to cast it to something that does have a title property (though it would be much better to avoid the use of NSMutableArray completely, if possible, since a Swift array is mutable).

Adding to Managed Object Ordered Set

I have a test app with two entities, folder with a relationship of To Many with another object called List.
In my storyboard I have a tableview controller with the list of created folders. When tapping on a folder I segue to another TableView passing on the selectedFolder which should display the List ordered set saved to the selectedFolder. I have a modal that appears to add a item to the List.
Unfortunately I have not been able to save a List to the selectedFolder ordered set. I receive an error when executing the save function unrecognized selector sent to instance this error is because of the following line:
selectedFolder.list = list.copy() as! NSOrderedSet
I am not sure what I am doing wrong with the save function and was wondering if anyone could help, it would be much appreciated.
Folder Subclass:
class Folder: NSManagedObject {
#NSManaged var title: String
#NSManaged var details: String
#NSManaged var date: NSDate
#NSManaged var list: NSOrderedSet
}
List Subclass
class List: NSManagedObject {
#NSManaged var item: String
#NSManaged var folder: Event
}
Modal View to add List to selected Folder ordered set.
class PopoverViewController: UIViewController {
//selectedFolder passed from segue. Works fine displays title of folder
var selectedFolder: Folder!
#IBOutlet weak var popoverTextField: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
self.popoverTextField.becomeFirstResponder()
// Do any additional setup after loading the view.
}
#IBAction func addListItem(sender: AnyObject) {
//Get the context
let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
//get entity details
let entity = NSEntityDescription.entityForName("List", inManagedObjectContext: moc!)
//Create the managed object to be inserted
let list = List(entity: entity!, insertIntoManagedObjectContext: moc!)
// Add Text
list.item = popoverTextField.text
//Insert the new checklist into the folder set
var folder = selectedFolder.list.mutableCopy() as! NSMutableOrderedSet
folder.addObject(list)
selectedFolder.list = list.copy() as! NSOrderedSet
//Error check & Save
var error: NSError?
if moc!.save(&error){
println("Could not save: \(error)")
}
}
var folder = selectedFolder.list.mutableCopy() as! NSMutableOrderedSet
folder.addObject(list)
selectedFolder.list = list.copy() as! NSOrderedSet
The last assignment makes no sense because list is a List object and not an (ordered) set, and btw. this computes a folder variable
which is then ignored.
What you probably meant (and this should work) is
var folder = selectedFolder.list.mutableCopy() as! NSMutableOrderedSet
folder.addObject(list)
selectedFolder.list = folder
As already mentioned in a comment, adding to an ordered relationship
is a bit tricky, but can be done with Key-Value coding as
let mutableChecklist = selectedFolder.mutableOrderedSetValueForKey("list")
mutableChecklist.addObject(list)
(Compare Exception thrown in NSOrderedSet generated accessors and Setting an NSManagedObject relationship in Swift.)
In the case of a one-to-many relationship however, the easiest way
is to set the other direction of the relationship:
list.folder = selectedFolder

Resources