The following code is deleting the wrong entry. I have an array that shows a list of events. in the debugger indexPath.row shows 1 (which is the entry I selected to delete. However when the view refreshes it has deleted the entry 4.
func tableView(_ tableView: UITableView,
commit editingStyle: UITableViewCellEditingStyle,
forRowAt indexPath: IndexPath) {
print(type(of: selectedRecipient))
var eventsOnArray = selectedRecipient?.events?.allObjects
guard let event = eventsOnArray?[indexPath.row] as? Event, editingStyle == .delete else {
return
}
managedContext.delete(event)
do {
try managedContext.save()
print("Deleted")
eventsOnArray?.remove(at: indexPath.row)
getEvents()
self.eventList.reloadData()
} catch let error as NSError {
print("Saving error: \(error), description: \(error.userInfo)")
}
}
My guess here is that the order of your data source is not the same as the array you use in the code we see.
I base this on the fact that you call events?.allObjects which suggests that events is a NSSet which is unordered and calling allObjects on it gives you an array with an undefined order. You need to have an array instead of a set so you can guarantee the same sort order of your objects through different parts of your code.
Adding to #Joakim Danielson’s answer, you would add an extension for your entity to provide for an array instead of a set, like this:
extension Recipient {
var eventsArray: [Event] {
get {
if let eventSet = events as? Set<Event> {
return eventSet.sorted { (item0, item1) -> Bool in
return item0.title < item1.title
}
}
return []
}
}
}
This assumes there is a title attribute in your Event entity so the array returns all events sorted by title. Modify the sorted closure according to your needs.
Related
I'm new to this so have just been learning about completion blocks, but I am unsure of how to do so in such a way that I get the data to then be apart of a tableview. I have seen other questions related, but regarding older versions of Swift.
I want the table view to contain all the fruit names collected from my database.
I have initialised an empty array list like so:
var fruitNames : [String] = []
Then fetch the data from my firestore database
func getNames(){
let db = Firestore.firestore()
db.collection("fruits").getDocuments() {(snapshot, error) in
if let error = error {
print("There was an error!")
} else {
for document in snapshot!.documents {
let name = document.get("name") as! String
self.fruitNames.append(name)
//completion needed
}
}
}
}
}
I have an extension added on for my tableView
extension FruitsViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
fruitNames.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
cell?.textLabel?.text = self.fruitNames[indexPath.row]
return cell!
}
}
Inside your completion block, you need to tell your table view to update by calling the reloadData() method. In your class, you should have a variable holding your tableView. So, your getName should look like
func getNames(){
let db = Firestore.firestore()
db.collection("fruits").getDocuments() {(snapshot, error) in
if let error = error {
print("There was an error!")
} else {
for document in snapshot!.documents {
let name = document.get("name") as! String
self.fruitNames.append(name)
//completion needed
}
self.tableView.reloadData()
}
}
}
First of all, use [weak self] in closure. Otherwise it can lead to memory leak or crashes. You should read about
automatic reference counting and memory management
closures (https://docs.swift.org/swift-book/LanguageGuide/Closures.html)
If you want to display fruit names, you should call .reloadData() on your tableView object. Then, all delegate methods like numberOfRowsInSection or cellForRowAt will be called again.
You can do something like this :
You have to take an escaping closure as a parameter to the getName() method, which would return Void :
func getName(onComplition: #escaping (_ isSuccess: Bool, _ dataList: [String]) -> Void) {
let db = Firestore.firestore()
db.collection("fruits").getDocuments() {(snapshot, error) in
if let error = error {
print("There was an error!")
onComplition(true, [])
} else {
var data = [String]()
for document in snapshot!.documents {
let name = document.get("name") as! String
data.append(name) // Here data is local variable.
}
onComplition(true, data)
}
}
}
in ViewDidLoad()
override func viewDidLoad() {
self.getName { [weak self] (isSuccess, dataList) in
guard let weakSelf = self else { return }
weakSelf.fruitNames = dataList // fruitNames is your TableViewController's instance variable
weakSelf.tableView.reloadData()
}
}
I have written it directly in IDE, please ignore if there's any syntax error
If you have written perfect code to fetch fruit names.
But your table view is already initialized and loaded with default/empty items in the table view.
You have fetched data after the table view loaded.
So solution is you have to reload your table view again.
So in the closure (After fetching and appending your data to an array) just reload the table view like below and it reloads fresh data.
tableView.reloadData()
User [weak self] or [unowned self] for closure to avoid retain cycles and it causes memory issues.
I'm working on a Core Data project and I got a complier error for deleting item from a TableViewController:
Cannot convert value of type String to expected argument type NSManagedObject
Here's the code:
var listArr:[String] = []
override func viewDidLoad() {
super.viewDidLoad()
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let request = NSFetchRequest<NSFetchRequestResult>(entityName:"Entity")
request.returnsObjectsAsFaults = false
do {
let results = try context.fetch(request)
if results.count > 0 {
for result in results as! [NSManagedObject] {
if let listName = result.value(forKey: "listName") {
listArr.append(listName as! String)
}
}
}
} catch {
// Handle error
print(error)
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifier = "cellID"
let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
// Configure the cell...
cell.textLabel?.text = listArr[indexPath.row]
return cell
}
// Override to support editing the table view.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// Delete the row from the data source
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
context.delete(listArr[indexPath.row]) //The error is here
listArr.remove(at: indexPath.row)
}
self.tableView.reloadData()
}
What do I need to change?
The delete() method that you're calling takes an NSManagedObject as its sole argument. You're passing in an element from your listArr array, which is an array of strings.
It seems obvious to say it, but if you want to delete a managed object, you need to tell Core Data which one to delete. It doesn't know what to do with a string.
I'm guessing (since you didn't say) that your listArr is made up of strings which are stored in Core Data as a property of an entity. You're fetching data somewhere and saving only the strings. That's fine as long as your data store is read-only, at least in this part of the app. You can display the strings but you can't go back and update Core Data, because you don't have the necessary information.
If that's true, what you should do is use an array of managed objects instead of an array of strings. When you need the string value, get it from one of the managed objects. When you need to delete an entry, delete that managed object.
There are two ways that you can populate a table from core-data. Either you can use a NSFetchedResultsController that tracks changes from core data and keeps your data in sync. Or you can do a single fetch and store the values you want to display in our own datasource (such as an array).
With a NSFetchedResultsController you will get updates as your data changes. With this model the correct way to delete a row is to delete it from core-data and wait for a callback from the NSFetchedResultsController to update your view.
With a single fetch (as you are doing) you do not get updates as core-data changes. In this model you can simply remove the object from your array and update the view. Updating core-data is another task unrelated to your view or datasource. In this case you should use persistentContainer performBackgroundTask to fetch the object you want to delete and then delete it in the background. For this to work you need to have a way to fetch the managedObject that you which to delete. I don't know what kind of strings you are storing - but if they are unique you could use them for your fetch. Otherwise you would also have to store some unique ID of the object such as it's objectID.
I'm just starting out and I've used UserDefaults once or twice, never used CoreData, so I'm not sure which to use in this situation. I have a table view, which has cells populated from an empty array, which is is populated by an array of strings, depending on the page title.
In other words, I have a function in viewDidLoad which checks the navigationItem.title, and depending on what it is, populates the empty array with the correct strings. Everything loads well but I need deleted items to remain deleted. Here is my code:
Initializations:
static var places = [String]()
var nycPlaces = ["Central Park", "Rockefeller Center", "Empire State Building"]
var londonPlaces = ["London Eye", "Windsor Castle", "Tower of London"]
var parisPlaces = ["Champs Elysee", "Eiffel Tower", "Catacombs"]
This is the function that populates the table view with the correct array - this is called in viewDidLoad:
func populateCells() {
switch navigationItem.title {
case "New York":
ListController.places = nycPlaces
case "London":
ListController.places = londonPlaces
default:
ListController.places = parisPlaces
}
tableView.reloadData()
}
And to delete, I have:
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath)
{
if editingStyle == .delete
{
tableView.beginUpdates()
ListController.places.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
tableView.endUpdates()
}
}
The problem is that when I navigate away from the page then re-load it, the empty array is re-popoulated with the 3 strings, regardless of which ones had been deleted before. How do I ensure that the deleted cell doesn't come back?
Thanks for any help!
You should use UserDefaults if your app is a simple as the code you posted (i.e. you're not fetching the data from some remote server and it really is just a few hard-coded values).
CoreData has a steep learning curve, it is difficult to use, and difficult to use correctly. And it is overkill for the scenario you have presented.
Also note: if you don't need this to persist across your process exiting and restarting you can just create mutable arrays that live somewhere accessible by all your classes (e.g. your AppDelegate, a global singleton class, whatever) and just modify them in place.
Since this is mostly static data, I would advise against having the overhead of CoreData. Instead I would use this approach:
var nycPlaces: [String] {
get {
guard let retval = UserDefaults.standard.object(forKey: "nyc") as? [String] else {
return ["Central Park", "Rockefeller Center", "Empire State Building"]
}
return retval
} set {
UserDefaults.standard.set(newValue, forKey: "nyc")
UserDefaults.standard.synchronize()
}
}
var londonPlaces: [String] {
get {
guard let retval = UserDefaults.standard.object(forKey: "london") as? [String] else {
return ["London Eye", "Windsor Castle", "Tower of London"]
}
return retval
} set {
UserDefaults.standard.set(newValue, forKey: "london")
UserDefaults.standard.synchronize()
}
}
var parisPlaces: [String] {
get {
guard let retval = UserDefaults.standard.object(forKey: "paris") as? [String] else {
return ["Champs Elysee", "Eiffel Tower", "Catacombs"]
}
return retval
} set {
UserDefaults.standard.set(newValue, forKey: "paris")
UserDefaults.standard.synchronize()
}
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath)
{
if editingStyle == .delete
{
tableView.beginUpdates()
ListController.places.remove(at: indexPath.row)
switch navigationItem.title {
case "New York":
nycPlaces = ListController.places
case "London":
londonPlaces = ListController.places
default:
parisPlaces = ListController.places
}
tableView.deleteRows(at: [indexPath], with: .fade)
tableView.endUpdates()
}
}
P.S. - I'd also advise to have this data at a more global scope and to use an enum instead of doing string comparison against your navigation item's title :)
I have a tableview showing data from a Firebase console (JASON structure) like this:
chklst-xxx
- avaliation
-students
-KHZAjBi44eZ8JsaswkKI
-teacher: Paul
-datTime: 09.12.2016 12:25pm
-name: Mary Anne
-mark: 7.5
-preceptor: John
-KHWZlh7aasy78ahsiuKIi0
-teacher: Paul
-datTime: 09.12.2016 12:48pm
-name: Anne Caroline
-mark: 9.4
-preceptor: Peter
These data are shown at a tableview, each one, after node students, except the key. I have one function to delete an especific row, as i do with other apps in CoreData (tableview editingStyle):
// FUNC DELETAR
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
teste.remove(at: indexPath.row)
ref.removeValue() // HERE IS MY DOUBT
tableView.reloadData()
}
}
I can erase the item from the array and from the table, but i just don't know how to do it from Firebase. I have tried other ways with removeValue, but it just didn't work, except an way to erase the whole data, not specifically that row selected.
let ref = FIRDatabase.database().reference().child("avaliation").child("students")
I need some teaching to make this works. Thank you.
Here is my method for Saving this data: (Added 09.12 19:30pm)
func Save(){
let ref = FIRDatabase.database().reference().child("avaliation").child("students").childByAutoId()
referencia.child(“name”).setValue(Name)
referencia.child(“teacher”).setValue(Teacher)
referencia.child("preceptor").setValue(Preceptor)
referencia.child("datTime”).setValue(DateTime)
referencia.child(“mark”).setValue(ResultComFReFC)
}
And here is my Class where I work with the main Array:
import Foundation
import Firebase
struct ChkList {
var name: String?
var teacher: String?
var preceptor: String?
var DateToday: String?
var mark: String?
var key: String!
init(name: String, teacher: String, preceptor: String, mark: String, DateToday: String, key: String = "") {
self.name = name
self.teacher = teacher
self.preceptor = preceptor
self.mark = mark
self.DateToday = DateToday
self.key = key
}
init(snapshot: FIRDataSnapshot) {
key = snapshot.key
let snapshotValue = snapshot.value as! [String: AnyObject]
name = snapshotValue["name"] as? String
teacher = snapshotValue["teacher"] as? String
preceptor = snapshotValue["preceptor"] as? String
mark = snapshotValue["mark"] as? String
DateToday = snapshotValue["DateToday"] as? String
}
func toAnyObject() -> Any {
return [
"name": name,
"teacher": teacher,
"preceptor": preceptor,
"mark": mark,
"DateToday": DateToday ]}}
UPDATE: Tableview EdityingStyle
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let ref = FIRDatabase.database().reference().child("avaliation").child("students").childByAutoId()
teste.remove(at: indexPath.row) //Array delet item
let userKeyA = ref.key
ref.child(userKeyA).removeValue()
tableView.reloadData()
}
}
My suggestion, store the pushKey of the user by using 'getKey' method when you push the user details.
chklst-xxx
- avaliation
-students
-KHZAjBi44eZ8JsaswkKI
-teacher: Paul
-userKey:KHZAjBi44eZ8JsaswkKI <--------------
-datTime: 09.12.2016 12:25pm
-name: Mary Anne
-mark: 7.5
-preceptor: John
Now you can retrieve the 'key` of the user. Suppose you are listing, users in the list view and would want to delete a row.
let ref = FIRDatabase.database().reference().child("avaliation").child("students").child(students.getUserKey()).removeValue();
Let me know, if it works.
Update: I don't know swift. But i'll give u an abstract idea.
When you create a new student, you refer student node and push() right. Like below,
DatabaseReference studentsRef = FIRDatabase.database().reference().child("avaliation").child("students").push();
And then you put your data like,
studentRef.child("teacher").setValue("Paul");
studentRef.child("userKey").setValue(studentRef.getKey()); <------------
I did it, using suggests above and studying and trying a lot.
The way to the method works is really capture the userkey.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let user: ChkList = teste[indexPath.row]
let uk = user.key
print("Userkey: \(uk)")
ref.child(uk!).removeValue()
}
}
This way you will have removed the data from Firebase and the row form tableview.
This answer moves the Firebase delete behavior behind the scenes so as
to not interfere with the normal tableview loading mechanisms. One
benefit is reducing the number of calls to Firebase and removing
timing issues from the internal tableview animation process.
What worked for me is getting the Firebase stuff out of the way. In my case, I collect all the keys that I want. During the delete, I remove the values from my local object, and when leaving the scene I call my cleanup method from viewDidDisappear() to sync the updated data back to Firebase:
func syncFavorites() {
//ref is the leaf for the favorites
//clear existing
ref?.removeValue()
//temp collection
var faves = [String:Any]()
//dataArrayKeys is my collection of keys on self
dataArrayKeys.keys.forEach { (key) in
faves.updateValue("true", forKey: key)
}
ref?.updateChildValues(faves)
}
I handle my tableview's delete action using the normal method.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// Delete the row from the data source
if let key = dataArr[indexPath.row]["key"] as? String {
dataArrayKeys.removeValue(forKey: keyToDelete)
dataArr.remove(at: indexPath.row)
}
tableView.deleteRows(at: [indexPath], with: .fade)
self.tableView.reloadData()
}
}
I have no doubt there is probably a better way. This was the simplest possible thing that worked for me.
I have a UITableView which looks like this image
.
When I swipe to delete the record, I can remove it perfectly okay from the array in which it is stored, but I am having difficulties in accessing it in Firebase to delete it there.
My Firebase database structure is as follows for the above screenshot:
-KWc7RTuOe5PefiMM2tL
bodyPart: "Arms"
exerciseName: "Test 1 "
userId: "8rHmyTxdocTEvk1ERiiavjMUYyD3"
-KWcEbpw_f6kxcePY5cO
bodyPart: "Chest"
exerciseName: "Test 2 "
userId: "8rHmyTxdocTEvk1ERiiavjMUYyD3"
-KWcEdUN49QaJIVf0kwO
bodyPart: "Legs"
exerciseName: "Test 3 "
userId: "8rHmyTxdocTEvk1ERiiavjMUYyD3"
-KWcFrMSaLKQRxghGHyT
bodyPart: "Arms"
exerciseName: "Test 4"
userId: "8rHmyTxdocTEvk1ERiiavjMUYyD3"
How can I access the autoId value which is set when it is created e.g "-KWc7RTuOe5PefiMM2tL" so I can remove that child node?
Or alternatively could I access the exerciseName value depending on the UserId that is logged in?
To solve this issue I tried a number of different methods before finally reaching my intended result.
To delete the value, I created a reference to the child node 'userExercises', then ordered it by 'exerciseName' and then .queryEqual(toValue:) the exercise name value which I extracted form the UITableViewCell.
I then removed the snapshot value of this and the example code is below:
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
if let exerciseName = exercises[indexPath.row].exerciseName {
let ref = FIRDatabase.database().reference().child("userExercises")
ref.queryOrdered(byChild: "exerciseName").queryEqual(toValue: exerciseName).observe(.childAdded, with: { (snapshot) in
snapshot.ref.removeValue(completionBlock: { (error, reference) in
if error != nil {
print("There has been an error:\(error)")
}
})
})
}
exercises.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .left)
}
}
Following on from what MHDev has already answered:
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
if let exerciseName = exercises[indexPath.row].exerciseName {
let ref = FIRDatabase.database().reference().child("userExercises")
ref.queryOrdered(byChild: "exerciseName").queryEqual(toValue: exerciseName).observe(.childAdded, with: { (snapshot) in
snapshot.ref.removeValue(completionBlock: { (error, reference) in
if error != nil {
print("There has been an error:\(error)")
}
})
})
}
exercises.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .left)
}
}
It's a fairly straightforward process:
In general, a datasource for tableViews is an array. That array is built from dictionaries read from Firebase snapshots - or an array of objects built from the snapshots (recommended).
So here's an example that matches your Firebase structure (this was populated from a single node from a snapshot)
class Exercise {
key: "KWc7RTuOe5PefiMM2tL"
bodyPart: "Legs"
exerciseName: "Test 3 "
userId: "8rHmyTxdocTEvk1ERiiavjMUYyD3"
}
Then, when the user swipes row 3 for example, retrieve the Exercise object from the array, row3.
let theObject = ExerciseArray[3]
let parentNode = theObject.key
let ref = rootNode.child(parentNode)
ref.setValue(nil)
and you're done.