TableView Segue to new ViewController does not work twice - ios

I have a TableViewController in which selecting a row makes a network call, shows an activityIndicator, and in the completion block of the network call, stops the indicator and does a push segue.
In testing, I found that when going to the new view controller, going back, and selecting another row, it shows the activity indicator, but never stops or calls the push segue. The code for didSelect:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.tableView.userInteractionEnabled = false
self.searchController?.active = false
self.idx = indexPath
let cell = tableView.cellForRowAtIndexPath(indexPath) as! CustomTableViewCell
cell.accessoryView = self.activityIndicator
self.activityIndicator.startAnimating()
self.networkingEngine?.getObjectById("\(objectId)", completion: { (object) -> Void in
if object != nil {
self.tableView.userInteractionEnabled = true
self.chosenObject = object
self.activityIndicator.stopAnimating()
self.tableView.reloadRowsAtIndexPaths([self.idx], withRowAnimation: .None)
self.tableView.deselectRowAtIndexPath(self.idx, animated: true)
self.performSegueWithIdentifier("segueToNewVC", sender: self)
}
else {
}
})
}
and my network call using Alamofire
func getObjectbyId(objectId: String, completion:(Object?) -> Void) {
Alamofire.request(Router.getObjectById(objectId: objectId))
.response { (request, response, data, error) -> Void in
if response?.statusCode != 200 {
completion(nil)
}
else if let parsedObject = try!NSJSONSerialization.JSONObjectWithData(data as! NSData, options: .MutableLeaves) as? NSDictionary {
let object = parsedObject["thing"]
dispatch_async(dispatch_get_main_queue(), { () -> Void in
completion(object)
})
}
}
}
So I made breakpoints, and it actually is going into the completion block the second time and calling it to stop indicator, reload that row, deselect the row, re-enable the tableView userInteraction, and perform the segue. It calls my prepareForSegue as well, but as soon as it finishes, it just sits with the indicator still spinning, and the RAM usage skyrockets up to almost 2GB until the simulator crashes.
I believe it has to do with multi-threading but I can't narrow down the exact issue since I'm putting my important stuff on the main thread.

The problem had to do with making the accessoryView of the cell an activityIndicator and then removing it and reloading it all while segueing away. I couldn't figure out how to keep it there and stop the multi-threading issue but I settled for a progress AlertView. If anybody has a solution, I'd love to know how this is fixed.

Related

What is the correct way to reload a tableView from inside a nib?

I have a tableview which shows a custom cell.
Inside the cell is a button.
Once the button is clicked, a network call is made and the tableview should reload.
I tried this, but I get Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value at vc.reloadData().
#IBAction func acceptRequestPressed(_ sender: UIButton) {
DatabaseManager.shared.AnswerFriendRequest(emailFriend: friendNameLabel.text!, answer: true) { success in
if success {
let vc = FriendRequestViewController()
vc.reloadData()
}else {
print ("error at answer")
}
}
}
The problem is this line:
let vc = FriendRequestViewController()
After that, vc is the wrong view controller — it is just some view controller living off emptily in thoughtspace, whereas the view controller you want is the already existing view controller that's already in the view controller hierarchy (the "interface").
Few pointers:
You can have a closure upon the data call completion in your class that has the table view.
Eg:
// Custom Table Cell Class
class CustomCellClass: UITableViewCell {
let button = UIButton()
// All other UI set up
// Create a closure that your tableView class can use to set the logic for reloading the tableView
public let buttonCompletion: () -> ()
#IBAction func acceptRequestPressed(_ sender: UIButton) {
DatabaseManager.shared.AnswerFriendRequest(emailFriend: friendNameLabel.text!, answer: true) { [weak self] success in
if success {
// Whatever logic is provided in cellForRow will be executed here. That is, tableView.reloadData()
self?.buttonCompletion()
}else {
print ("error at answer")
}
}
}
}
// Class that has tableView:
class SomeViewController: UIViewController {
private let tableView = UITableView()
.
.
// All other set up, delegate, data source
func cellForRow(..) {
let cell = tableView.dequeueTableViewCell(for: "Cell") as! CustomCellClass
cell.buttonCompletion = {[weak self] in
tableView.reloadData()
}
}

Access a UIView's properties on a background thread

I am trying to have my CollectionView scroll it's first cell after the view appears, and then again whenever a button is pressed. The problem is that the collectionView hasn't generated all it's cells at any of the view lifecycle functions.
My solution is to beging a while loop on a background thread that checks to see if collectionView.visibleCells.count > 0, and when it is, return to the main thread and scroll to the first cell. However, I get an error, telling me that I shouldn't access visibleCells off the main thread, and the app chugs when I do.
How can I achieve this functionality on the main thread, or check the number of cells in the background thread?
private func scrollToFirst() {
DispatchQueue.global(qos: .background).async { [weak self] in
if (self != nil) {
while(self!.collectionView.visibleCells.count != 0) {
DispatchQueue.main.async { [weak self] in
self!.collectionView.scrollToItem(at: IndexPath(item: 0, section: 0), at: .centeredHorizontally, animated: true)
}
}
}
}
}
There is a delegate method willDisplay that gets called right before a collectionViewCell gets displayed. If you previously had no cells and this gets called, then you know you are about to go from zero to more than zero cells.
Yeah, don't do that. UIKit is not thread-safe, so the data structures of view objects may change out from under you when you try to view them from a background threads.
It seems like there should be a better way to deal with this than waiting for cells to appear.
If you can't figure out a cleaner way to do it, you could use a Timer object, which runs on the main thread. That code might look something like this:
private func scrollToFirst(afterDelay delay: Double = 0.2) {
_ = Timer.scheduledTimer(withTimeInterval: delay, repeats: false) {
timer, [weak self] in
guard strongSelf = self else {
return
}
if strongSelf.collectionView.visibleCells.count != 0 {
self!.collectionView.scrollToItem(at: IndexPath(item: 0, section: 0), at: .centeredHorizontally, animated: true)
}
}
That code would fire once, and scroll to the beginning of the collection view if there are once the timer fires.

Firebase database observer in each tableViewCell

I am creating a chat application where the main view is a tableView with each row representing a chat. The row has a label for the person's name as well as a label with the latest chat message. If a user taps a row, it will segue into a chat view. I am using MessageKit for the chat framework. All is working well except I am having trouble displaying the most recent chat consistently.
Here is how my tableView looks. Please ignore the black circles, I was quickly clearing out personal photos I was using for testing.
I have a custom tableViewCell with the method below. This creates an observer for this specific chat starting with the most recent message.
func getLatestChatMessage(with chatID: String) {
guard let loggedInUserID = Auth.auth().currentUser?.uid else { return }
DatabaseService.shared.chatsRef.child(chatID).queryLimited(toLast: 1).observe(.childAdded) { [weak self] (snapshot) in
guard let messageDict = snapshot.value as? [String:Any] else { return }
guard let chatText = messageDict["text"] as? String else { return }
guard let senderId = messageDict["senderId"] as? String else { return }
DispatchQueue.main.async {
if senderId == loggedInUserID {
self?.latestChatMessage.textColor = .black
} else {
self?.latestChatMessage.textColor = UIColor(red: 0/255, green: 122/255, blue: 255/255, alpha: 1)
}
self?.latestChatMessage.text = chatText
}
}
}
I call this function in the TableView's method cellForRowAt after observing the chatID for this user.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
DatabaseService.shared.matchedChatIdRef.child(loggedInUserID).child(match.matchedUserId).observeSingleEvent(of: .value) { (snapshot) in
if let chatID = snapshot.value as? String {
cell.getLatestChatMessage(with: chatID)
}
}
}
Ideally this should create an observer for each chat and update the label every time there is a new message sent. The issue I am having is it sometimes works but other times seems to stop observing.
For example, if I am on the chat tableView and get a new message for one of the cells, it updates. However, if I then tap that chat to segue into a specific chat and send a message then tap the back button to go back to my tableView, the label stops updating. It appears as though my observer gets removed. Does anyone have any ideas why my observers in each cell stop working? Also, I am interested if anyone has a recommendation for a better solution to this? I am worried that creating an observer for each cell may be a poor solution.
EDIT: I've found that the observer stops working after selecting the back button or swiping back to the tableView from the chat in the screenshot below.
Woke up this morning and realized my issue...In the ChatViewController I was calling removeAllObservers()
I updated my code to below to just remove the observer in the chat and it fixed my issue:
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(true)
if let observerHandle = observerHandle, let chatID = chatId {
DatabaseService.shared.chatsRef.child(chatID).removeObserver(withHandle: observerHandle)
}
}

Table view continues to add rows while observing .childAdded even when child was not added in Firebase

I have a UITableView that gets populated by the following firebase database:
"games" : {
"user1" : {
"game1" : {
"currentGame" : "yes",
"gameType" : "Solo",
"opponent" : "Computer"
}
}
}
I load all the games in viewDidLoad, a user can create a new game in another UIViewController, once a user does that and navigates back to the UITableView I want to update the table with the new row. I am trying to do that with the following code:
var firstTimeLoad : Bool = true
override func viewDidLoad() {
super.viewDidLoad()
if let currentUserID = Auth.auth().currentUser?.uid {
let gamesRef =
Database.database().reference().child("games").child(currentUserID)
gamesRef.observe(.childAdded, with: { (snapshot) in
let game = snapshot
self.games.append(game)
self.tableView.reloadData()
})
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if firstTimeLoad {
firstTimeLoad = false
} else {
if let currentUserID = Auth.auth().currentUser?.uid {
let gamesRef = Database.database().reference().child("games").child(currentUserID)
gamesRef.observe(.childAdded, with: { (snapshot) in
self.games.append(snapshot)
self.tableView.reloadData()
})
}
}
}
Lets say there is one current game in the data base, when viewDidLoad is run the table displays correctly with one row. However anytime I navigate to another view and navigate back, viewDidAppear is run and for some reason a duplicate game seems to be appended to the games even though no child is added.
The cells are being populated by the games array:
internal func tableView(_ tableView: UITableView, cellForRowAt indexPath:
IndexPath) -> UITableViewCell {
let cell = Bundle.main.loadNibNamed("GameTableViewCell", owner:
self, options: nil)?.first as! GameTableViewCell
let game = games[indexPath.row]
if let gameDict = game.value as? NSDictionary {
cell.OpponentName.text = gameDict["opponent"] as? String
}
return cell
}
UPDATE:
Thanks to everyone for their answers! It seems like I misunderstood how firebase .childAdded was functioning and I appreciate all your answers trying to help me I think the easiest thing for my app would be to just pull all the data every time the view appears.
From what I can see, the problem here is that every time you push the view controller and go back to the previous one, it creates a new observer and you end up having many observers running at the same time, which is why your data appears to be duplicated.
What you need to do is inside your viewDidDisappear method, add a removeAllObservers to your gameRef like so :
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
guard let currentUserId = Auth.auth().currentUser?.uid else {
return
}
let gamesRef = Database.database().reference().child("games").child(currentUserId)
gamesRef.removeAllObservers()
}
I cannot see all your code here so I am not sure what is happening, but before adding your child added observer, you need to remove all the elements from your array like so :
games.removeAll()
Actually, as per best practices, you should not call your method inside your ViewDidLoad, but instead you should add your observer inside the viewWillAppear method.
I cannot test your code right now but hopefully it should work like that!
Let me know if it doesn't :)
UPDATE:
If you want to initially load all the data, and then pull only the new fresh data that is coming, you could use a combination of the observeSingleEvent(of: .value) and observe(.childAdded) observers like so :
var didFirstLoad = false
gamesRef.child(currentUserId).observe(.childAdded, with: { (snapshot) in
if didFirstLoad {
// add your object to the games array here
}
}
gamesRef.child(currentUserId).observeSingleEvent(of: .value, with: { (snapshot) in
// add the initial data to your games array here
didFirstLoad = true
}
By doing so, the first time it loads the data, .childAdded will not be called because at that time didFirstLoad will be set to false. It will be called only after .observeSingleEvent got called, which is, by its nature, called only once.
Try following code and no need to check for bool , Avoid using bool here its all async methods , it created me an issue in between of my chat app when its database grows
//Remove ref in didLoad
//Remove datasource and delegate from your storyboard and assign it in code so tableView donates for data till your array don't contain any data
//create a global ref
let gamesRef = Database.database().reference()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.games.removeAllObjects()
if let currentUserID = Auth.auth().currentUser?.uid {
gamesRef.child("games").child(currentUserID)observe(.childAdded, with: { (snapshot) in
self.games.append(snapshot)
self.tableView.dataSource = self
self.tableView.delegate = self
self.tableView.reloadData()
})
gamesRef.removeAllObserver() //will remove ref in disappear itself
//or you can use this linen DidDisappear as per requirement
}
else{
//Control if data not found
}
}
//TableView Delegate
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if self.games.count == 0{
let emptyLabel = UILabel(frame: CGRect(x: 0, y: 0, width: self.view.bounds.size.width, height: self.view.bounds.size.height))
emptyLabel.text = "No Data found yet"
emptyLabel.textAlignment = .center
self.tableView.backgroundView = emptyLabel
self.tableView.separatorStyle = .none
return 0
}
else{
return self.games.count
}
}
observe(.childAdded) is called at first once for each existing child, then one time for each child added.
Since i also encounter a similar issue, assuming you don't want to display duplicate objects, in my opinion the best approach, which is still not listed in the answers up above, is to add an unique id to every object in the database, then, for each object retrieved by the observe(.childAdded) method, check if the array which contains all objects already contains one with that same id. If it already exists in the array, no need to append it and reload the TableView. Of course observe(.childAdded) must also be moved from viewDidLoad() to viewWillAppear(), where it belongs, and the observer must be removed in viewDidDisappear. To check if the array already includes that particular object retrieved, after casting snapshot you can use method yourArray.contains(where: {($0.id == retrievedObject.id)}).

Dynamically scroll through tableView with scrollToRowAtIndexPath

I'd like to visually scroll through my whole tableView. I tried the following, but it doesn't seem to perform the scrolling. Instead it just runs through the loops. I inserted a dispatch_async(dispatch_get_main_queue()) statement, thinking that that would ensure the view is refreshed before proceeding, but no luck.
What am I doing wrong?
func scrollThroughTable() {
for sectionNum in 0..<tableView.numberOfSections() {
for rowNum in 0..<tableView.numberOfRowsInSection(sectionNum) {
let indexPath = NSIndexPath(forRow: rowNum, inSection: sectionNum)
var cellTemp = self.tableView.cellForRowAtIndexPath(indexPath)
if cellTemp == nil {
dispatch_async(dispatch_get_main_queue()) {
self.tableView.scrollToRowAtIndexPath(indexPath!, atScrollPosition: .Top, animated: true)
self.tableView.reloadData()
}
}
}
}
}
I found a solution. Simply use scrollToRowAtIndexPath() with animation. To do so I had to create a getIndexPath() function to figure out where I want to scroll. Has more or less the same effect as scrolling through the whole table if I pass it the last element of my tableView.
If you want it to happen slower with more scrolling effect, wrap it inside UIView.animateWithDuration() and play with 'duration'. You can even do more animation if you want in its completion block. (No need to set an unreliable sleep timer, etc.)
func animateReminderInserted(toDoItem: ReminderWrapper) {
if let definiteIndexPath = indexPathDelegate.getIndexPath(toDoItem) {
self.tableView.scrollToRowAtIndexPath(definiteIndexPath, atScrollPosition: .Middle, animated: true)
}
}

Resources