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)
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'm attempting to show a loading spinner when I'm doing some network calls when my app first starts up from being closed. These network calls usually take a very small amount of time because they are GETs on a json string and some processing on them, but if they take longer than usual, I don't want my users trying to maneuver in the app without the data they need being there. So, I'm trying to show a spinner when these calls are going on. But the spinner never shows up. I had this working before I changed a lot of stuff, and now it's not working again, and I can't for the life of me figure out why.
Here's my viewDidLoad() method in my HomeViewController, where this information is pulled from the API and loaded into CoreData.
override func viewDidLoad() {
super.viewDidLoad()
self.showSpinner(onView: self.view)
let teamsByConferenceNetworkManager = TeamsByConferenceNetworkManager()
teamsByConferenceNetworkManager.getTeamsByConference(completion: { (data, error) in
guard let data = data else {
os_log("Could not unwrap teamsByConference data in LoginViewController.viewDidLoad()", type: .debug)
self.removeSpinner()
let _ = UIAlertAction(title: "Network unavailable", style: .cancel, handler: { (alert) in
alert.isEnabled = true
})
return
}
let dataModelManager = DataModelManager.shared
DispatchQueue.main.sync {
dataModelManager.loadTeamNamesByConference(teamNamesByConferenceName: data)
dataModelManager.loadGamesFromCoreData()
}
if let _ = dataModelManager.allGames {
self.removeSpinner()
return
} else {
let gamesNetworkManager = GamesNetworkManager()
gamesNetworkManager.getGames { (data, error) in
guard let data = data else {
os_log("Could not unwrap games data in LoginViewController.viewDidLoad()", type: .debug)
self.removeSpinner()
let _ = UIAlertAction(title: "Network unavailable", style: .cancel, handler: { (alert) in
alert.isEnabled = true
})
return
}
DispatchQueue.main.sync {
dataModelManager.loadGames(gameApiResponses: data)
}
}
}
})
self.removeSpinner()
}
You need to remove this
DispatchQueue.main.sync {
dataModelManager.loadGames(gameApiResponses: data)
}
}
}
})
self.removeSpinner(). <<<<<< this line
}
as the call is asynchronous and you remove the spinner directly after you add it with self.showSpinner(onView: self.view)
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()
}
}
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")
}
I have a simple iOS application written in Swift that loads JSON data downloaded over the Internet and displays it into a UITableView. When I launch the application for the first time, everything works correctly and the table displays the correct information.
In my app, I have a button that triggers a refresh when I would like to reload the data. So I call the displayTable() method when the refresh button is tapped which downloads the data again and then calls the tableView.reloadData() method on the main thread to refresh the data but the reload does not appear to work. I manually change the data on the server and hit refresh but I still have the old data cached in my application. I can exit/kill the application and the old data stays 'stuck' forever unless I delete the app and reinstall.
I've attached the code for my ViewController below. I've spent way too many hours looking at this and I cannot seem to find why the reloadData doesn't work. Any ideas, hints would be greatly appreciated.
thanks
--Vinny
class ViewController: UITableViewController {
var tableData = []
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(animated: Bool) {
println("viewWillAppear was just called")
super.viewWillAppear(true)
self.displayTable()
}
func displayTable() {
getJSONData("http://www.example.com/data.json") { (results, resultError) in
if (resultError == nil) {
if var results = results {
dispatch_async(dispatch_get_main_queue(), {
self.tableData = [] //create out data on subsequent refreshes
self.tableData = results
self.tableView.reloadData() //This doesn't appear to be working!
})
} else {
self.displayErrorPopup()
}
} else {
self.displayErrorPopup()
}
}
}
func displayErrorPopup() {
let alertViewController = UIAlertController(title: "Error", message: "Couldn't connect to API", preferredStyle: .Alert)
let okButton = UIAlertAction(title: "OK", style: .Default, handler: nil)
let cancelButton = UIAlertAction(title: "Cancel", style: .Cancel, handler: nil)
alertViewController.addAction(okButton)
alertViewController.addAction(cancelButton)
self.presentViewController(alertViewController, animated: true, completion: nil)
}
//todo - extract this method into it's own class
func getJSONData(ttAPIURL : String, completion: (resultsArray: NSArray?, resultError: NSError?) -> ()){
let mySession = NSURLSession.sharedSession()
let url: NSURL = NSURL(string: ttAPIURL)!
let networkTask = mySession.dataTaskWithURL(url, completionHandler : {data, response, error -> Void in
var err: NSError?
if (error == nil) {
var theJSON = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &err) as NSMutableDictionary
let results : NSArray = theJSON["list"]!["times"] as NSArray
completion(resultsArray: results, resultError: error)
} else {
completion(resultsArray: nil, resultError: error)
}
})
networkTask.resume()
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableData.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("ttCell", forIndexPath: indexPath) as UITableViewCell
let timesEntry = self.tableData[indexPath.row] as NSMutableDictionary
cell.textLabel.text = (timesEntry["routeName"] as String)
return cell
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
#IBAction func refreshButtonTap(sender: AnyObject) {
self.displayTable()
}
}
Ok, so you say that reloadData is not working but that is almost certainly not your problem. To debug this you should:
Make sure reloadData is actually being called (breakpoint or println)
Check if the UITableViewDataSource methods are being called after you call reloadData (breakpoint or println)
Check the results array that you are getting back from the network request (probably println(results))
My guess is that it is failing at number 3 which means it has nothing to do with reloadData. Perhaps the shared NSURLSession has caching enabled? Try setting mySession.URLCache to nil.
Try to replace it
override func viewWillAppear(animated: Bool) {
println("viewWillAppear was just called")
super.viewWillAppear(true)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), { () -> Void in
self.displayTable()
})
}
Thanks drewag and Jirom for your answers. Great comments that were very helpful. I just found my problem and its something I should have thought about earlier. The domain which is hosting my dynamic JSON data is being protected by CloudFlare and it appears that my pages rules to disable caching for a certain directory aren't working. So all of the JSON content was being cached and that was the reason my application wouldn't show the new data.
Thanks again guys for your responses.
--Vinny
PS: Per Drewag's suggestion, I added an instance of NSURLSessionConfiguration which defines the behavior and policies to use when downloading data using an NSURLSession object, and set the URLCache property to nil to disable caching.
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
config.URLCache = nil
let mySession = NSURLSession(configuration: config)