uitableview scroll to top on data's first load - swift

I am using reloadData() to load tableview data but my table view on its first load scrolls to top when it's loading data like so ... and that's not so nice to the user experience. Does anybody of you know the solution guys to stop this behavior. Thank you for your help !
I getting users from firebase and I am loading the images like so :
func getUsersFromFirebase(){
refUsers.observeSingleEvent(of: .value, with: { (snapshot) in
let usersNbr = snapshot.childrenCount
var counter = 0
var newUsers: [User] = []
for item in snapshot.children {
counter = counter + 1
let newitem:DataSnapshot = item as! DataSnapshot
let user = User(snapshot: item as!DataSnapshot,uid:newitem.key.description)
var newWishlists:[WishList]=[] self.refProducts.child(newitem.key.description).observeSingleEvent(of : DataEventType.value, with: { (snapshot) in
for item in snapshot.children {
let newItem:DataSnapshot = item as! DataSnapshot
let newWishlist=WishList.init(snapshot: newItem)
if(newWishlist.products.count != 0 && !newWishlist.name.trimmingCharacters(in: .whitespaces).isEmpty){
newWishlists.append(newWishlist)
}
}
user.wishLists=newWishlists
if(!(user.name=="" || user.image=="" ) ){
print("user privacy 1: " + user.isPrivate.description)
if(!user.isPrivate && user.wishLists.count != 0){
if(!user.isPrivate){
newUsers.append(user)
}
}
}
self.firebaseUsers = newUsers.shuffled()
GlobalVar.isAppFirstLoad=false
GlobalVar.allUsers = self.firebaseUsers
DispatchQueue.main.async {
self.reload(tableView: self.usersTableView)
print("user tableview reloaded 1")
}
})
}
}) { (error) in
print(error.localizedDescription)
MBProgressHUD.hideAllHUDs(for: self.usersTableView, animated: true)
}
}
func reload(tableView: UITableView) {
let contentOffset = tableView.contentOffset
tableView.reloadData()
tableView.layoutIfNeeded()
tableView.setContentOffset(contentOffset, animated: false)
}
I found the trick and its the .shuffled() that cause the behavior I think the array still shuffling when the tableview is reloading the data.
So anyone of you knows how the check if the array has finished shuffling?

So if you check in your code you have flag
GlobalVar.isAppFirstLoad=false
If you think with commom sense why it exist? you should check for
if condition that contains GlobalVar.isAppFirstLoad this value as true and in its block their will be some code that will be scrolling tableView to top.
Find it and remove it.

Related

Is there any way to listen for cells that are no longer displayed in a UITableView?

I want to know which cells are not visible anymore of a UITableView so I can delete their data from the DataModel class where they are stored.
This is what I have tried but does not work. I guess because indexPathsForVisibleRows can't be used as publisher.
func addSubscriberToVisibleCells(){
var publisher = self.table_view.indexPathsForVisibleRows.publisher.receive(on: DispatchQueue.main)
print("Publisher added")
publisher.sink { pathsVisible in
guard pathsVisible != nil else{return}
self.lastIndexPathRows.forEach { lastIndex,value in
if pathsVisible.firstIndex(of: lastIndex) == nil {
self.postData.deleteDataForIndex(lastIndex)
self.lastIndexPathRows.removeValue(forKey: lastIndex)
}
}
pathsVisible.map { visibleIndexPath in
self.lastIndexPathRows[visibleIndexPath] = 1
}
}
}
self.lastIndexPathRows is a dictionary to keep track of the latest visible cells.
Implement tableView(_:didEndDisplaying:forRowAt:) in your table view delegate.

How to refresh storage reference data in tableView cached with SDWebImage?

I have a reference to a project image in Firebase, and I want to edit the photo by replacing the data in the storage reference, without changing the reference link. I am successfully doing this, and the image view is updating as well when a user selects a new image. However, when I go back in the navigation controller, my tableView still shows the same image - and when I click on the tableView item - the project image does not seem to be updated.
The only way the image and tableview data successfully update is when I delete the simulator from my computer and re-add it. Here is some of my code for how I populate the tableView - the code works great for new projects being added but the modified portion is not working - I call this function in viewwillappear to populate the tableview:
func checkForUpdates(uid: String) {
handler = db.collection("Projects").whereField("UID", isEqualTo: "\(uid)").addSnapshotListener { snapShot, error in
guard let document = snapShot else {return}
if document.count > 0 {
document.documentChanges.forEach { difference in
if difference.type == .added {
if let project = Project(dictionary: difference.document.data()) {
self.projectArray.append(project)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
else {return}
}
if difference.type == .modified {
DispatchQueue.main.async {
self.loadData(uid: uid)
self.tableView.reloadData()
}
return
}
if difference.type == .removed {return}
else {return}
}
}
else {
print("no documents to show")
}
}
}
When I segue into viewing a particular project (by clicking on tableviewcell) - here is my code
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "viewProject" {
if let vc = segue.destination as? ViewMyProject {
vc.selectedProject = self.selectedProject!
}
else {return}
}
else {return}
}
Here is my code for how I set cells within a xib file
func setProject(project: Project) {
projectName.text = project.title
projectCategory.text = project.category
projectDate.text = project.timeStamp.dateValue().dateToString(style: .short)
projectImage!.sd_setImage(with: URL.init(string: project.imageLink)) { (image, error, cacheType, url) in
if error != nil {
print ("Error setting project cell image")
return
}
else {
print("Successfuly set project cell image")
}
}
}
And here is how I load the project after clicking on the cell (I call this in viewWillAppear)
func loadProject(project: Project) {
filterProjectFeedback(project: project)
if projectImage.image == nil {projectImage.sd_setImage(with: URL.init(string: project.imageLink))}
projectDescription.text = project.description
}
Thanks so much in advance - I have a feeling it has to do with a strong reference - but I cannot make my "projectArray" variable weak
Here is a code in swift 3 to refresh cache everytime:
imgCardBack.sd_setImage(with: URL(string: objUserData.back_image!), placeholderImage:UIImage(named: "cardBack"), options: .refreshCached)
How to update image in cache when image changed on server with SDWebImage

SearchBar problem while trying to search Firestore and reload the tableview

I have a tableView and I use infinite scroll to populate firestore data with batches. Also I have a searched bar and I am trying to query firestore with the text from the text bar and then populate it in the tableview. I have 3 main problems.
When I click search thee first time I get an empty array and an empty tableview, but when I click search the second time everything seems fine.
When I finally populate the searched content I want to stop fetching new content while I am scrolling.
If I text a wrong word and press search then I get the previous search and then the "No Ingredients found" printed twice.
This is my code for searchBar:
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
guard let text = searchBar.text else {return}
searchIngredients(text: text)
self.searchBarIngredient.endEditing(true)
print("\(searchIngredients(text: text))")
}
The code for function when I click search
func searchIngredients(text: String) -> Array<Any>{
let db = Firestore.firestore()
db.collection("Ingredients").whereField("compName", arrayContains: text).getDocuments{ (querySnapshot, err) in
if let err = err {
print("\(err.localizedDescription)")
print("Test Error")
} else {
if (querySnapshot!.isEmpty == false){
self.searchedIngredientsArray = querySnapshot!.documents.compactMap({Ingredients(dictionary: $0.data())})
}else{
print("No Ingredients found")
}
}
}
self.tableView.reloadData()
ingredientsArray = searchedIngredientsArray
return ingredientsArray
}
Finally the code for scrolling
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let off = scrollView.contentOffset.y
let off1 = scrollView.contentSize.height
if off > off1 - scrollView.frame.height * leadingScreensForBatching{
if !fetchMoreIngredients && !reachEnd{
beginBatchFetch()
}
}
}
I don't write the beginBatchFetch() cause its working fine and I don't think is relevant.
Thanks in advance.
The issue in your question is that Firestore is asynchronous.
It takes time for Firestore to return documents you've requested and that data will only be valid within the closure calling the function. The code outside the closure will execute way before the data is available within the closure.
So here's what's going on.
func searchIngredients(text: String) -> Array<Any>{
let db = Firestore.firestore()
db.collection("Ingredients").whereField("compName", arrayContains: text).getDocuments{ (querySnapshot, err) in
//the data has returned from firebase and is valid
}
//the code below here will execute *before* the code in the above closure
self.tableView.reloadData()
ingredientsArray = searchedIngredientsArray
return ingredientsArray
}
what's happening is the tableView is being refreshed before there's any data in the array.
You're also returning the ingredientsArray before it's populated. More importantly, attempting to return a value from an asynchronous function can (and should) generally be avoided.
The fix is to handle the data within the closure
class ViewController: NSViewController {
var ingredientArray = [String]()
func searchIngredients(text: String) {
let db = Firestore.firestore()
db.collection("Ingredients").whereField("compName", arrayContains: text).getDocuments{ (querySnapshot, err) in
//the data has returned from firebase and is valid
//populate the class var array with data from firebase
// self.ingredientArray.append(some string)
//refresh the tableview
}
}
Note that the searchIngredients function should not return a value - nor does it need to

Swift Firebase: UIRefreshControl with .childAdded

Ok, pls pardon me if this might be a very elementary question, but how do I refresh my tableView with only new items added to Firebase?
I am trying to implement a 'pull to refresh' for my tableView and populate my tableView with new items that have been added to Firebase. At viewDidLoad, I call .observeSingleEvent(.value) to display the table.
At the refresh function, I call .observe(.childAdded). However doing so will make the app consistently listen for things added, making my app consistently reloadingData. How do I code it such that it only refreshes when I do the 'pull to refresh'? My code so far:
lazy var refresh: UIRefreshControl = {
let refresh = UIRefreshControl(frame: CGRect(x: 50, y: 100, width: 20, height: 20))
refresh.addTarget(self, action: #selector(refreshData), for: .valueChanged)
return refresh
}()
var eventsArray: [FIRDataSnapshot]! = []
override func viewDidLoad() {
super.viewDidLoad()
ref = FIRDatabase.database().reference()
ref.child("events").observeSingleEvent(of: .value, with: { (snapshot) in
for snap in snapshot.children {
self.eventsArray.insert(snap as! FIRDataSnapshot, at: 0)
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}) { (error) in
print(error.localizedDescription)
}
}
func refreshData() {
self.eventsArray = []
ref.child("events").observe(.childAdded, with: { (snapshot) in
print(snapshot)
self.eventsArray.insert(snapshot, at: 0)
DispatchQueue.main.async {
self.refresh.endRefreshing()
self.tableView.reloadData()
}
}) { (error) in
print(error.localizedDescription)
}
}
I also attempted observeSingleEvent(.childAdded) but it only pulls out one single entry (the very top entry) from Firebase.
I am not sure if my approach is correct in the first place. Any advice here pls, thanks.
Add
var lastKey: String? = nil
In the loop where you iterate over the children set the lastKey to the current snap's key.
lastKey = snap.key
Let me know if this works for you.
func refreshData() {
// Make sure you write a check to make sure lastKey isn't nil
ref.child("events").orderByKey().startAt(lastKey).observeSingleEvent(of: .value, with: { (snapshot) in
// You'll have to get rid of the first childsince it'll be a duplicate, I'm sure you can figure that out.
for snap in snapshot.children {
self.eventsArray.insert(snap as! FIRDataSnapshot, at: 0)
}
DispatchQueue.main.async {
self.refresh.endRefreshing()
self.tableView.reloadData()
}
}) { (error) in
print(error.localizedDescription)
}
}

How to reload UITableView without printing data twice?

I have a single view of SlackTextViewController which works as a UITableView. I'm switching between "states" using a public String allowing it to read 1 set of data in a certain state and another set of data in another state. The problem is when I switch back and forth to the original state it prints the data 2, 3, 4 times; as many as I go back and forth. I'm loading the data from a Firebase server and I think it may be a Firebase issue. Here is my code...
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if chatState == "ALL"
{
self.globalChat()
}else if chatState == "LOCAL"
{
self.localChat()
}
}
override func viewDidDisappear(animated: Bool) {
self.messageModels.removeAll()
tableView.reloadData()
ref.removeAllObservers()
}
func chatACTN()
{
if chatState == "ALL"
{
self.viewWillAppear(true)
}else if chatState == "LOCAL"
{
self.viewWillAppear(true)
}
}
func globalChat()
{
self.messageModels.removeAll()
tableView.reloadData()
let globalRef = ref.child("messages")
globalRef.keepSynced(true)
globalRef.queryLimitedToLast(100).observeEventType(.ChildAdded, withBlock: { (snapshot) -> Void in
if snapshot.exists()
{
let names = snapshot.value!["name"] as! String
let bodies = snapshot.value!["body"] as! String
let avatars = snapshot.value!["photo"] as! String
let time = snapshot.value!["time"] as! Int
let messageModel = MessageModel(name: names, body: bodies, avatar: avatars, date: time)
self.messageModels.append(messageModel)
self.messageModels.sortInPlace{ $0.date > $1.date }
}
self.tableView.reloadData()
})
}
func localChat()
{
self.messageModels.removeAll()
tableView.reloadData()
print("LOCAL")
}
The problem is that each time you call globalChat() you're creating another new observer which results in having multiple observers adding the same items to self.messageModels. Thats why you're seeing the data as many times as you switch to the global state.
Since you want to clear the chat and load the last 100 each time you switch to global, there's no point in keeping the observer active when you switch to "Local".
Just remove the observer when you switch to Local, that should fix your problem.
From firebase docs:
- (void) removeAllObservers
Removes all observers at the current reference, but does not remove any observers at child references.
removeAllObservers must be called again for each child reference where a listener was established to remove the observers.
So, ref.removeAllObservers() will not remove observers at ref.child("messages") level.
Using ref.child("messages").removeAllObservers at the beginning of localChat function to remove the observer you created in globalChat would be ok if you're only dealing with this one observer at this level but if you have more on the same level or you think you might add more in the future the best and safest way would be to remove the specific observer you created. To do that you should use the handle that is returned from starting an observer. Modify your code like this:
var globalChatHandle : FIRDatabaseHandle?
func globalChat()
{
self.messageModels.removeAll()
tableView.reloadData()
ref.child("messages").keepSynced(true)
globalChatHandle = ref.child("messages").queryLimitedToLast(100).observeEventType(.ChildAdded, withBlock: { (snapshot) -> Void in
if snapshot.exists()
{
let names = snapshot.value!["name"] as! String
let bodies = snapshot.value!["body"] as! String
let avatars = snapshot.value!["photo"] as! String
let time = snapshot.value!["time"] as! Int
let messageModel = MessageModel(name: names, body: bodies, avatar: avatars, date: time)
self.messageModels.append(messageModel)
self.messageModels.sortInPlace{ $0.date > $1.date }
}
self.tableView.reloadData()
})
}
func localChat()
{
if globalChatHandle != nil {
ref.child("messages").removeObserverWithHandle(globalChatHandle)
}
self.messageModels.removeAll()
tableView.reloadData()
print("LOCAL")
}
And in viewDidDisappear method replace this
ref.removeAllObservers()
with
ref.child("messages").removeAllObservers()