Removing value from UITableView causes crash - ios

I have a UITableView and when I click on a cell I update a value in firebase to be true. I also implemented the ability to delete the UITableViewCell. The problem is that if the cell has been clicked on (updated) and then deleted the app crashes. When I have a look in firebase the value that has been updated has not been deleted (everything else has been), this only happens if the cell has been updated. This is what I got
// remove task
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
let taskToDelete = groupTask[indexPath.row]
if editingStyle == UITableViewCellEditingStyle.delete {
groupTask.remove(at: indexPath.row)
DataService.instance.REF_GROUPS.child(group!.key).child("task").child(taskToDelete.id).removeValue(completionBlock: { (error, refer) in
if error != nil {
} else {
}
})
self.tableView.reloadData()
}
}
// update task value
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? GroupTaskCell else { return }
if cell.isSelected == true {
let selected = groupTask[indexPath.row]
DataService.instance.REF_GROUPS.observe(.value) { (snapShot) in
DataService.instance.updateTaskStatus(desiredGroup: self.group!, selected: true, atIndexpath: indexPath.row, childPath: selected.id, handler: { (complete) in
self.tableView.reloadData()
})
}
} else {
}
self.tableView.reloadData()
}
// update task status
func updateTaskStatus(desiredGroup: Group, selected: Bool, atIndexpath: Int, childPath: String, handler: #escaping (_ taskArray: [Task]) -> ()) {
REF_GROUPS.child(desiredGroup.key).child("task").child(childPath).updateChildValues(["selected": selected])
}
EDIT!
Could not cast value of type 'NSNull' (0x1b5915f18) to 'NSString' (0x1b5921ad8).
2018-01-01 17:38:53.431901+0200 WireUp[587:103199] Could not cast value of type 'NSNull' (0x1b5915f18) to 'NSString' (0x1b5921ad8).
(lldb)
What firebase looks like before I delete the task:
And after:
As you can see it is not removing the value that has been updated and that is what's causing the crash. I appreciate all help.
func addTask(withTask task: String, andPeople people: String, forUID uid: String, withGroupKey groupKey: String?, selectedStatus selected: Bool, sendComplete: #escaping (_ taskCreated: Bool ) -> ()) {
if groupKey != nil {
REF_GROUPS.child(groupKey!).child("task").childByAutoId().updateChildValues(["taskToDo": task, "peopleToDoTask": people, "senderId": uid, "selected": selected])
sendComplete(true)
} else {
}
}
func updateTaskStatus(desiredGroup: Group, selected: Bool, childPath: String, handler: #escaping (_ taskArray: [Task]) -> ()) {
REF_GROUPS.child(desiredGroup.key).child("task").child(childPath).updateChildValues(["selected": selected])
}
func configureTaskCell(taskToDo task: String, nameForPerson name: String, isSelected: Bool ) {
self.task.text = task
self.name.text = name
if isSelected {
self.chechImg.isHidden = false
} else {
self.chechImg.isHidden = true
}
}

Instead of using self.tableView.reloadData() from within the table view delegate functions try updating and deleting cells using the beginUpdate() and endUpdate() methods of UITableView. You won't crash and you won't be reloading the whole table. In Obj-C, it looks like this for removing and updating a cell:
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
[tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects: indexPath, nil] withRowAnimation:UITableViewRowAnimationNone];
[tableView endUpdates];

It seems as though the code you've provided isn't the issue. It's being deleted correctly but as you delete it, it's also updating again with just the selected: true. Somewhere in your code, it'll be trying to fetch the rest of the data, including the selected: true BUT it's not there; resulting in the crash. You may want to do some debugging to see where you're actually updating this selected value to your database and get rid of it

Related

saving API response to coreData, still shows nil when accessing

func getGenreKeys(complition: #escaping (_ genre : GenreListModel?) -> ())
{
let genreUrl = URL(string: "\(baseUrl)\(genreListUrl)\(apiKey)")!
urlSessionManager(url: genreUrl,toUseDataType: GenreListModel.self) { json in
//json will contain genreList Object , which can be used to get keys
switch json
{
case .success(let genreListData) :
complition(genreListData)
CoreData.shared.saveGenreList(json: genreListData)
case .failure(let error) :
print(error)
}
}
}
this above is the api completion code
func saveGenreList(json: GenreListModel){
let context = persistentContainer.viewContext
let genreList = GenreList(context: context)
json.genres?.forEach({ Genres in
genreList.name = Genres.name
do{
try context.save()
}
catch{
print("error in saving")
}
})
}
this is what i did to save data after completion of api fetch.
var coreGenre : GenreList?
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return coreGenre?.name?.count ?? 0
this above code is the part of VC that requires to get the coreGenre.name to give the count but it is nill
but when i try to access from viewController by creating a variable of the core data entity class , it returns nill
What's wrong?
I'll base my answer on code there. In your GitHub account where you shared that code.
First issue:
You have:
var genrelist : GenreListModel? {
didSet{
//after getting data a table needs to reload and ui elements needs to be used in main thread only
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
var coreGenre : [GenreList]?
override func viewWillAppear(_ animated: Bool) {
...
ApiManager.shared.getGenreKeys { genre in
self.genrelist = genre
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return coreGenre?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
genrelist?.genres?[indexPath.row].id
...
cell.genreLabel.text = genrelist?.genres?[indexPath.row].name
...
}
First issue:
coreGenre IS NEVER SET! genreList is set, but not coreGenre. So don't expect it to be something else that nil!
You didn't write coreGenre = something
So tableView(_:numberOfRowsInSection:) will always return 0.
Make your choice. Either genreList or coreGenre:
ApiManager.shared.getGenreKeys { genreList in
self.coreGenre = genreList.genres
}
&
var coreGenre: [GenreList] {
didSet { DispatchQueue.main.async { self.tableView.reloadData() } }
}
And remove var genreList
Or, remove coreGenre.
That should fix the UITableView, and that's where your question is misleading: There is no CoreData here. In the completion of getGenreKeys(), you return the value. CoreData saves the value in the method, but that's all.
It's:
API -> Parse Value -> Save in CoreData
-> Completion -> Reload TableView to Display
Now in your CoreData stack, you have a method saveGenreList(json:). Assuming it works, you save it but never retrieve it.
There is no NSFetchRequest in your code. Look how to execute fetch in CoreData.
Just stop thinking about CoreData, to understand the issue:
let retrieveValueFromAPI = "I got it"
let savedValue = retrieveValueFromAPI // (1)
return retrieveValueFromAPI
In (1), you'll have an unused variable warning from Xcode. It's exactly the same issue. Here its a "RAM value" instead of a disk value, but that's the same logic. You don't use what you've saved.
You need to continue working and debug. And debug is not only fixing the issue. There are a few steps:
Reproduce the issue
Find its origin/understand what could be the issue
Fix the issue
Finding its origin, and why, are the steps you were missing, either by misconcept, or not enough step by step research in your code.

function not called in escaping closure

I start learning MVVM and I decided to refactor my code with MVVM.
In ViewModel I have function which fetch director value:
func loadDirector(id: Int, completed: #escaping () -> ()) {
homeRepository.getDirector(id: id){ (creditReponse) -> (Void) in
creditReponse?.crew.forEach({ singleValue in
if singleValue.knownForDepartment == .directing {
self.directorName = singleValue.name
}
})
}
}
Function getDirector is from Repository where I am fetching data from URL.
func getDirector(id:Int, _ completed: #escaping (CreditsResponse?) -> (Void)) {
movieServiceAPI.fetchData(from: NetworkData.directorUrl(id: id).value, by: completed)
}
View part where I am calling loadDirector :
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let movie = homeViewModel.movieList[indexPath.row]
homeViewModel.loadDirector(id: movie.id, completed: {
self.changeVC(movie: movie, director: (self.homeViewModel.directorName), groups: self.homeViewModel.setupGenres(groups: movie.genreIds), movieIndex: indexPath.row)
})
}
Problem is when I click on cell in my tableview, function changeVC is not called (it doesnt show any error, function is just not called). loadDirector function firstly only had id parameter and then problem was that directorName wasnt updated on time (second cell click had first cell click director value). For that reason I added escaping closure so I can change my VC when director value is updated, but I am having problem with that. Where am I making mistake?

How to reload tableview after adding new entry?

I am creating a cloudkit tableview. I load the app and my tableview appears with my entries from cloud kit.
I then use my add method insertNewObject which adds the record to cloud kit but this does not show up in my tableview. It will only show up on my next run of the app.
func insertNewObject(sender: AnyObject) {
let record = CKRecord(recordType: "CloudNote")
record.setObject("New Note", forKey: "Notes")
MyClipManager.SaveMethod(Database!, myRecord:record)
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
This is my add method. I am calling tableview reload as you can see but nothing is happening.
My tableview creation code:
// Tableview stuff --- Done
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
/////// Get number of rows
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count
}
//// FIll the table
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
let object = objects[indexPath.row]
cell.textLabel!.text = object.objectForKey("Notes") as? String
return cell
}
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
As requested: Method that saves to CloudDB
func SaveMethod(publicDatabase: CKDatabase, myRecord: CKRecord ) -> CKRecord {
publicDatabase.saveRecord(myRecord, completionHandler:
({returnRecord, error in
if let err = error {
self.notifyUser("Save Error", message:
err.localizedDescription)
} else {
dispatch_async(dispatch_get_main_queue()) {
self.notifyUser("Success",
message: "Record saved successfully")
}
}
}))
return myRecord
}
My viewdidload method in masterview:
override func viewDidLoad() {
super.viewDidLoad()
// Database loading on runtime
Database = container.privateCloudDatabase
///Build Query
let query = CKQuery(recordType: "CloudNote", predicate: NSPredicate(format: "TRUEPREDICATE"))
///Perform query on DB
Database!.performQuery(query, inZoneWithID: nil) { (records, error) -> Void in
if (error != nil) {
NSLog("Error performing query. \(error.debugDescription)")
return
}
self.objects = records!
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
You should not reload your entire tableView when you insert a single object. Only do that when you know ALL the data has changed.
To do what you want, this is the order:
Insert a new data object into your datasource (self.objects). Make sure you get the index of where it ends up in the array.
Call insertRowAtIndexPath: with the correct indexPath on your tableView. This will make sure your data and tableView are in sync again, and tableView:cellForRowAtIndexPath: is called for at least your new data object (and possible others, as certain cells might now be reused to display other data).
Note that the order is always: update your data first, then update your UI (the only place I know of that his is hairy is when using a UISwitch).

Deleting an object in a tableView cell from Parse

I am attempting to delete certain objects from Parse. The user will be able to save posts and I am trying to implement a way for them to delete them. The saved posts can be found in a TableView.
I currently have the following.
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
let deleteAction = UITableViewRowAction(style: .Default, title: "Remove") { (action: UITableViewRowAction!, indexPath: NSIndexPath!) -> Void in
let query = PFQuery(className: "SavedObjects")
query.orderByDescending("createdAt")
query.whereKey("savedByUser", equalTo: (PFUser.currentUser()?.username)!)
query.findObjectsInBackgroundWithBlock({ (objects : [PFObject]?, error: NSError?) -> Void in
if error == nil {
for object in objects! {
object.deleteInBackground()
}
}
})
self.tableView.reloadData()
}
deleteAction.backgroundColor = UIColor.redColor()
return [deleteAction]
}
The object.deleteInBackground() does work, but it deletes all objects that were saved by the user. I would like it to only delete the object that the cell represents.
I have also tried to use object.deleteInBackgroundWithTarget([indexPath.row], selector: nil) but have had no luck.
if I understood your question correctly, I think you need to add another query.WhereKey() filtering by the objectId of the corresponding row saved on Parse. This way the call to object.deleteInBackground() will only delete the cell you selected from the TableView. I hope this was helpful!
Valerio

Building table view while getting number of rows from completion block

I am trying to retrieve the calendar events in swift 2, and I can not solve this: to build the table view I need to know the number of cells, which I can get from a method like this (for the sake of simplicity array is String):
func fetchCalendarEvents () -> [String] {
var arrayW = [String]()
let eventStore : EKEventStore = EKEventStore()
eventStore.requestAccessToEntityType(EKEntityType.Event, completion: {
granted, error in
if (granted) && (error == nil) {
print("access granted: \(granted)")
//do stuff...
}
else {
print("error: access not granted \(error)")
}
})
return arrayW
}
I am calling this method in viewDidLoad, and save the array to var eventArray. Immediately the following method gets called:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return eventArray.count
}
The problem is that the completion block of the fetchCalendarEvents is not complete at this point, and therefore returns 0 (while there are events in the calendar).
My question: how can I handle building the table view from array that I get from method, that has completion block, and takes some time to be completed?
Add aBlockSelf.tableView.reloadData() in your calendar completion block to reload your table with fetched data.
Here aBlockSelf is a weak reference to self because its being passed to a block.
EDIT: Post OP comment - Try this:
weak var aBlockSelf = self
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (eventArray.count == 0) { //or unwrap value, depends on your code
return 0 // or 1, if you want add a 'Loading' cell
} else {
return eventArray.count
}
}
And, when you get eventArray, just reload table with
//without animation
tableView.reloadData()
//or with, but it can get little tricky
tableView.insertRowsAtIndexPaths(indexArray, withRowAnimation:UITableViewRowAnimationRight];
Make 2 cells type. One for events and one which just say "Loading...". While your block in progress show only 1 cell with "Loading...". When an events will retrieve hide this cell and reload table with events.
Cheers.
Ok, all seemed to answer relatively correct, but not exactly in the way that worked straight. What worked for me, is that instead of returning array, I just need to reload the table view after the for loop, where array is built up:
func fetchCalendarEvents () {
let eventStore : EKEventStore = EKEventStore()
eventStore.requestAccessToEntityType(EKEntityType.Event, completion: {
granted, error in
if (granted) && (error == nil) {
print("access granted: \(granted)")
let startDate=NSDate().dateByAddingTimeInterval(-60*60*24)
let endDate=NSDate().dateByAddingTimeInterval(60*60*24*3)
let predicate2 = eventStore.predicateForEventsWithStartDate(startDate, endDate: endDate, calendars: nil)
print("startDate:\(startDate) endDate:\(endDate)")
let events = eventStore.eventsMatchingPredicate(predicate2) as [EKEvent]!
if events != nil {
var arrayOfEvents = [CalendarEventObject]()
for event in events {
var eventObject: CalendarEventObject
// (ಠ_ಠ) HARDCODEDE
eventObject = CalendarEventObject(id: 0, title: event.title, location: event.location!, notes: event.notes!, startTime: event.startDate, endTime: event.endDate, host: "host", origin: "origin", numbers: ["0611111111", "0611111112"], passcodes: ["123456", "123457"], hostcodes: ["123458", "123459"], selectedNumber: "0611111113", selectedPasscode: "123457", selectedHostcode: "123459", scheduled: true, parsed: false, update: true, preferences: [], eventUrl: "www.youtube.com", attendees: [])
arrayOfEvents.append(eventObject)
}
self.eventArray = arrayOfEvents
//reload data after getting the array
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
}
}
else {
print("error: access not granted \(error)")
}
})
}

Resources