I need some help! I can't seem to delete just a single row from Parse. I have the "Swipe to delete" figured out, however, when you try and delete something form the table, it doesn't do anything. I get no errors. Nothing gets deleted. This is my code.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// delete from server
let myFeatherquery = PFQuery(className: "FeatherPosts")
myFeatherquery.whereKey("message", equalTo: (PFUser.current()?.objectId!)!)
myFeatherquery.findObjectsInBackground(block: { (objects, error) in
if error != nil {
print("THERE WAS AN ERROR")
}else{
for object in objects!{
self.messages.remove(at: indexPath.row)
object.deleteInBackground()
self.tableView.reloadData()
}
}
})
}
}
In short, I want to delete a post from the tableView and delete it on the parse side as well. If I change:
"myFeatherquery.whereKey("message", equalTo: (PFUser.current()?.objectId!)!)"
to
"myFeatherquery.whereKey("userid", equalTo: (PFUser.current()?.objectId!)!)"
it just deleted everything that the user has ever posted. Please help!
You dont need to query inside UITableViewCellEditingStyle becasue IndexPath is already established which one you want to delete.
Now I've added a few extra bit to this logic.
1:) You can slide the cell to view the delete button. Once clicked it will confirm if you want to delete.
2:) Once deleted a fade will take place. It will then refresh the tableView and delete the object in parse in the background.
FeatherPostsArray that you see I made is your array of objects you used in the tableView. In your numberOfRowsInSection you would have done a count on it.
So this is what it should be:
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
var recClass = PFObject(className:"FeatherPosts")
recClass = self.FeatherPostsArray[(indexPath as NSIndexPath).row]
let deleteAction = UITableViewRowAction(style: .destructive, title: "Delete") { (action, indexPath) in
let alert = UIAlertController(title: "App Name",
message: "You sure you want to delete?",
preferredStyle: .alert)
let delete = UIAlertAction(title: "Delete", style: .default, handler: { (action) -> Void in
recClass.deleteInBackground {(success, error) -> Void in
if error != nil {
}}
self.FeatherPostsArray.remove(at: (indexPath as NSIndexPath).row)
tableView.deleteRows(at: [indexPath], with: .fade)
tableView.reloadData()
})
let cancel = UIAlertAction(title: "Cancel", style: .destructive, handler: { (action) -> Void in })
alert.addAction(delete)
alert.addAction(cancel)
self.present(alert, animated: true, completion: nil)
}
//This is nice if you want to add a edit button later
return [ deleteAction]
}
Let me know if you get stuck.
Related
When a cell is deleted, the item at the end of the list takes the place of the item that just got deleted. This only happens when there are more than 3 items in the list.
In the gif below I delete numbers 3 and 4 which leaves me with numbers 1,2,5 in the simulator. HOWEVER in the Realm file I have numbers 1,2,4. I have no clue why it does this?
Data Model
import Foundation
import RealmSwift
class Item: Object {
#objc dynamic var name = ""
}
View Controller
import UIKit
import RealmSwift
class ListViewController: UITableViewController {
let realm = try! Realm()
var itemArray : Results<Item>?
var item:Item?
override func viewDidLoad() {
super.viewDidLoad()
self.itemArray = realm.objects(Item.self)
}
#IBAction func addButtonPressed(_ sender: UIBarButtonItem) {
var textField = UITextField()
let alert = UIAlertController(title: "Add New Item", message: "", preferredStyle: .alert)
alert.view.tintColor = UIColor.red
let action = UIAlertAction(title: "Add Item", style: .default) { (action) in
let newItem = Item()
newItem.name = textField.text!
try! self.realm.write {
self.realm.add(newItem)
}
self.tableView.reloadData()
}
alert.addTextField { (alertTextField) in
alertTextField.placeholder = "Create new item"
textField = alertTextField
}
alert.addAction(action)
present(alert, animated: true, completion: nil)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.itemArray!.count//Size of the Array
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath)//Asigns the Protocol Cell
let data = self.itemArray![indexPath.row]
cell.textLabel?.text = data.name
return cell
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
if let item = itemArray?[indexPath.row] {
try! self.realm.write {
self.realm.delete(item)
}
tableView.deleteRows(at: [indexPath], with: .automatic)
}
}
}
You are retrieving an unsorted result set from Realm, which as per documentation does not necessarily retain insertion order after deletions (basically when you remove 3, then 5 is shifted in its place):
Note that the order of Results is only guaranteed to stay consistent when the query is sorted. For performance reasons, insertion order is not guaranteed to be preserved.
So there are two things you can do:
1.) sort the result set
2.) instead of assuming you're only deleting a single object and otherwise have no movements of any sort, you can rely on Realm's own diffing + change set evaluation with a notification token so that you receive a change set for any possible change that happens to the result set.
// see https://realm.io/docs/swift/latest/#collection-notifications
class ViewController: UITableViewController {
var notificationToken: NotificationToken? = nil
override func viewDidLoad() {
super.viewDidLoad()
let realm = try! Realm()
let results = realm.objects(Person.self).filter("age > 5")
// Observe Results Notifications
notificationToken = results.observe { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
tableView.reloadData()
case .update(_, let deletions, let insertions, let modifications):
// Query results have changed, so apply them to the UITableView
tableView.beginUpdates()
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.endUpdates()
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
}
}
}
deinit {
notificationToken?.invalidate()
}
}
I have researched this and nothing seems to be working. I am trying to build a recipe app and the the image of the dish & names of the dish (appetizer) are not downloading in order. How can I do this?
Code:
class Appetizers: UITableViewController {
var valueToPass: String!
var valuePassed: String!
var appetizer = [String]()
var images = [UIImage]()
func refresh() {
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
}
override func viewDidLoad() {
super.viewDidLoad()
// Parse - class - column
let query = PFQuery(className: "Appetizers")
query.orderByAscending("appetizer")
query.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if error == nil {
if let objects = objects {
for object in objects {
let load = object.objectForKey("appetizer") as! String
self.appetizer.append(load)
let imageFile = object["imageFiles"] as! PFFile
imageFile.getDataInBackgroundWithBlock({ (imageData: NSData?, error: NSError?) -> Void in
if error != nil {
print(error)
} else {
if let data = imageData {
self.images.append(UIImage(data: data)!)
self.tableView.reloadData()
}
}
})
self.tableView.reloadData()
}
}
} else {
print("Error: \(error!) \(error!.userInfo)")
}
}
sleep(1)
refresh()
}
override func viewWillAppear(animated: Bool) {
self.tableView.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return appetizer.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
cell.textLabel?.text = appetizer[indexPath.row]
// add image to table
if images.count > indexPath.row {
cell.imageView?.image = images[indexPath.row]
}
return cell
}
// when user taps on cell ...
func getCellLabel () {
let indexPath = tableView.indexPathForSelectedRow!
let currentCell = tableView.cellForRowAtIndexPath(indexPath) as UITableViewCell!
valueToPass = currentCell.textLabel!.text
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
getCellLabel()
self.performSegueWithIdentifier("0", sender: self)
}
}
When performing asynchronous queries, you have no assurances regarding the order they complete. So, the concept of two separate arrays, one an array of strings and another an array of images will always be problematic.
You could, for example, replace images with a dictionary indexed by the appetizer name, and thus it wouldn't matter what order they complete.
var appetizer = [String]()
var images = [String: UIImage]()
Thus, it might look like:
override func viewDidLoad() {
super.viewDidLoad()
let query = PFQuery(className: "Appetizers")
query.orderByAscending("appetizer")
query.findObjectsInBackgroundWithBlock { objects, error in
guard error == nil, let objects = objects else {
print(error)
return
}
for (index, object) in objects.enumerate() {
let appetizerName = object.objectForKey("appetizer") as! String
self.appetizer.append(appetizerName)
let imageFile = object["imageFiles"] as! PFFile
imageFile.getDataInBackgroundWithBlock { imageData, error in
guard error == nil, let data = imageData else {
print(error)
return
}
// when the image comes in, asynchronously update only that one row
self.images[appetizerName] = UIImage(data: data)
self.tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: index, inSection: 0)], withRowAnimation: .Fade)
}
}
// reload the table only once, after all of the `appetizer` entries are created (but likely before the images come in)
self.tableView.reloadData()
}
}
And
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
let name = appetizer[indexPath.row]
cell.textLabel?.text = name
cell.imageView?.image = images[name]
return cell
}
Or you could just replace these two separate properties with one that is an array of custom objects (e.g. an Appetizer object that has both a name property and an image property).
But any way you do that, you want to name sure you're not dealing with two separate array.
By the way, but this process of loading all of the images can be problematic if you have a lot of rows. This code is employing "eager" loading of images (loading them whether they're currently required or not). The problem is that images are relatively large assets (in comparison to the string values) and you can run into memory issues, network bandwidth issues, etc.
One generally likes to employ "lazy" loading (e.g. let cellForRowAtIndexPath request the image only when it's needed. For example, let's say you have 200 rows, of which only 12 are visible at one point. You shouldn't be requesting 200 images, but rather only that for the 12 visible ones. If you take the image retrieval out of viewDidLoad and, instead, have cellForRowAtIndexPath request them one at a time, you'll have much better network performance and less demanding memory characteristics.
If you're going to save the images in some structure like the code currently does, at the very least make sure you purge those images upon receiving notification of a memory warning (and, obviously, gracefully handle the re-requesting them in a JIT manner as needed).
I figured out the problem with my table not loading without sleep() ...
I had 'self.tableView.reloadData()' outside of the block.
Rob was very helpful :)
I have a tableView that has a header, a footer and a prototype cell for dynamic purposes. I have a Sign Out button in the footer but can't figure out how to get it linked up with a function. I setup a segue to go back to a login page but I want an alert box to ask for confirmation first so I need to somehow call a function.
override func tableView(tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
let footerCell = tableView.dequeueReusableCellWithIdentifier("Footer") as! MenuTableViewCell
footerCell.logout.targetForAction("logOutButtonClicked", withSender: self)
return footerCell
}
This is what I have now, but upon click it crashes and gives me
'NSInvalidArgumentException', reason: '-[.MenuTableViewCell logOutButtonClick:]: unrecognized selector sent to instance 0x7ffce28615d0'
I have a function called logOutButtonClicked that looks like this...
func logOutButtonClicked(){
let alertView = UIAlertController(title: "Log Out?", message: "Are you sure you want to Log Out?", preferredStyle: .Alert)
alertView.addAction(UIAlertAction(title: "Cancel", style: .Default, handler: nil))
alertView.addAction(UIAlertAction(title: "Log Out", style: .Default, handler: {(alertAction) -> Void in
self.logOut()}))
presentViewController(alertView, animated: true, completion: nil)
}
func logOut(){
performSegueWithIdentifier("goHome", sender: self)
}
Edit:
I've also tried footerCell.logout.addTarget(self, action: "logOutButtonClicked", forControlEvents: .TouchUpInside)
but that gives the same error as well.
You have to create a function called "logOutButtonClicked" since you specified by adding the targetForAction, it would look something like this:
func logOutButtonClicked() {
}
I managed to fix this problem by erasing all of the events and outlets corresponding to the button (the ones you find when you right click on it) and then rewriting the function like so...
override func tableView(tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
let footerCell = tableView.dequeueReusableCellWithIdentifier("Footer") as! MenuTableViewCell
footerCell.logout.addTarget(self, action: "logOutButtonClicked", forControlEvents: .TouchUpInside)
return footerCell
}
In my app. There will be user list and message list as my code below
message list code (load the list from parse)
#IBOutlet var messageTableView: UITableView!
var messageArray:[String] = ["Lope"]
override func viewDidLoad() {
super.viewDidLoad()
retrieveMessages()
}
func retrieveMessages() {
var query = PFQuery(className:"Messages")
var user = PFUser.currentUser()
query.whereKey("user", equalTo:user.objectId)
query.findObjectsInBackgroundWithBlock { [weak self]
(objects:[AnyObject]?, error:NSError?) -> Void in
println(objects)
println("succeed")
let messages = objects
for object in objects!{
if let message = object["messageTextColumn"] as? String {
println(object)
self?.messageArray.append(message)
}
}
self?.tableView.reloadData()
}
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messageArray.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("messageCell", forIndexPath: indexPath) as UITableViewCell
cell.textLabel?.text = messageArray[indexPath.row]
return cell
}
add message code (add new message to parse)
class addMessageViewController: UIViewController {
#IBOutlet weak var addMessageText: UITextField!
#IBAction func addMessage(sender: AnyObject) {
var newMessage = addMessageText.text
let message = PFObject(className: "Messages")
var user = PFUser.currentUser()
message["messageTextColumn"] = newMessage
message["user"] = user.objectId
message.saveInBackgroundWithBlock {(success: Bool, error: NSError?) -> Void in
if (success) {
println("added to Message Class")
println(user)
message.saveInBackground()
} else {
// Error saving message
}
}
}
I want to use parse local datastore to store these data in my app locally so that my app won't have to use the internet connect all the time and when the user is not connect to the internet the user list and message list will still appear.
The problem is I don't know what method in local datastore should I use where should I put the local datastore code in "add message code" to save the new message and in "message list code" to query it to my app locally and if there's any update, It will do later after our local "message list" has been loaded. Any help is appreciated.
Thanks!
To begin with Parse data store, you need to opt in from your app delegate:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
// Opt in for Parse Local data store *Before Parse.setApplicationId*
Parse.enableLocalDatastore()
Parse.setApplicationId("YOUR PARSE APP ID",
clientKey: "YOUR PARSE CLIENT ID")
//... other code that you might need when app did finish launching
return true
}
Later when you save a new message you will use:
message.saveEventually()
This will save in the local data store, and eventually (when internet will be available) in the remote data store.
From here you might also be interested in the use of Parse data pinning.
See Parse doc for more.
Hope this helps
Hi I'm new with swift and parse.com, I'm trying do populate my array with already saved images in parse, try to use the dispatch_async but don't know how this works, heres the code:
//imageArray declaration in table:
var imageArray: Array<UIImage> = []
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! CustomCell
var query = PFQuery(className: "ParseClass")
query.findObjectsInBackgroundWithBlock { ( objects: [AnyObject]?, error: NSError?) -> Void in
if(error == nil) {
let imageObjects = objects as! [PFObject]
for object in objects! {
let thumbNail = object["columnInParse"] as! PFFile
thumbNail.getDataInBackgroundWithBlock ({ (imageData: NSData?, error: NSError?) -> Void in
if (error == nil) {
if let image = UIImage(data:imageData!) {
//here where the error appears: Cannot invoke 'dispatch_async' with an argument list of type '(dispatch_queue_t!, () -> _)'
dispatch_async(dispatch_get_main_queue()) {
cell.image.image = self.imageArray.append( image )
}
}
}
})
}
}
else{
println("Error in retrieving \(error)")
}
}
return cell
}
Hope you people understand that code.
The proper way to use that dispatch block is like so:
dispatch_async(dispatch_get_main_queue(), { () -> Void in
//perform your work here
self.imageArray.append(image)
cell.image.image = imageArray[indexPath.row]
})
Another reason that the compiler is giving this warning is because you're trying to append images to an array and set that as the cells image. What you should be doing is shown above.
Finally, I would recommend moving your queries out of cellForRowAtIndexPath: and into seperate methods as this is bad code design.
Edit: Rewrote method to provide examples of good code design...
var imageArray = [UIImage]()
func performLookupQuery() {
var query = PFQuery(className: "ParseClass")
query.findObjectsInBackgroundWithBlock {(objects: [AnyObject]?, error: NSError?)
if error == nil {
let imageObjects = objects as! [PFObject]
for object in imageObjects {
let thumbnail = object["columnInParse"] as! PFFile
thumbnail.getDataInBackgroundWithBlock{(imageData: NSData?, error: NSError?)
if error == nil {
if let image = UIImage(data: imageData!) {
imageArray.append(image)
//now your imageArray has all the images in it that you're trying to display
//you may need to reload the TableView after this to get it to display the images
}
}
}
}
}
self.tableView.reloadData()
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
cell.image.image = nil
cell.image.image = imageArray[indexPath.row]
return cell
}
I didn't add all the error checking in the query method like you should, I was just writing it to show what you would do. Now that you have this query method, you can call it in viewDidLoad and then the results will be available when you try to load your table.
override func viewDidLoad(animated: Bool) {
super.viewDidLoad(animated)
self.performLookupQuery()
}