Table Refresh doubles number of Array items - ios

I have static data (3 values) coming from CloudKit, and the problem is when I refresh the UITableView, I get 6 values instead of 3 values.
I'm not sure why it doesn't refresh and throw out old data from the Array, but instead it keeps the old data and adds the same data to it Array.
Initial UITableView set up:
func getData() {
cloud.getCloudKit { (game: [Game]) in
var teamColorArray = [String]()
for item in game {
let itemColor = item.teamColor
teamColorArray.append(itemColor)
print("TCA in: \(teamColorArray)")
}
self.teamColorArray = teamColorArray
self.tableView.reloadData()
}
}
Prints: ["CC0033", "FDB927", "E3263A"]
Refresh data when UIRefreshControl pulled:
#IBAction func refreshData(_ sender: Any) {
self.teamColorArray.removeAll()
getData()
self.refreshControl?.endRefreshing()
}
Prints: ["CC0033", "FDB927", "E3263A", "CC0033", "FDB927", "E3263A"]
I think I have it narrowed down to somehow game in the function getData() is incremented to a count of 6 items. I'm not sure why it wouldn't always stay at 3 items if it were pulling all new data from CloudKit, but maybe I'm not understanding that calling a completion handler doubles the values and maybe I need to removeAll inside of that? I'm just really not sure
Does anyone have anything they see I'm doing wrong, or anything they'd do to fix my code?
Thanks!

Might have to do with your async call to cloudkit. I'm not too familiar with refresh control but here is a way to maybe solve your problem and also make your code a little cleaner.
func getData(_ completion: () -> ()) {
teamColorArray.removeAll()
cloud.getCloudKit { [weak self] (game: [Game]) in
guard let unwrappedSelf = self else { return }
var updatedColorArray = [String]()
game.forEach { updatedColorArray.append($0.teamColor) }
unwrappedSelf.teamColorArray = updatedColorArray
completion()
}
}
now when you call getData it would look like this
getData {
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadData()
self?.refreshControl?.endRefreshing()
}
}
you add weak self to remove the possibility of retain cycles
make sure your updating UI from the main thread
Call reloadData and endRefreshing when you know the array has been set properly
I put teamColorArray.removeAll() inside the getData function since it seems like you will need to call it everytime and it saves you from having to add it before the function everytime.

Related

How retrieve snapshot count using Swift Code

I am new to swift and developing my first application. In my application there is a collection view on which I want to display images. These images are retrieved from Firebase Database.
The structure for Firebase Database is as follows
{
"CHILD1" : {
"URL1" : "https://Child1_url1.com",
"URL2" : "https://Child1_url2.com",
"URL3" : "https://Child1_url3.com"
}
"CHILD2" : {
"URL1" : "https://Child2_url1.com",
"URL2" : "https://Child2_url2.com",
}
"CHILD3" : {
"URL1" : "https://Child3_url1.com",
}
}
To retrieve urls I am using below code
override func viewDidLoad() {
super.viewDidLoad()
checkSectionCount()
}
func checkSectionCount(){
Database.database().reference().observe(.value) { (snapShot: DataSnapshot) in
let snapDict = snapShot.value as? NSDictionary
let snapCnt = String(snapDict!.count)
print("snapCnt --> \(snapCnt)")// This prints the number of child counts correctly
}
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return snapCnt //I am unable to refer the snapCnt to set the number of section dynamically here
}
As indicated in above code, I am unable to refer snapCnt out of func checkSectionCount. I also tried to return this value as function parameter however swift is unable to identify snapCnt out of
Database.database().reference(){ Code
}
Not sure what I am missing here. Appreciate if anyone can guide me through this on how to dynamically set the section and row numbers for collection view based on the details available in Firebase Database.
Data is loaded from Firebase asynchronously, since it may take some time for it to come back from the server. Instead of blocking your code (and making the app unresponsive), the main code continues to run. Then when the code comes back from the server, the completion handler is called.
It's easiest to see this if you change your code and add some logging statements:
print("Before starting to get data")
Database.database().reference().observe(.value) { (snapShot: DataSnapshot) in
print("Got data")
}
print("After starting to get data")
When you run this code, it prints:
Before starting to get data
After starting to get data
Got data
This is probably not the order that you expected, although it is the normal and documented behavior.
For this reason any code that needs the data from the database must be inside the completion handler, or be called from there.
For some more examples of how to deal with this, see:
Why isn't my function that pulls information from the database working?
Finish asynchronous task in Firebase with Swift
How to structure code to deal with asynchronous Firebase snapshot?
Asynchronous Swift call into array
Swift Function returning a value from asynchronous firebase call
Alright, with help of links shared by #Frank i could resolve the issue. Below is the approach i took, which worked for me.
Step1: Created func with escaping completion handler
func getServiceTypeCount(completion: #escaping (Int) -> Int){
dbRef.child("Services").observeSingleEvent(of: .value) { (serviceDataSnapshot) in
let snapShot = serviceDataSnapshot.value as? [String: Any] ?? [:]
let serviceTypeCount = completion(Int(snapShot.count))
}
}
Step2: Called this function in viewDidLoad()
getServiceTypeCount { serviceTypeCount in
self.finalServiceTypeCount = serviceTypeCount
DispatchQueue.main.async {
self.newClassesTableVw.reloadData()
}
return self.finalServiceTypeCount
}
Note that i have added DispatchQueue.main.async to reload the table. That was to refresh the table with proper count value
Step3: Added the count value to display sections
func numberOfSections(in tableView: UITableView) -> Int {
return self.finalServiceTypeCount
}
Hope this helps, Thanks.
Regards.

UITableview does not respond while loading data from firebase

My Tableview does not respond to any touches when it starts loading data from firebase. (It already shows the cells, but does not react) After a while it does the scrolling you tried to do when the tableview wasn't reacting.
var filteredData = [archivCellStruct]()
func firData() {
filteredData.removeAll()
var databaseRef : DatabaseReference!
databaseRef = Database.database().reference()
databaseRef.child("Aufträge").child("Archiv").queryOrderedByKey().observe(.childAdded, with:{
snapshot in
let snap = snapshot.value as? NSDictionary
//extracting the data and appending it to an Array
self.filteredData.append(//myData)
self.tableView.reloadData()
})
}
So it is working for smaller amounts of data (tableview rows) but with big delay(and I am already filtering the data to limit the data displayed in the tableView).
I think it has something to do with the tableView.reloadData() (maybe userinteraction is disabled while reloading?)
everytime you have to reload your tableview asynchronously -
var filteredData = [archivCellStruct]()
func firData() {
filteredData.removeAll()
var databaseRef : DatabaseReference!
databaseRef = Database.database().reference()
databaseRef.child("Aufträge").child("Archiv").queryOrderedByKey().observe(.childAdded, with:{
snapshot in
let snap = snapshot.value as? NSDictionary
//extracting the data and appending it to an Array
self.filteredData.append(//myData)
DispatchQueue.main.async { //change this in you code
self.tableView.reloadData()
}
})
}
With the firebase you can go manual way:
UseCase:
struct UseCaseName {
struct Request {}
struct Response {
let firebaseCallbackData: [FirebaseModelType]
}
struct ViewModel {
let data: [DisplayType]
}
}
ViewController:
var filteredData: [DisplayType]! = []
override func viewWillAppear() {
// this one can make you trouble, adjust the observation logic to whatever you need. `WillAppear` can fire multiple times during the view lifecycle
super.viewWillAppear()
interractor?.observe()
}
func showData(viewModel: Scene.Usecase.ViewModel) {
// this is where the different approach begins
tableView.beginUpdates()
var indexPaths = [NSIndexPath]()
for row in (filteredData.count..<(FilteredData.count + viewModel.data.count)) {
indexPaths.append(NSIndexPath(forRow: row, inSection: 0))
}
filteredData += viewModel.data
tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic)
tableView.endUpdates()
}
Interractor:
func observe(request: Scene.UseCase.Request) { /* signup for updates with observe(_ eventType: DataEventType, with block: #escaping (DataSnapshot) -> Void) -> UInt */
callback is something like { DataSnapshot in presenter.presentData(response: Scene.Usecase.Response())
}
Presenter:
func presentData(response: Scene.UseCase.Response) {
/* format for representation */
DispatchQueue.main.async {
controller.present(viewModel: Scene.UseCase.ViewModel())
}
}
Sorry for the separation of the flow, I've got addicted to this way.
Also I'm assuming, that the data in the firebase is not modified, but added (because of observe(.childAdded, part. If I'm wrong, please edit your question to reflect that. Another assumption is that you have the single section. Don't forget to change the inSection: 0 to the proper section. I'm to lazy and SO isn't that friendly for mobile devices
This way only appends new values sent by Firebase and works faster
EDIT: on another answer.
DispatchQueue.main.async { //change this in you code
self.tableView.reloadData()
}
Sometimes it's not good to reload all items. It depends on the count however. If my assumptions are correct, it'll be better to update separate cells without reloading the whole table on change. Quick example: something like a chat with the Firebase "backend". explanation: adding a single row will work faster then reloadData() anyway, but the difference is heavy only when you call those methods often. With the chat example, the difference may be huge enough in case the chat is spammy to optimize the UITableView reload behaviour in your controller.
Also it'll be nice to see the code, which affects the methods. It maybe a threading issue, like the John Ayers told in the comments. Where do you call func firData()?

Why do I have to add sleep() when I am trying to use threads?

I am trying to parse an XML file with a thread. However, the XML is large and I am storing over 20,000 items into my array. I thought of using a thread to help the parser parse the XML file as the table data loads. However, it seems like using a thread doesn't work. I still have to insert two sleep() in order for it work a little bit.
How can I parse my XML file and load data into my tableview simultaneously?
let hr115XMLParser = HR115XMLParser()
let billXMLParser = BillXMLParser()
var viewBillItems: [BillModel] = []
private var billItems: [BillModel]?
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.global(qos: .userInteractive).async {
self.hr115XMLParser.parseFeed(url: "https://www.gpo.gov/smap/bulkdata/BILLSTATUS/115hr/sitemap.xml")
DispatchQueue.main.async {
self.fetchData()
self.tableView.reloadData()
}
}
}
private func fetchData()
{
sleep(20)
for index in 0..<self.hr115XMLParser.billItems.count {
print()
print("NEW ITEM \(index)")
print()
self.billXMLParser.parseFeed(url: self.hr115XMLParser.billItems[index])
sleep(5)
print(self.billXMLParser.billItemsArray[index].billtitle)
print(self.billXMLParser.billItemsArray[index].billfullName)
print(self.self.billXMLParser.billItemsArray[index].billURL)
print()
print("END ITEM")
print()
var newViewBillItem = BillModel()
newViewBillItem.billtitle = self.billXMLParser.billItemsArray[index].billtitle
newViewBillItem.billfullName = self.billXMLParser.billItemsArray[index].billfullName
newViewBillItem.billURL = self.billXMLParser.billItemsArray[index].billURL
self.viewBillItems.append(newViewBillItem)
}
}
parseFeed is an async functions than goes to another thread. This means that fetchData() will be called before the parseFeed function has finished.
In order to avoid this, maybe (i'm don't know HR115XMLParser to say it for certain) it is possible to use a closure.
self.hr115XMLParser.parseFeed(url: "https://www.gpo.gov/smap/bulkdata/BILLSTATUS/115hr/sitemap.xml") {
DispatchQueue.main.async {
self.fetchData()
self.tableView.reloadData()
}
}

Extract array from closure before prepare for segue - Swift

I am building arrays via a for in loop inside a closure, then passing these arrays through a segue. I am able to print the contents of the arrays from within the closure, but when I access the array from outside, it is always empty.
I have tried to use .reloadData() to update the arrays outside of the closure, but I can't seem to get it quite right. Does anyone know if this is the correct approach?
Here is my code:
var distances: [Double]
var journeyTimes: [Double]
directions.calculate { response, error in
if let route = response?.routes.first {
print("Distance: \(route.distance/1000) km, ETA: \(route.expectedTravelTime/60) mins")
self.distances.append(route.distance/1000)
self.journeyTimes.append(route.expectedTravelTime/60)
print(self.distances)
print(self.journeyTimes)
} else {
print("Error!")
} updateArrays()
}
func updateArrays() {
self.collectionView.reloadData()
}
The print statements above produce results, and my prepare for segue carries distances and journeyTimes through, but they are always empty when I try to access them.
I think I need to update the distances and journeyTimes arrays before prepare for segue, outside of the closure, but I don't know how to do it.
Grateful for any help on this
Thanks
One way to address this is by using a Dispatch Group. Create a new constant:
let myDispatchGroup = DispatchGroup()
var distances: [Double]
var journeyTimes: [Double]
Just before you make your network call add:
myDispatchGroup.enter()
Make sure you add it before the asynch network call begins.
After your arrays have been fully updated and your are about to exit your Asynchronous network call add:
self.myDispatchGroup.leave()
Only after the number of enters and leaves on the myDispatchGroup are equal will this function be called. Add it just before you perform your segue
func yourFunctionThatCallsSegue() {
myDispatchGroup.notify(queue: DispatchQueue.main) {
let destinationViewController = segue.destination as! YourDestinationViewController
destinationViewController.distances = self.distances // you are in a dispatch group closure
destinationViewController.journeyTimes = self.journeyTimes
performSegue(withIdentifier: "Your Identifier", sender: AnyClass.self)
}
}

Swift iOS -How To Reload TableView Outside Of Firebase Observer .childAdded to Filter Out Duplicate Values?

I have a tabBar controller with 2 tabs: tabA which contains ClassA and tabB which contains ClassB. I send data to Firebase Database in tabA/ClassA and I observe the Database in tabB/ClassB where I retrieve it and add it to a tableView. Inside the tableView's cell I show the number of sneakers that are currently inside the database.
I know the difference between .observeSingleEvent( .value) vs .observe( .childAdded). I need live updates because while the data is getting sent in tabA, if I switch to tabB, I want to to see the new data get added to the tableView once tabA/ClassA is finished.
In ClassB I have my observer in viewWillAppear. I put it inside a pullDataFromFirebase() function and every time the view appears the function runs. I also have Notification observer that listens for the data to be sent in tabA/ClassA so that it will update the tableView. The notification event runs pullDataFromFirebase() again
In ClassA, inside the callback of the call to Firebase I have the Notification post to run the pullDataFromFirebase() function in ClassB.
The issue I'm running into is if I'm in tabB while the new data is updating, when it completes, the cell that displays the data has a count and the count is thrown off. I debugged it and the the sneakerModels array that holds the data is sometimes duplicating and triplicating the newly added data.
For example if I am in Class B and there are 2 pairs of sneakers in the database, the pullDataFromFirebase() func will run, and the tableView cell will show "You have 2 pairs of sneakers"
What was happening was if I switched to tabA/ClassA, then added 1 pair of sneakers, while it's updating I switched to tabB/ClassB, the cell would still say "You have 2 pairs of sneakers" but then once it updated the cell would say "You have 5 pairs of sneakers" and 5 cells would appear? If I switched tabs and came back it would correctly show "You have 3 pairs of sneakers" and the correct amount of cells.
That's where the Notification came in. Once I added that if I went through the same process and started with 2 sneakers the cell would say "You have 2 pairs of sneakers", I go to tabA, add another pair, switch back to tabB and still see "You have 2 pairs of sneakers". Once the data was sent the cell would briefly show "You have 5 pairs of sneakers" and show 5 cells, then it would correctly update to "You have 3 pairs of sneakers" and the correct amount of cells (I didn't have to switch tabs).
The Notification seemed to work but there was that brief incorrect moment.
I did some research and the most I could find were some posts that said I need to use a semaphore but apparently from several ppl who left comments below they said semaphores aren't meant to be used asynchronously. I had to update my question to exclude the semaphore reference.
Right now I'm running tableView.reloadData() in the completion handler of pullDataFromFirebase().
How do I reload the tableView outside of the observer once it's finished to prevent the duplicate values?
Model:
class SneakerModel{
var sneakerName:String?
}
tabB/ClassB:
ClassB: UIViewController, UITableViewDataSource, UITableViewDelegate{
var sneakerModels[SneakerModel]
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(pullDataFromFirebase), name: NSNotification.Name(rawValue: "pullFirebaseData"), object: nil)
}
override func viewWillAppear(_ animated: Bool){
super.viewWillAppear(animated)
pullDataFromFirebase()
}
func pullDataFromFirebase(){
sneakerRef?.observe( .childAdded, with: {
(snapshot) in
if let dict = snapshot.value as? [String:Any]{
let sneakerName = dict["sneakerName"] as? String
let sneakerModel = SneakerModel()
sneakerModel.sneakerName = sneakerName
self.sneakerModels.append(sneakerModel)
//firebase runs on main queue
self.tableView.reloadData()
}
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sneakerModels.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SneakerCell", for: indexPath) as! SneakerCell
let name = sneakerModels[indePath.row]
//I do something else with the sneakerName and how pairs of each I have
cell.sneakerCount = "You have \(sneakerModels.count) pairs of sneakers"
return cell
}
}
}
tabA/ClassA:
ClassA : UIViewController{
#IBAction fileprivate func postTapped(_ sender: UIButton) {
dict = [String:Any]()
dict.updateValue("Adidas", forKey: "sneakerName")
sneakerRef.?.updateChildValues(dict, withCompletionBlock: {
(error, ref) in
//1. show alert everything was successful
//2. post notification to ClassB to update tableView
NotificationCenter.default.post(name: Notification.Name(rawValue: "pullFirebaseData"), object: nil)
}
}
}
In other parts of my app I use a filterDuplicates method that I added as an extension to an Array to filter out duplicate elements. I got it from filter array duplicates:
extension Array {
func filterDuplicates(_ includeElement: #escaping (_ lhs:Element, _ rhs:Element) -> Bool) -> [Element]{
var results = [Element]()
forEach { (element) in
let existingElements = results.filter {
return includeElement(element, $0)
}
if existingElements.count == 0 {
results.append(element)
}
}
return results
}
}
I couldn't find anything particular on SO to my situation so I used the filterDuplicates method which was very convenient.
In my original code I have a date property that I should've added to the question. Any way I'm adding it here and that date property is what I need to use inside the filterDuplicates method to solve my problem:
Model:
class SneakerModel{
var sneakerName:String?
var dateInSecs: NSNumber?
}
Inside tabA/ClassA there is no need to use the Notification inside the Firebase callback however add the dateInSecs to the dict.
tabA/ClassA:
ClassA : UIViewController{
#IBAction fileprivate func postTapped(_ sender: UIButton) {
//you must add this or whichever date formatter your using
let dateInSecs:NSNumber? = Date().timeIntervalSince1970 as NSNumber?
dict = [String:Any]()
dict.updateValue("Adidas", forKey: "sneakerName")
dict.updateValue(dateInSecs!, forKey: "dateInSecs")//you must add this
sneakerRef.?.updateChildValues(dict, withCompletionBlock: {
(error, ref) in
// 1. show alert everything was successful
// 2. no need to use the Notification so I removed it
}
}
}
And in tabB/ClassB inside the completion handler of the Firebase observer in the pullDataFromFirebase() function I used the filterDuplicates method to filter out the duplicate elements that were showing up.
tabB/ClassB:
func pullDataFromFirebase(){
sneakerRef?.observe( .childAdded, with: {
(snapshot) in
if let dict = snapshot.value as? [String:Any]{
let sneakerName = dict["sneakerName"] as? String
let sneakerModel = SneakerModel()
sneakerModel.sneakerName = sneakerName
self.sneakerModels.append(sneakerModel)
// use the filterDuplicates method here
self.sneakerModels = self.sneakerModels.filterDuplicates{$0.dateInSecs == $1.dateInSecs}
self.tableView.reloadData()
}
})
}
Basically the filterDuplicates method loops through the sneakerModels array comparing each element to the dateInSecs and when it finds them it excludes the copies. I then reinitialize the sneakerModels with the results and everything is well.
Also take note that there isn't any need for the Notification observer inside ClassB's viewDidLoad so I removed it.

Resources