How can I prevent TableView cells from duplicating? Swift - ios

So I am building a notes app and have tried everything but can not figure this issue out. I have 3 UIViewController's. When you click a button on the first UIViewController, it shoots you over to the third one which consist of a UITextView, UITextField and a Static UILabel which gets updated with some information. When you fill out these fields and tap the back button it brings you to the second view controller which is a table that gets updated with this information.
The issue is: when I tap the UITableViewCell it loads the information back to the third view controller so the user can edit his notes but when I come back to the UITableView it creates a brand new cell instead of just updating the old one.
If I could just update my array with the same object I sent back to be edited by the user I think this issue would be solved but I have no idea how to do this. Thanks for the help!
VC2 - this is how I am sending my data from the tableView to the textView Back
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let nextView = segue.destinationViewController as! TextViewController
guard let indexPath = self.tableView.indexPathForSelectedRow else {
print("Didnt work")
return
}
let dataToSendBackToBeEdited = textViewObjectData[indexPath.row]
print(dataToSendBackToBeEdited.dreamText)
print(dataToSendBackToBeEdited.dreamTitle)
print(dataToSendBackToBeEdited.dreamType)
print(dataToSendBackToBeEdited.description)
print(dataToSendBackToBeEdited)
nextView.objectSentFromTableViewCell = dataToSendBackToBeEdited
}
This is how I am saving the information the the user taps back to go to the tableView
func determineSave() {
guard var savedDreamArray = NSKeyedUnarchiver.unarchiveObjectWithFile(TextViewController.pathToArchieve.ArchiveURL.path!) as? [Dream] else {
//First dream object saved
let dreamObject = Dream(dreamTitle: titleForDream.text!, dreamType: typeOfDreamLabel.text!, dreamText: textView.text, currentDate: NSDate())
dreamArray.append(dreamObject)
NSKeyedArchiver.archiveRootObject(dreamArray, toFile: pathToArchieve.ArchiveURL.path!)
return
}
//print(savedDreamArray.count)
//print(savedDreamArray.first!.dreamTitle)
let dreamObject = Dream(dreamTitle: titleForDream.text!, dreamType: typeOfDreamLabel.text!, dreamText: textView.text!, currentDate: NSDate())
savedDreamArray.append(dreamObject)
NSKeyedArchiver.archiveRootObject(savedDreamArray, toFile: pathToArchieve.ArchiveURL.path!)
}

I was having this issue as well. Came in here, and got the answer. The array!
I was appending the array as well, which apparently was causing duplicate cells to appear.
I just reset my array back to empty before I retrieved the data again and reloaded the table.
I'm using Firebase so my code looks like this:
DataService.instance.dateProfileRef.observeEventType(FIRDataEventType.Value) { (snapshot: FIRDataSnapshot)
in
//have to clear the array here first before reloading the data. otherwise you get duplicates
self.posts = [] //added this line
if let snapshot = snapshot.children.allObjects as? [FIRDataSnapshot] {
for snap in snapshot {
let snapUserID = (snap.childSnapshotForPath("userID").value!)
if snapUserID as? String == USER_ID {
if let profileDict = snap.value as? Dictionary<String, AnyObject> {
let key = snap.key
let post = Post(postKey: key, postData: profileDict)
self.posts.append(post)

You do a .append() on your array, this will add a new cell.
You must find the index of the object you want to update and then assign it:
array[index] = newObject

Related

How to know when all tasks are completed after HTTPRequest (Swift)

I am currently facing an issue that has been bothering me for days. I have tried several solutions but don't seem to be able to fix the issue. Let me illustrate what is going on.
I have a feed with posts in a UITableView. As soon as the user selects one of the cells, he is shown a detailed view of this post in a new UIViewController. In there, I set up all views and call a function loadPostDetails() in the viewDidLoad() method. As soon as the view shows up, a custom loading indicator is presented which is set to disappear (hide) when the details are loaded and then the UITableView of this UIViewController (let's call it PostController) is shown.
In my loadPostDetails()function, I make a HTTPRequest which acquires the JSON data I need. This returns three kinds of info: the post details themselves, the likes and the comments. I need to handle each of these three elements before I reload the UITableView and show it. Currently, I do it like this:
HTTP.retrievePostDetails(postID: postID, authRequired: true) { (error, postDetails) in
if(error != nil) {
self.navigationController?.popViewController(animated: true)
} else {
if let postDetails = postDetails, let postInfo = postDetails["postInfoRows"] as? [[String: Any]], let postLikesCount = postDetails["postLikesCount"] as? Int, let postLikesRows = postDetails["postLikesRows"] as? [[String: Any]], let postCommentsRows = postDetails["postCommentsRows"] as? [[String: Any]] {
DispatchQueue.main.async {
let tableFooterView = self.postTableView.tableFooterView as! tableFooterView
if let postTitle = postInfo[0]["postTitle"] as? String, let postText = postInfo[0]["postText"] as? String {
self.postTitleLabel.text = postTitle
self.postTextLabel.text = postText
}
for (index, postCommentRow) in postCommentsRows.enumerated() {
tableFooterView.postComments.append(Comment(userID: postCommentRow["userID"] as! Int, userProfilePicURL: postCommentRow["userProfilePicURL"] as! String, userDisplayName: postCommentRow["userDisplayName"] as! String, commentTimeStamp: postCommentRow["commentTimeStamp"] as! TimeInterval, commentText: postCommentRow["commentText"] as! String))
}
var likeDisplayNames = [String]()
for postLikeRow in postLikesRows {
likeDisplayNames.insert(postLikeRow["userDisplayName"] as! String, at: 0)
}
if(postLikesCount > 2) {
tableFooterView.likesLabel.text = "\(likeDisplayNames[0]), \(likeDisplayNames[1]) and \(postLikesCount - 2) others"
} else {
tableFooterView.likesLabel.text = "\(postLikesCount) likes"
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
self.screenDotsLoader.isHidden = true
self.screenDotsLoader.stopAnimating()
self.postTableView.isHidden = false
self.postTableView.reloadData()
})
}
}
}
Note: I add more text to UILabels, like the date and the profile picture of the user, but I have removed a couple of lines to make it more readible and because the extra code is irrelevant for this problem.
Now, as you might already see, I call the reload stuff 1 second later, so in 95% of the cases it works just fine (but still, it is not perfect, as it is a "hack"). In the other 5%, the layout can't figure out the right constraints, resulting in a very bad layout.
I have in the last days tried to play with DispatchGroups(), but I couldn't figure out how to do it. I am in fact trying to know when all tasks have been performed, so when all UILabels have been updated, all UIImageViews have been updated etc. Only then, I want to reload the UITableView.
I was hoping someone could point me in the right direction so I can enhance my user experience a bit more. Thank you!
DispatchGroups is used when you do a bunch of asynchronous tasks together , and need to be notified upon finish of all , but currently you don't do this as when you receive the response , all are inside the main thread which is synchronous ( serial ) which means all the stuff before reloading the table will happen before it's reload , so you're free to use dispatch after if this will make sense to your UX

Displaying an array in a table view before the user see it

I am working on pulling in data from firebase and displaying it in a table view. I populate an array with the data received and use the array to fill the tableview, however the data is not being displayed.
I realized the issue is that the tableview is loading before the array gets populated. I've attempted putting a reloadData in viewdidload but this makes the data pop in after it is displayed and does not look clean.
How can I get the tableview to load the data before the view appears so that the transition is smooth?
This code is inside my viewdidload method :
let categoryRef = Database.database().reference().child("category/\(category)")
let user = Auth.auth().currentUser
if let user = user {
let uid = user.uid
categoryRef.observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.hasChildren(){
for child in (snapshot.value as? NSDictionary)! {
if let object = child.value as? [String:AnyObject]{
let name = object["name"] as! String
self.nameArray.append(name)
self.categoryDict["\(name)"] = child.key as! String
}
}
}
})
}
}
Try reloading the data just after you appended all the children:
let categoryRef = Database.database().reference().child("category/\(category)")
let user = Auth.auth().currentUser
if let user = user {
let uid = user.uid
categoryRef.observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.hasChildren(){
for child in (snapshot.value as? NSDictionary)! {
if let object = child.value as? [String:AnyObject]{
let name = object["name"] as! String
self.nameArray.append(name)
self.categoryDict["\(name)"] = child.key as! String
}
}
self.tableView.reloadData()
}
})
}
}
Call func insertSections(IndexSet, with: UITableViewRowAnimation) instead but remember to wrap it inside a call to beginUpdates and endUpdates. Also make sure your updating your UI on the main queue.
because
categoryRef.observeSingleEvent
is asynchronous, you should reload the table view after the block finishes, the way #barbarity proposes.
If you want a smoother transition, start this call
let categoryRef = Database.database().reference().child("category/\(category)")
before presenting your viewController, set the array and then present it.
But the practice in most of the cases with asynchronous processing is, start loading the data on viewDidLoad and have a UI loading mechanism (spinner)
I guess you could make other views which will make you sense the feeling of 'data pop' showed after the data is ready.
On the extreme condition, you may try to hide the whole tableview or even show Activity Indicator on the screen, then show the tableview and hide indicator after the datas for tableview is ready (In Objective-C):
- (void)viewDidLoad {
//do init work ...
self.tableview.hidden = YES;
[self showIndicator];
[self requestDataFinish:^{
self.tableview.hidden = NO;
[self hideIndicator];
[self.tableview reload];
}];
}

How can I stop UICollectionView from showing duplicate items after Firebase update

I have two UICollection views on a page that displays data about a Room. It includes photos of the room in one UICollection View and another UICollection View which contains a list of items in that room. There's a link to edit the Room. When a user clicks on the link, they then segue to another view that let's them update it including adding additional photos.
After adding a photo, and hitting submit, in the background the photo is uploaded to Firebase storage and in the Firebase database, the record is updated to include the name of the file that was just uploaded. Meanwhile, the user is segued back to the Room view.
There's a watched on the record of the room in Firebase and when it updates, then the view is refreshed with new data. This is where the problem occurs. It appears, based on a lot of debugging that I've been doing, that the Observe method fires twice and what ends up happening, is the UICollection view that holds the images of the room will show duplicates of the last photo added.
For example, if I add one photo to the room, that photo will appear in the collection 2x. I've attempted to clear the array before the array is updated with the images, and from my analysis, it appears that the array only contains two items, despite showing three in the view. I'm not sure what is happening that would cause this?
Here's a link to the entire file, because I think it might help.
Here's the loadData() method in case this is all that's important:
func loadData() {
self.ref = Database.database().reference()
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = true
guard let userID = Auth.auth().currentUser?.uid else { return }
let buildingRef = self.ref.child("buildings").child(userID)
buildingRef.keepSynced(true)
buildingRef.child(self.selected_building as String).observe(DataEventType.value, with: { (snapshot) in
let value = snapshot.value as? NSDictionary
if ((value) != nil) {
let building_id = value?["id"] as! String
let saved_image = value?["imageName"] as! String
let user_id = userID as! String
let destination = "/images/buildings/\(userID)/\(building_id)/"
let slideShowDictionary = value?["images"] as? NSDictionary
if ((slideShowDictionary) != nil) {
self.slideShowImages = [UIImage]()
self.slideShowCollection.reloadData()
var last_value = ""
slideShowDictionary?.forEach({ (_,value) in
print("are they different? \(last_value != (value as! String))")
if (last_value != value as! String) {
print("count: \(self.slideShowImages.count)")
print("last_value \(last_value)")
print("value \(value)")
last_value = value as! String
CloudStorage.instance.downloadImage(reference: destination, image_key: value as! String, completion: { (image) in
self.slideShowImages.append(image)
self.slideShowCollection.reloadData()
})
}
})
CloudData.instance.getBuildingById(userId: user_id, buildingId: building_id, completion: { (building) in
self.title = building.buildingName as String
self.roomsCollection.reloadData()
})
}
}
})
// User is signed in.
self.getRooms()
}
I am not completely familiar with the Firebase API but if you are having issues with the observation I would suspect the following:
#IBAction func unwindToRoomsVC(segue:UIStoryboardSegue) {
loadData()
}
Triggering loadData a second time looks like it would add a second observation block. As best I can tell the .observe method probably persists the block it is given and triggers it on all changes.

After retrieving data from Firebase, how to send retrieved data from one view controller to another view controller?

This is my table created in Firebase here. I have a search button. The button action will be at first it will fetch data from firebase and then it will send it to another view controller and will show it in a table view. but main problem is that before fetching all data from Firebase
self.performSegueWithIdentifier("SearchResultPage", sender: self )
triggers and my next view controller shows a empty table view. Here was my effort here.
From this post here I think that my code is not placed well.
Write this line of code in your dataTransfer method in if block after complete for loop
self.performSegueWithIdentifier("SearchResultPage", sender: self )
Try sending the search string as the sender when you are performing the segue, for example:
self.performSegueWithIdentifier("SearchResultPage", sender: "searchString")
Then, in your prepare for segue get the destination view controller and send over the string to the new view controller. something like:
destinationViewController.searchString = sender as! String
When the segue is completed and you have come to the new view controller, you should now have a searchString value that is set, use this value to perform your query and get the relevant data to show in the new table view.
Please note that this is just one solution of many, there are other ways to achieve this.
The reason why you are not able to send data is because, you are trying to send data even before the data could actually be fetched.
Try the following where you are pushing to next view controller only after getting the data
func dataTransfer() {
let BASE_URL_HotelLocation = "https://**************.firebaseio.com/Niloy"
let ref = FIRDatabase.database().referenceFromURL(BASE_URL_HotelLocation)
ref.queryOrderedByChild("location").queryStartingAtValue("uttara").observeEventType(.Value, withBlock: { snapshot in
if let result = snapshot.children.allObjects as? [FIRDataSnapshot] {
for child in result {
let downloadURL = child.value!["image"] as! String;
self.storage.referenceForURL(downloadURL).dataWithMaxSize(25 * 1024 * 1024, completion: { (data, error) -> Void in
let downloadImage = UIImage(data: data!)
let h = Hotel()
h.name = child.value!["name"] as! String
print("Object \(count) : ",h.name)
h.deal = child.value!["deal"] as! String
h.description = child.value!["description"] as! String
h.distance = child.value!["distance"] as! String
h.latestBooking = child.value!["latestBooking"] as! String
h.location = child.value!["location"] as! String
h.image = downloadImage!
self.HotelObjectArray.append(h)
})
}
let storyboard = UIStoryboard(name:"yourStoryboardName", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier("SearchResultPageIdentifier") as! SearchResultPage
self.navigationController.pushViewController(vc, animated: true)
}
else {
print("no results")
}
})
}

Segue to a Table View Controller based on what was clicked on in a different Table View Controller

I have a table view controller that is filled with data that is being pulled from a JSON file. This table view controller segues to another table view controller that is pulling from the same JSON file. I want the information that loads into the second view controller to change based on what table cell was clicked on in the first table view controller.
For example: If my first table view controller listed states (Alabama, Alaska, Arizona, etc) and Alabama was clicked on, it would return a list of cities in Alabama. However, if Alaska is clicked on, then the second table view controller would show cities that are in Alaska instead.
I am not exactly sure how to even begin here but here is my code first table view controller didSelectRowAtIndexPath function:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var industry: Industry!
if inSearch{
industry = filteredSearch[indexPath.row]
}
else{
industry = industryOfMifi[indexPath.row]
}
performSegueWithIdentifier("IndustryPush", sender: industry)
}
And here is the code that is loading the appropriate information in the second table view controller:
func parseJSON(){
do{
let data = NSData(contentsOfURL: NSURL(string: "https://jsonblob.com/api/jsonBlob/580d0ccce4b0bcac9f837fbe")!)
let jsonResult = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers)
for anItem in jsonResult as! [Dictionary<String, AnyObject>]{
let industry = anItem["mediaIndustry"] as! String
if industry == "Interactive Media" {
let mifiIndustry = anItem["name"] as! String
print(mifiIndustry)
let mifiId = anItem["employeeId"] as! Int
let newIndustry = Name(mifiName: mifiIndustry, mifiId: mifiId)
industryOfMifi.append(newIndustry)
}
else if industry == "Newspaper" {
let mifiIndustry = anItem["name"] as! String
print(mifiIndustry)
let mifiId = anItem["employeeId"] as! Int
let newIndustry = Name(mifiName: mifiIndustry, mifiId: mifiId)
industryOfMifi.append(newIndustry)
}
else if industry == "Radio" {
let mifiIndustry = anItem["name"] as! String
print(mifiIndustry)
let mifiId = anItem["employeeId"] as! Int
let newIndustry = Name(mifiName: mifiIndustry, mifiId: mifiId)
industryOfMifi.append(newIndustry)
}
}
}
catch let error as NSError{
print(error.debugDescription)
}
}
It's a little difficult to tell what's going on here, but in essence what you want to do is pass a reference to the selected Industry value to the 2nd Table View Controller.
Here's one way to do that. First, create a class-level variable for Industry in the 2nd table VC.
class SecondTableViewController: UITableViewController {
var industry: Industry?
}
Second, make use of the prepareForSegue() method in your 1st Table VC to pass the instance of Industry to the new view controller.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "IndustryPush" {
let secondTableViewController = segue.destinationViewController as! SecondTableViewController
secondTableViewController.industry = sender as! Industry
}
}
Alternatively, you can create a reference to the selected industry in your first VC. (You set that in the didSelectRowAtIndexPath function.) And then you can pass THAT to the second VC in prepareForSegue().
From there, it's on you to figure out how to use the Industry instance to filter your JSON. It's a little hard to tell from what you've posted.
Hope that helps.

Resources