Delete items from tableview row and core data simultaneously - ios

I'm trying to delete items from a TableView and an entity called "Books." I have no idea if I'm remotely on the right track, however. When I try this code:
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
var appdel:AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
var context:NSManagedObjectContext = appdel.managedObjectContext!
var request = NSFetchRequest(entityName: "Books")
if editingStyle == UITableViewCellEditingStyle.Delete {
addBook.myBooks.removeAtIndex(indexPath.row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
context.deleteObject(addBook.myBooks[indexPath.row] as! NSManagedObject)
}
}
I get a warning on the last line that says "Cast from 'String' to unrelated type 'NSManagedObject' always fails." Does anyone know how I can get around this? I've read that you can use a fetchedResultsController to handle core data in tables easily but I am new to programming and found that method a bit more confusing when setting up core data in general. Is the fetchedResultsController necessary to manage the data in my TableView?

From your error it sounds like addBook.myBooks is an array of strings.
The immediate problem is that deleteObject doesn't work on strings, it works on managed objects-- that is, instances of NSManagedObject or a subclass of NSManagedObject. You can't delete a string from Core Data like that, you have to delete the managed object that corresponds to the string. The error is specifically telling you that as! NSManagedObject doesn't work on a string, because a string is a completely different kind of thing from a managed object.
[It's also a problem that you're removing the string at indexPath.row via removeAtIndex, and then later trying to use the string at indexPath.row that you just removed, but that's not the real problem here.]
What you need to do is find out the managed object that corresponds to the table view row you're deleting, and pass that to deleteObject. Without a fuller picture of how your view controller works it's impossible to say exactly how you would do that, but there are a couple of things that are clear:
Those first three lines in your method are not doing anything useful. Cut them-- even if you made them work, they'd be the wrong approach here. You don't want to have to fetch the managed object you're deleting right here. By the time you reach this method you should already know enough to delete it.
It's not necessary to use NSFetchedResultsController to put Core Data together with table views. But if you're new to programming you'll probably find things a lot easier if you use it.

Try to save your context after deleteObject
context.deleteObject(addBook.myBooks![indexPath.row])
addBook.myBooks.removeAtIndex(indexPath.row)
do {
try context.save()
}
catch {
print("Error")
}
self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Automatic)
}

Related

Closure does not run in the main thread in swift

I want to show some images on UITableViewCell. However I got an error below
fatal error: Index out of range. The problem is that closure does not run in the main thread probably. How can I solve this issue?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PickupTableViewCell", for: indexPath) as! PickupTableViewCell
APIManager.getAnotherArticle{ (articles: Array<Article>?) in
for info in articles! {
self.authorArray.append(info.author)
self.descriptionArray.append(info.description)
if info.publishedAt != nil {
self.publishedAtArray.append(info.publishedAt)
}
self.titleArray.append(info.title)
self.urlArray.append(info.url)
self.urlToImageArray.append(info.urlToImage)
print(self.authorArray)
}
}
let program = urlToImageArray[indexPath.row] //index out of range
let urlToImage = NSURL(string: program)
cell.pickupImageView.sd_setImage(with: urlToImage as URL!)
return cell
}
Wrap anything you want to run on the main queue in DispatchQueue.main.async{ ... }.
That said, your current approach likely won't work. This method gets called a lot. While the user is scrolling, this method gets called every time a cell is about to come on the screen (in iOS 10, sometimes a bit before it'll come on the screen). Cells are often recycled, and you're appending data to the titleArray and other arrays every time a cell is requested (they may not be in order; they might have already been fetched; this array isn't going to wind up in the right order).
You need to move all your data about a cell into a model object and out of the view controller. There shouldn't be a titleArray and an urlArray, etc. There should just be an Article, and the Article should take care of fetching itself and updating its properties. And the job of this method is to fetch the correct Article from your cache, or create a new one if needed, and assign it to an ArticleCell. The ArticleCell should watch the Article and update itself any time the Article changes (i.e. when the fetch completes). Almost no work should happen directly in this method since it gets called so often, and in possibly random orders.
The common way to build this kind of thing is with a simple model object (often a reference type so it can be observed; there are many other approaches that allow a struct, but they're a little more advanced so we'll keep this simple):
class Article {
var author: String
var description: String
var publishedAt: Date
var title: String
var url: URL
var image: UIImage
func refresh() {
// fetch data from server and replace all the placeholder data
}
}
Then there's some kind of Model that vends these:
class Model {
func article(at index: Int) -> Article {
if let article = lookupArticleInCache(at: index) {
return article
}
let article = createAndCachePlaceholderArticle(at: index)
article.refresh()
}
}
And then your code looks like:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PickupTableViewCell", for: indexPath) as! PickupTableViewCell
cell.article = sharedModel.article(at: indexPath.row)
return cell
}
You can use KVO or Swift Observables or an ArticleDelegate protocol to let the cell observe the Article. When the Article updates, the cell updates itself.
Again, there are many ways to approach this. You could have a "PlaceHolderArticle" that all the cells share and when the real Article comes in, the cell replaces the whole thing (so that Articles are immutable rather than self-updating). You could use the more generic approaches described by Swift Talk. There are lots of ways. But the key is that there is this model that updates itself, independent of any particular UI, and a UI (views, view controllers) that watch the model and display what it holds.
If you want much, much more on this topic, search for "Massive View Controller." That's the common name for the anti-pattern you're currently using. There are lots of ways to fight that problem, so don't assume that any particular article you read on it is "the right way" (people have come up with some very elaborate, and over-elaborate, solutions). But all of them are based on separating the model from the UI.
APIManager.getAnotherArticle{ (articles: Array<Article>?) in
for info in articles! {
self.authorArray.append(info.author)
self.descriptionArray.append(info.description)
if info.publishedAt != nil {
self.publishedAtArray.append(info.publishedAt)
}
self.titleArray.append(info.title)
self.urlArray.append(info.url)
self.urlToImageArray.append(info.urlToImage)
print(self.authorArray)
}
}
you have to make separate function for this calculation and try to avoid the any calculate functionality in "cellForRowAt"

managedContext.insertObject extra argument in call `atIndex` in Swift?

So here is my code: (I want to re-order the table and update Core Data):
override func tableView(tableView: UITableView, moveRowAtIndexPath fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) {
let entity = NSEntityDescription.entityForName(entity, inManagedObjectContext: managedContext)
let entityObject = NSManagedObject(entity: entity!, insertIntoManagedObjectContext: managedContext)
entityObject.setValue(content, forKey: key)
self.managedContext.insertObject(entityObject, atIndex: 0) //ERROR HERE
do {
try managedContext.save()
} catch let error as NSError {
}
}
I've seen similar code here but why mine isn't working? Thanks!
The link you provided is not doing the same thing you did here. What he did there was removing and inserting object in to a array of Playlist.
If you really want to re-order the table and update Core Data accordingly, you may want to add a index field to your Core Data model, and update it with the index of cell every time the cell is moved.
So you can populate the data to table view in order of the index filed, and keep cell order synchronized with data model.
First of all a side note: The objects in NSManagedObjectContext are unordered so there is no method to insert an object at an particular index.
Since the object is inserted already two lines above in the method NSManagedObject(entity:insertIntoManagedObjectContext:), delete the line which causes the error.
Agreed with Vadian, you do not need to worry about ordering the Managed Object Context.
To update the UI you should run -
"tableView.reloadData"
in addition to above code.

Parse.com query not working properly

I am trying to filter the posts based on their profile. For instance, when I go to my profile I only want to see my posts, not all the posts in my database. I attempted to make a filter for that but the code below does not seem to work and I am unsure as to why that is. It may be something obvious but I can not seem to pinpoint the issue, any ideas?
I have attached a picture of the database to further assist anybody.
The code runs perfectly fine it just does not filter the usernames.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var user = PFUser.currentUser()?.username!
let bucketCellObj = tableView.dequeueReusableCellWithIdentifier("bucket", forIndexPath: indexPath) as! BucketTableViewCell
var query = PFQuery(className: "Bucket")
query.whereKey("creator", equalTo: user!)
query.findObjectsInBackgroundWithBlock { (PFObject, error) -> Void in
if error == nil {
bucketCellObj.bucketname.numberOfLines = 0
bucketCellObj.username.text = self.bucketsArray[indexPath.row]["creator"] as? String
bucketCellObj.bucketname.text = self.bucketsArray[indexPath.row]["name"] as? String
bucketCellObj.selectionStyle = .None
} else {
print("ERROR")
}
}
return bucketCellObj
}
What you are doing here might work under some circumstances but will certainly bite you at some point.
What your code does:
show some arbitrary number of cells - probably based on self.bucketsArray.count or something similar
in each cell creation, run a parse query
when the query returns, customize the already displayed cell accordingly - without any usage of the requested query response
That will cause problems for a couple of reasons:
you perform too many requests, each cell requests some data, each new displayed cell requests its own data again
you display the old data in the cell until the new data is fetched which could take a few seconds due the amount of requests
you could encouter a problem where you requests some data for a cell, that cell moves off-screen, gets reused, then the first query returns, still holds the reference to it and will therefore display wrong data
How it can be solved
Do not requests the data in the cellForRowAtIndexPath.
Request the data once in viewDidLoad or similar. as soon as the data gets returned, parse it and initiate a tableView.reload().
In the cellForRowAtIndexPath make use of the already retrieved data, do not perform anymore async tasks.

Why is my Swift for-in-loop not executing/iterating?

I'm trying to delete an array of NSManagedObjects associated with a CoreData model. I'm creating a new instance of the class my property is apart of, and attempting to delete each item within. Any clues on what I'm doing wrong?
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
// THIS FOR IN LOOP WILL NOT EXECUTE...
for x in DrilledDownCategoryViewController().categoryItemsItems {
println("^^^")
managedObjectContext?.deleteObject(x as NSManagedObject)
}
// EVERYTHING ELSE WILL THOUGH...
println("asdfjkl;")
let logItemToDelete = categoryItems[indexPath.row]
managedObjectContext?.deleteObject(logItemToDelete)
self.fetchCategory()
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
save()
}
}
DrilledDownCategoryViewController is a class.
DrilledDownCategoryViewController() is its contructor.
With DrilledDownCategoryViewController() you create a new instance of your view controller rather than accessing the "running" one.
Naturally a new Object's related array (or what ever) would be either nil or what it was set to in its constructor.
If you are used to Objective-C, this is what you did:
[[[DrilledDownCategoryViewController alloc] init| categoryItemsItems]
An Obj-C forin loop woult not iterate a single time too.
The for-in-loop will not execute an empty array.
Just do this so you don't run into an empty fetch results array from another class: If you're trying to access a fetched results array of CoreData items; don't depend on accessing an array from another class. Just fetch particular predicate filtered items and inject them into a new array. You'll be able to alter or delete them from there...

Swift editing related Core Data objects with NSFetchedResultsController across views / how do I format and use NSPredicate with a block?

I have a question regarding editing/deleting Core Data objects in a UIListView that are related to another object.
To simplify the problem, let's say I have a list view of Family objects. Tapping on a Family name opens a list view of FamilyMember objects for the chosen Family object. I have a one to many relationship between Family and FamilyMember set up in Core Data.
I perform an NSFetchRequest to grab the list of all 'Family' objects in my initial table view controller and put them into an NSFetchedResultsController. When a family in the list is tapped, that 'Family' object is passed into the FamilyMembersViewController with prepareForSegue.
The issue I'm facing is that since I passed in a 'Family' object directly from the initial view controller to FamilyMembersViewController, I never had to create a FetchRequest or a NSFetchedResultsController in FamilyMembersViewController. Therefore, some editing functions that worked great on the initial view now don't work in the FamilyMembersViewController.
How do I take advantage of NSFetchedResultsController functions (such as the following) in a view that doesn't have a NSFetchedResultsController? Do I need to re-query Core Data, limiting the results to a specific 'Family" object? It seems like a waste since I already have the object available in my view.
Am I approaching this functionality incorrectly? I guess I'm just hoping to have the functions be similar from one view controller to the next.
I've tried making a new array called 'familyMembers' and a corresponding NSFetchedResultsController within my FamilyMembersViewController and predicated it with this:
var predicate = NSPredicate(format: "family == %#", "\(self.family)")
where self.family is the 'Family' object passed from the initialView. But in this case, for some reason the "self.family" part just returns an empty string, when really it should return the Family object. Even if it did return the Family object, it doesn't seem like passing that into the predicate would help anything, since the predicate seems to match only strings. I can't quite figure out how to format a Predicate with a block statement.
Any help would be much appreciated! Thank you for reading.
func controllerWillChangeContent(controller: NSFetchedResultsController!) {
tableView.beginUpdates()
}
func controller(controller: NSFetchedResultsController!, didChangeObject anObject: AnyObject!, atIndexPath indexPath: NSIndexPath!, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath!) {
switch type {
case .Insert:
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
case .Delete:
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
case .Update:
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
default:
tableView.reloadData()
}
families = controller.fetchedObjects as [Family]
}
func controllerDidChangeContent(controller: NSFetchedResultsController!) {
tableView.endUpdates()
}
You are on the right track, though predicate arguments don't have to be strings - you can pass the family object itself:
var predicate = NSPredicate(format: "family == %#", self.family)

Resources