I'm trying to delete items from a Firebase database when user swipes to delete on the UITableView.
I'm also using the nifty showActivityIndicator and hideActivityIndicator functions from https://github.com/erangaeb/dev-notes/blob/master/swift/ViewControllerUtils.swift in order to easily show and hide the activity indicator.
I can see both the database record as well as the JPG file properly deleted from Firebase when the following code is run.
But the problem is that the activity indicator never disappears from the screen, and the UITableView does not refresh.
In fact, the
print("Stopping activity indicator")
and
print("Reloading table view")
do not even run.
What am I doing wrong?
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let alert = UIAlertController(title: "Delete Task", message: "Do you want to delete this task?", preferredStyle: .alert)
let confirmAction = UIAlertAction(title:"OK", style: .default) { action in
// Show activity indicator
ViewControllerUtils().showActivityIndicator(uiView: self.view)
// Retrieve Firebase reference to delete
let repairItem = self.repairItems[indexPath.row]
let storage = Storage.storage()
let storageRef = storage.reference()
let imageRef = storageRef.child(repairItem.imageRef)
// Delete the item from Firebase
repairItem.ref?.removeValue(completionBlock: {(error, ref) in
if let error = error {
print("Problem deleting item")
} else {
print("Item deleted successfully")
}
})
// Delete the photo
imageRef.delete { error in
if let error = error {
print("Problem deleting photo")
} else {
print("Photo deleted successfully")
}
}
print("Stopping activity indicator")
// Stop the activity indicator
ViewControllerUtils().hideActivityIndicator(uiView: self.view)
print("Reloading table view")
// Reloading table view
self.tableView.reloadData()
}
let cancelAction = UIAlertAction(title: "Cancel", style: .default)
alert.addAction(confirmAction)
alert.addAction(cancelAction)
present(alert, animated: true, completion: nil)
}
}
Still not sure why the code below the Firebase removeValue and delete functions aren't running, but I managed to figure out a workaround.
Turns out that the code doesn't work only when I'm deleting the last remaining record on the table, after which the entire Firebase node is removed.
So I've added an observer to forcefully reload the table when this happens.
Under viewDidLoad(), I added the following lines:
override func viewDidLoad() {
super.viewDidLoad()
// ... other preceding lines of code
// Forcefully trigger a TableView refresh in case the Firebase node is empty
refItems.observe(.value, with: { snapshot in
if snapshot.exists() {
print("Found the Firebase node")
} else {
print("Firebase node doesn't exist")
self.tableView.reloadData()
}
})
// ...other following lines of code
}
When it comes to keeping a tableView and Firebase synchronized, there are two main options:
1) When removing a node from Firebase, manually remove it from the
tableView datasource when removing it from Firebase. No observer needed.
2) Attach a remove event observer to Firebase so when a node is removed,
your App will receive an event of which node it was, and remove it.
Here's an example; I am leaving the tableView code out for the sake of brevity.
Suppose we have a tableView that displays users. The tableView has a dataSource of usersArray which contain UserClass objects.
Given a Firebase structure
users
user_0
name: "Frank"
user_2
name: "Bill"
user_3
name: "Hugh"
We add three observers to the users node, childAdded, childChanged, childRemoved
class UserClass {
var key = ""
}
var usersArray = [UserClass]()
let usersRef = self.ref.child("users")
usersRef.observe(.childAdded, with: { snapshot in
let user = UserClass()
user.key = snapshot.key
self.usersArray.append(user)
self.tableView.reloadData()
})
usersRef.observe(.childChanged, with: { snapshot in
if let index = self.usersArray.index(where: {$0.key == snapshot.key}) {
//update it in the array via the index
self.tableView.reloadData()
} else {
//item not found
}
})
usersRef.observe(.childRemoved, with: { snapshot in
if let index = self.usersArray.index(where: {$0.key == snapshot.key}) {
self.usersArray.remove(at: index) //remove it from the array via the index
self.tableView.reloadUI()
} else {
item not found
}
})
If a user is added, the .childAdded observer receives the new user so it can be appended to the array.
If a user is changed, the .childChanged observer receives the changed user. Grab the key, which would typically be the uid, locate it in the array and update the properties.
If a user is removed, the .childRemoved observer receives the removed user. Grab the key, locate it in the array and remove it.
In each case, reload the tableView after the change to the dataSource.
you need to write code inside of block as below :
imageRef.delete { error in
if let error = error {
print("Problem deleting photo")
} else {
print("Photo deleted successfully")
ViewControllerUtils().hideActivityIndicator(uiView: self.view)
print("Reloading table view")
// Reloading table view
self.tableView.reloadData()
}
}
Related
So my goal here is to have the firestore query logic work every time instead of it working perfectly one time and not working properly every time after that. Currently, I have a function that determines whether a school user can delete an event or not based off the fact if a student has purchased a ticket for this event already.
If even one student has purchased a ticket for this event, the school user cannot delete this event, having this in place will prevent bugs and app crashes. The logic I use is simple, I do a query snapshot to check if there are actually students under the school ID of the school user, and then I do a second query snapshot to see if there are students that have purchased a ticket for the event already.
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { (deleted, view, completion) in
guard let user = Auth.auth().currentUser else { return }
let deleteIndex = client.index(withName: IndexName(rawValue: user.uid))
let documentid = self.documentsID[indexPath.row].docID
let algoliaID = self.algoliaObjectID[indexPath.row].algoliaObjectID
self.getTheSchoolsID { (id) in
guard let id = id else { return }
self.db.collection("student_users").whereField("school_id", isEqualTo: id).getDocuments { (querySnapshot, error) in
guard error == nil else { return }
for document in querySnapshot!.documents {
let userUUID = document.documentID
self.db.collection("student_users/\(userUUID)/events_bought").whereField("eventID", isEqualTo: documentid).getDocuments { (querySnapshotTwo, error) in
guard error == nil else { print(error)
return }
guard let query = querySnapshotTwo?.isEmpty else { return }
if query == false {
self.showAlert(title: "Students Have Purchased Tickets For This Event", message: "This event cannot be deleted until all students who have purchased a ticket for this event have completely refunded their purchase. Please be sure to make an announcement that this event will be deleted.")
return
} else {
print(querySnapshotTwo?.count)
let alert = UIAlertController(title: "Delete Event", message: "Are you sure you want to delete this event?", preferredStyle: .alert)
let deleteEvent = UIAlertAction(title: "Delete", style: .destructive) { (deletion) in
let batch = self.db.batch()
let group = DispatchGroup()
group.enter()
deleteIndex.deleteObject(withID: ObjectID(rawValue: algoliaID)) { (result) in
if case .success(_ ) = result {
group.leave()
}
}
group.notify(queue: .main) {
let docRef = self.db.document("school_users/\(user.uid)/events/\(documentid)")
batch.deleteDocument(docRef)
batch.commit { (err) in
guard err == nil else { return }
}
self.eventName.remove(at: indexPath.row)
tableView.reloadData()
}
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(deleteEvent)
self.present(alert, animated: true, completion: nil)
}
}
}
}
}
}
deleteAction.backgroundColor = UIColor.systemRed
let config = UISwipeActionsConfiguration(actions: [deleteAction])
config.performsFirstActionWithFullSwipe = false
return config
}
I show two different alerts based off the query result, if the query is empty, the event can be deleted as normal and this is what it would like if you try to delete it.
Now if a student has already purchased a ticket for this event, the school user should be seeing this alert every time:
The issue is that this works perfect the first time, first time as in when I first run the simulator or log out and log back in. Every time after that, it shows the other alert as if no students have purchased a ticket for it, but the purchase is still active which makes me so confused. To keep it brief, the code I am using works, but it doesn't work more than once which I can't figure out for the life of me.
Does anybody know how I can make this firestore query logic work perfect every time?
I have a tableViewController (ArticlesVC) that displays an array of Articles that are fetched from an API. Each of these tableViewCell has a "more action" button that allows the user to save the article for later reading in the SavedVC. Note that only saved articles are written to Realm DB, the displaying of articles in the ArticlesVC are not written to Realm DB.
If the user has saved the article, I would show "Remove saved article", otherwise, I show "Save for later". I query Realm to conduct this check.
However, if I delete a saved article (or all saved articles) from Realm from SavedVC and go back to the ArticlesVC and start swiping the tableView, it crashes with the above error. This crash happens on an intermittent basis, making it pretty hard to pinpoint.
Code
//At ArticlesVC, where articles are fetched via API
func getArticles() {
AF.request(apiUrl).responseJSON { (response) in
//Error checks and decoding ...
self.articles = try decoder.decode(Article.self, from: data)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! ArticleCell
cell.article = articles[indexPath.row]
cell.moreButton.tag = indexPath.row
cell.moreButton.addTarget(self, action: #selector(moreButtonTapped(sender:)), for: .touchUpInside)
return cell
}
#objc func moreButtonTapped(sender: UIButton) {
let buttonTag = sender.tag
let article = articles[buttonTag]
let alert = UIAlertController(title: "More", message: nil, preferredStyle: .actionSheet)
let action = getAction(article: article)
alert.addActions([action, .cancelAction()])
//present alert
}
func getAction(article: Article) -> UIAlertAction {
do {
let realm = try Realm()
let articleInRealm = realm.objects(Article.self).filter("id == %#", article.id)
if articleInRealm.isInvalidated {
return UIAlertAction(title: "Save for later", style: .default, handler: {(_) in
self.saveArticle(article: article)
})
} else {
if articleInRealm.count == 0 {
return UIAlertAction(title: "Save for later", style: .default, handler: {(_) in
self.saveArticle(article: article)
})
} else {
return UIAlertAction(title: "Remove saved article", style: .destructive, handler: {(_) in
self.deleteArticle(article: articleInRealm)
})
}
}
} catch {
Log("Err getting article: \(error.localizedDescription)")
return UIAlertAction(title: "Save for later", style: .default, handler: {(_) in
self.saveArticle(article: article)
})
}
}
func saveArticle(article: Article) {
do {
let realm = try Realm()
try realm.write {
realm.add(article, update: .modified)
}
} catch {
Log("Err saving article: \(error.localizedDescription)")
}
}
func deleteArticle(article: Results<Article>) {
do {
let realm = try Realm()
try realm.write {
realm.delete(article)
}
} catch {
Log("Err saving article: \(error.localizedDescription)")
}
}
//At SavedVC, which is another VC in the tabBarController.
//ArticlesVC is in tab1 and SavedVC is in tab2
var notificationToken: NotificationToken? = nil
var articles: Results<Article>?
override func viewDidLoad() {
super.viewDidLoad()
subscribeToRealmNotifications()
}
fileprivate func subscribeToRealmNotifications() {
let realm = try! Realm()
let results = realm.objects(Article.self)
notificationToken = results.observe { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
self?.articles = results
tableView.reloadData()
case .update(_, _, _, _):
tableView.reloadData()
case .error(let error):
fatalError("Realm notif: \(error.localizedDescription)")
}
}
}
My guess is that because I have deleted the articles in SavedVC but ArticlesVC is still pointing to those Realm objects which no longer exist and therefore crashed.
I have read multiple SO posts that suggest to do a check on obj.isInvalidated, which I did and have included a condition for it. But this still crashes.
Other attempts includes:
using realm.create() instead of realm.add()
Here's the issue: You've saving an unmanaged object (article) into realm
func getAction(article: Article)
...
self.saveArticle(article: article)
which then makes it a managed object.
If it's deleted from Realm,
realm.delete(article)
it will also be removed from the results object that contains it
articles[indexPath.row]
Which will then make your app crash as it's scrolling as the dataSource and tableView are out of sync.
I have a feeling you don't want it to actually be removed from the master list of articles.
My suggestion is to save any articles you want to read later by their id which will have no effect on the master list of articles as they are added and removed from realm.
In other words your master list of articles stays the same but in Realm, you have a table of id's which are strings for example. So when you want to track what they want to read
self.saveArticleId(article: article.id)
and then when it's time to delete it from their saved reading list
self.deleteArticleBy(articleId: artical.id)
My current setup: TabViewController that is connected to two TableViewControllers embedded in navigation controllers. I am able to update the tableview no problem, on app load and when switching back between the different tabs.
My problem comes when I open another tableviewcontroller to edit. When I hit save from this view controller, it updates the data and saves everything just fine however, My tableView will not update no matter what I try unless I switch between the different tabs or close and reopen the app.
I have tried using delegates to trigger the update, I have tried use NSNotificationCenter, and no dice.
Has anybody else had this issue?
Here is my save function:
func saveDose(completion: (_ finished: Bool) -> ()) {
if searchName == "" {
guard let managedContext = appDelegate?.persistentContainer.viewContext else { return }
let dose = Dose(context: managedContext)
let doseNumberString = numberDosesText.text
let doseNumberInt = Int64(doseNumberString!) ?? 0
dose.name = nameText.text
dose.script = scriptText.text
dose.dosage = dosageText.text
// dose.doseInterval = doseInterval
dose.firstDose = datePicker.date
dose.numberDoses = doseNumberInt
dose.doseReminder = remindersSwitch.isOn
do {
try managedContext.save()
print("Data Saved")
completion(true)
} catch {
print("Failed to save data: ", error.localizedDescription)
completion(false)
}
} else {
update(name:searchName, firstDose: searchDate)
completion(true)
}
}
And here is where I call it and load back to my other tableview.
#IBAction func saveBtnPressed(_ sender: UIBarButtonItem) {
saveDose { (done) in
if done {
print("We need to return now")
navigationController?.popViewController(animated: true)
self.dismiss(animated: true, completion: nil)
} else {
print("Try again")
}
}
}
And here is where I reload my tableview when the view appears
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("View has been loaded back from other view")
self.fetchData()
self.tableView.reloadData()
print("View will appear")
}
And here is my fetchData and loadData functions
func fetchData() {
loadData { (done) in
if done {
setEmptyView(Array: logArray.count)
}
}
}
func loadData(completion: (_ complete: Bool) -> ()) {
guard let managedContext = appDelegate?.persistentContainer.viewContext else { return }
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Log")
do {
logArray = try managedContext.fetch(request) as! [Log]
print("Data Fetched No Issues")
completion(true)
} catch {
print("Unable to fetch data: ", error.localizedDescription)
completion(false)
}
}
Again I have tried delegates and have used this code in other applications that have worked fine. Is it something to do with the tab bar controller?
The data is obviously saving fine, I just can't get the tableView to update properly.
It seems like it is not calling the viewWillAppear function after the save. I have tried using delegates as well to force the update of the tableview but nothing has been working.
Basically you need to call tableView.reloadData(). after you have done saving.
Moreover you can use beginUpdates, endUpdates on tableView to perform animations on specific rows. for more information on this you may refer to This Link
Reload your tableview in custom delegates method your problem will be solved.
I use firebase in swift, I call the listener in viewDidAppear: and as it is written in the docs it should't download the same data again but now every time the view appears the same data will be displayed.
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(true)
getData()
self.messagesRef.keepSynced(true)
}
func getData(){
messagesRef.observeEventType(.ChildAdded) { (mesData: FDataSnapshot!) -> Void in
let messageDict = mesData.value as! NSDictionary
let key = mesData.key
self.likedRef = self.ref.childByAppendingPath("likedMessages/\(currentUser.uid)/\(key)")
self.likedRef.observeSingleEventOfType(.Value) { (likedFd: FDataSnapshot!) -> Void in
if likedFd.value is NSNull{
let liked = false
let message = Message(message: messageDict, key: key, liked: liked, topicKey: self.topic.key)
self.messages.append(message)
self.messages.sortInPlace{$0.createdAt.compare($1.createdAt) == .OrderedDescending}
}
else{
let liked = likedFd.value as! Bool
let message = Message(message: messageDict, key: key, liked: liked, topicKey: self.topic.key)
self.messages.append(message)
self.messages.sortInPlace{$0.createdAt.compare($1.createdAt) == .OrderedDescending}
}
dispatch_async(dispatch_get_main_queue()) { () -> Void in
self.messagesTableView.reloadData()
}
self.likedRef.keepSynced(true)
}
print(mesData.value)
}
}
If you add the first listener to a location, Firebase will download the current data for that location and start synchronizing the changes. It will keep a copy of the data available in memory.
If you add a second listener to the location, while the first one is still active, Firebase will not re-download the data. I just quickly verified this http://jsbin.com/foduxun/edit?js,console.
If you remove the first listener before you attach the second listener, Firebase will purge its cache. So then when you attach a listener again, it will re-download the data.
If you want to keep the data available on the device even after you detached all listeners, enable disk persistence:
[Firebase defaultConfig].persistenceEnabled = YES;
I am building an app that has a "Playlist" feature. Users can create new, empty playlists and then add contents to them.
I decided to use Core Data to do this. So I did some research and created this object model:
where the Utterance entity represents an item in a playlist.
The view controller that I used to display the playlist is UITableViewController. Here is part of the class:
var playlists: [Playlists] = []
let dataContext: NSManagedObjectContext! = (UIApplication.sharedApplication().delegate as? AppDelegate)?.managedObjectContext
override func viewDidLoad() {
if dataContext != nil {
let entity = NSEntityDescription.entityForName("Playlist", inManagedObjectContext: dataContext)
let request = NSFetchRequest()
request.entity = entity
let playlists = try? dataContext.executeFetchRequest(request)
if playlists != nil {
for item in playlists! {
self.playlists.append(item as! Playlists)
print((item as! Playlists).name)
}
}
}
}
Playlists is a NSManagedObject subclass generated by Xcode. In viewDidLoad, I get all the playlists and put them in self.playlists. Also please note that the tableView are implemented correctly.
Now I am writing the action method when the user taps on the add playlist button. I want to show an alert asking the user for the name of the playlist. If he/she doesn't enter anything, failAlert will be displayed. Otherwise, I create a new Playlist object and set its name to the textfield's text and save it in the database. Here's the code:
#IBAction func addPlaylist(sender: UIBarButtonItem) {
let alert = UIAlertController(title: "新播放列表", message: "请输入播放列表的名字", preferredStyle: .Alert)
alert.addTextFieldWithConfigurationHandler({ (textField) -> Void in
textField.placeholder = "名字"
})
alert.addAction(UIAlertAction(title: "确定", style: .Default, handler: { (action) -> Void in
if alert.textFields?.first?.text == "" || alert.textFields?.first?.text == nil {
let failAlert = UIAlertController(title: "失败", message: "播放列表名不能为空", preferredStyle: .Alert)
failAlert.addAction(UIAlertAction(title: "确定", style: .Default, handler: nil))
self.presentViewController(failAlert, animated: true, completion: nil)
return
}
let newPlaylist = Playlists(entity: NSEntityDescription.entityForName("Playlist", inManagedObjectContext: self.dataContext)!, insertIntoManagedObjectContext: self.dataContext)
newPlaylist.name = alert.textFields?.first?.text
if let _ = try? self.dataContext.save() {
print("Error occured")
}
self.tableView.reloadData()
}))
self.presentViewController(alert, animated: true, completion: nil)
}
As you can see, I wrote this:
newPlaylist.name = alert.textFields?.first?.text
if let _ = try? self.dataContext.save() {
print("Error occured")
}
self.tableView.reloadData()
So if an error occurred in saving process, it would print Error occurred. When I tested the app by clicking on the add playlist button, it asked me for the name of the playlist. So I entered some random letters like ggg and unfortunately it prints Error occured! Also, the table view remained empty.
I really didn't understand why and thought that the data is not saved. But when I ran the app again, I see ggg in the table view! This is so weird! An error occurred but it saved the data successfully! Why is this? What is the error?
Edit:
A lot of the answers says that save returns a Bool. But Xcode says it does not:
That's clearly the word Void!
NSManagedObject's save: method returns a boolean:
Return Value
YES if the save succeeds, otherwise NO.
Therefore, the if let statement is not the right way to go, as the method will always return something (a boolean) even if the save succeeds, causing the statements within the if to be run.
You should use Swift's error handling capabilities with a do-catch statement:
do {
try self.dataContext.save()
} catch let error as NSError {
print("Error: \(error)")
}
More about error handling in Swift 2.0
But this only fixes your problem with the save, not the fact that the new objects aren't appearing until you restart the app. To fix that, you need to look at the way you go about reloading data.
Right now, you are calling self.tableView.reloadData(), except the code you included shows that you are fetching the objects from the database in your viewDidLoad. Given that viewDidLoad is only called when the view is first loaded, the new object you added will not be included in the self.playlists array.
You should add the new playlist to self.playlists in the addPlaylist function, right after the save:
self.playlists.append(item as! Playlists)
According to Apple document, NSManagedObjectContext:save() returns true if the save succeeds, otherwise false.
Will you see error with the following code when saving the data:
do {
try self.dataContext.save()
} catch let error as NSError {
print("Error occurred")
}