Table Cell selected but didSelectRowAt indexPath sometimes does not get executed and causes lock up? - ios

[EDIT]:
I think I have solved the lockup issue. While refactoring the didSelectRowAt prep for exit to be a blocking task in the ping async queue, I noticed that successful transitions would consist of one instantaneous move to rootVC followed by another animated transition to rootVC again. Made me realize I had two segues from my tableVC: one being the generic controller to controller segue (which is the intended segue method) and one from selecting the table cell.
I am unsure whether it was just the removal of the extra segue or the combination of multiple attempted solutions AND the removal of the segue that solved the problem. My guess is the latter as memory tells me that there was a time where there was only one segue and "fixes" weren't working then either.
tl;dr: Make sure you have no competing segues and that your async code is thread safe/has proper clean up.
[PROBLEM]:
I have a TableViewController which is a child VC of a rootVC under a Navigation Controller. In other words NavController[rootVC, tableVC].
The app starts in rootVC and is segued into tableVC through a UIButton. At tableVC's viewDidLoad, a network scan is done if no previous scan was completed before. Devices on the network are pinged at a certain port asynchronously as they are discovered. If these pings are successful, the IP Address associated with the device is used to populate a cell in the Table View. If this cell is selected, some app settings are changed through didSelectRowAt indexPath and an unwind segue is done to return to rootVC. viewWillDisappear is utilized as well to pass on values through isMovingFromParentViewController (previously done through prepare for segue but error was also present). The user is able to select the IP Address cell as soon as it appears in the table view, which is intended design.
That functionality all works. Most of the time. However, the app locks up SOMETIMES after a cell is selected WHEN a cell is selected while scanning is not done (this would mean that there can still be async pings happening/queued). I have a print at the beginning of didSelectAtRow which never happens when the lock up occurs. I also have an activity indicator view in the table view, which continues spinning when everything else is unresponsive. There is no crash or break from Xcode's side. It seems like that didSelectAtRow just does not happen and cannot proceed for some reason.
This is how my didSelectAtRow looks like
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("Cell was selected")
if possibleDevices.count != 0 {
selectedTableCellAddress = possibleDevices[indexPath.row]
let defaults = UserDefaults.standard
// Set the current cell's IP Address to the setting's IP Address
if selectedTableCellAddress != nil {
pingOperationsShouldStop = true
lanScanner.stop()
defaults.set(selectedTableCellAddress, forKey: "address")
}
}
}
Note that lanScanner is an instance of MMLanScan
My rookie knowledge gives me a guess that this is due to my async pings (the ping results are the last things that appear on the console). The async functions happen when a device is found on the LAN. It is encapsulated in a function like this:
DispatchQueue.global(qos: .userInteractive).async {
print("pingOperationsShouldStop? \(self.pingOperationsShouldStop)")
if self.pingOperationsShouldStop {
print("Skipping ping for \(addr)")
return
}
self.pingOperationsHaveStopped = false
let port = Int32(self.portStr)
let client = TCPClient(address: address, port: port!)
switch client.connect(timeout: 1) {
case .success:
print("connected")
DispatchQueue.main.sync {
self.possibleDevices.append(address)
self.numberOfPossibleDevicesCounted += 1
self.pingOperations.leave()
}
client.close()
case .failure(let error):
print("failed due to \(error) for \(addr)")
DispatchQueue.main.sync {
self.numberOfPossibleDevicesCounted += 1
client.close()
self.pingOperations.leave()
}
}
}
Note that possibleDevices has a didSet property observer that calls reloadData.
At first I thought that it only occurred through cell selection (i.e. NavCon's Back worked fine). So I programmatically called the popping of the view, to not avail. I tried reproducing the lock up through NavCon's Back button and have been able to do it once.
I'm not sure what code is relevant to share, so let me know if you need more information.
Thanks!

Related

how to prevent button showing up for split second when view loads

So my goal is to smoothly load the viewController with no split second bugs. I have a function that is used to determine what buttons to show when the view loads based off a field in a Firestore document. Here is the function:
func determinePurchasedStatusVerification() {
db.collection("student_users/\(user?.uid)/events_bought").whereField("event_name", isEqualTo: selectedEventName!).whereField("isEventPurchased", isEqualTo: true).getDocuments { (querySnapshot, error) in
if let error = error {
print("\(error)")
} else {
guard let querySnap = querySnapshot?.isEmpty else { return }
if querySnap == true {
self.purchaseTicketButton.isHidden = false
self.viewPurchaseButton.isHidden = true
self.cancelPurchaseButton.isHidden = true
} else {
self.purchaseTicketButton.isHidden = true
self.viewPurchaseButton.isHidden = false
self.cancelPurchaseButton.isHidden = false
}
}
}
}
I call this function in the viewWillAppear() of the vc but when I instantiate to that vc, this is the result...
The extra purchase ticket button shows up for a split second. Even though it's very quick, you can still see it and it's just not something a user would need to see. It's also the other way around when you click on a cell that's not purchased, the two bottom buttons show up for a split second. I just want to know how I can prevent this quick bug and be able to display a smooth segue with no delays in the button hiding. Thanks.
getDocuments is an asynchronous function, meaning it doesn't call its callback function immediately -- it calls it when it gets data back from the server. It may seem like a split second just because your internet connection is fast and the Firebase servers are definitely fast, but it's a non-zero time for sure. And, someone with a slower connection might experience much more of a delay.
Unless your callback is getting called twice with different results (which seems doubtful), the only solution here is to make sure that your initial state has all of the buttons hidden (and maybe a loading indicator) and then show the buttons that you want once you get the data back (as you are right now). My guess is, though, that you have an initial state where the buttons are visible, which causes the flicker.

iOS tableView not reloading until click, scroll, or switch tabbar items

I have this problem where (in several places) after executing an API call, the view does not refresh until a user action - like a btn click, tab bar switch, etc occurs. I have a feeling it is related to threading, but I can't seem to figure it out and I am new to iOS programming. I have tried different solutions with DispatchQueue etc, using it, and not using it. Trying to call setNeedsDisplay on the controller view. But no luck yet. The following is an example of code pulled right from one of my tab bar item view controllers:
func getEmployeeUpdates(){
self.showLoader()
APIAdaptor.shared.getEmployeeUpdates(forEmployee: Session.shared.employee, completion: {
(updates:[ScheduleUpdate]?, error:Error?) in
guard error == nil else{
DispatchQueue.main.async {
// self.resetMainScreen()
self.hideLoader();
}
return
}
DispatchQueue.main.async {
self.hideLoader();
self.ScheduleUpdates = updates!
self.tableView.reloadData();
}
})
}
func showLoader(){
NSLayoutConstraint.activate([
activityIndicator!.centerXAnchor.constraint(equalTo: self.tableView.centerXAnchor),
activityIndicator!.centerYAnchor.constraint(equalTo: self.tableView.centerYAnchor)])
activityIndicator?.startAnimating();
}
func hideLoader(){
print("Hiding");
activityIndicator?.stopAnimating()
}
I have attached two images. The first image is where the api call has finished (confirmed through testing) but the view is not refreshing. The loader is frozen. It should disappear after a call to hideLoader(). the second Image is after a click, or tab bar item switch.
I should also mention that in this example, as well as in other api calls the view will refresh eventually after completing, but only after a significant delay.
If anyone can help I would appreciate it very much!
This was a problem caused by the simulator on Xcode 10.1. If you run into this problem, try updating Xcode, or using a real device.

Segue is not working, and i can't figure out why

I have an if...then statement in the ViewDidLoad method for the view that acts as my storyboard entry point.
Basically, I am doing a check to see if there is any data in core data, to indicate that they've completed a small "setup form".
If it is found that the core data is empty or that the app has not been properly set up, I want it to automatically kick them over to the settings view with a segue.
My ViewDidLoad method looks like this:
override func viewDidLoad() {
super.viewDidLoad()
//Get any entries from the App Settings Entity
getAppSettings()
//If any entries are found, check to see if the setup has been completed
if (appSettings.count > 0) {
print("We found entries in the database for App Settings")
if (appSettings[0].setupComplete == false) {
print("Setup has not been completed")
self.performSegue(withIdentifier: "toAppSettings", sender: self)
} else {
print("Setup is completed")
//Load the settings into global variables
preferredRegion = appSettings[0].region!
usersName = appSettings[0].usersName!
}
} else {
print("We found no entries in the database for App Settings")
self.performSegue(withIdentifier: "toAppSettings", sender: self)
}
}
I made sure that the segue does exist, and that the identifier for the segue is exactly as I have it in the quotes (I even copied & pasted all instances of it to make sure that they are all consistent).
I also went the extra mile and put a checker in the "prepare for segue" method, to print whether it was getting called, and who the sender was:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
print("We're getting ready to segue!")
print(segue.identifier)
}
Both of those items get printed to the log, which tells me that the segue is being recognized and that the app is attempting to fire it. But - for some reason that I can't figure out - it simply isn't firing.
Any ideas what I am missing here?
I have an if...then statement in the ViewDidLoad
But that's the problem. viewDidLoad is way too early for this. Remember, all viewDidLoad means is that your view controller has a view. That's all it means. That view is not yet in the interface, and the view controller itself may not even be in the view hierarchy! It's just sitting out there in object space. You cannot segue from here; there is nothing to segue from.
Try waiting until viewDidAppear. If that makes it work, you might try moving it back to viewWillAppear, but I don't guarantee anything. Keep in mind that these methods can be called multiple times, so you might also need a flag to make sure that this segue fires just the once.

Swift iOS, UITableView nil

I have a Swift (2.2) iOS app (my first) with a couple of UITableViews. One of the views lists payments which are added / removed throughout the life of the program.
This all works fine 99% of the time, but a few times I have come across an issue where the UITableView all of a sudden becomes nil.
The IBOutlet must be hooked up correctly, or it would not work at all.
What could possibly be causing this when I am not assigning to the IBOutlet variable anywhere (just calling methods on it)?
Or (if I cannot find the cause), advice on best handling when it happens (if I need to recreate it, what about outlets, events, autolayout, etc.?)
#IBOutlet weak var paymentTableView: UITableView!
func handlePayment(payment: PaymentRecord) -> Void {
let existingPaymentIndex = payments.indexOf({ $0.payNo == payment.payNo })
if (existingPaymentIndex != nil) {
payments.removeAtIndex(existingPaymentIndex!)
}
if (self.paymentTableView == nil) { // Here is where I notice the issue
Log.error?.message("handlePayment: paymentTableView is nil!!")
return
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.paymentTableView.reloadData()
})
}
What is happening is that the tableview is being unloaded from memory when the view is disposed of. You likely have an asynchronous task completing, most usually from making a network call, that is returning and calling this function after the view was disposed. I've seen this happen many times when a view is trying to load a network resource and the user is able to switch to a different view before the network call's completion handler is called.

(Swift) Putting a NSNotificationCenter observer in ViewDidLoad isn't working for my project... Where should I put it?

I'm using SWRevealController to display 3 (left, center, right) panels. Basically, the right panel is a tableview of numbers and the middle panel shows possible even divisions when a user clicks on a number. I connected a segue to the IBAction of a reusable tablecell in the right view controller which loads the MainViewController. This all works fine. The problem is that if the number can't be divided evenly it triggers a notification, which the main view controller observes on ViewDidLoad. This notification sets the alpha of a pseudo-"alert" (UIView at the bottom of screen) to 1.0 for 4 seconds, at which point it returns to 0. Unfortunately this is where the problem starts: the notification box appears for a brief second while the animation runs but when the main viewcontroller finishes animating, the alert box disappears. I have a hunch it's because the ViewDidLoad fires at this point and resets the NSNotificationOberser – if I remove the segue on Touch Up Inside and manually switch view controllers the alert-box remains present.
Can you help me think of what I'm getting wrong? Like I said, I think it's because the observer is initialized in the ViewDidLoad. Assuming this is the case, where should I initialize the observer so that this doesn't happen anymore?
Basically the main VC displays a calculator, the code for which runs in a Calculator.swift file. If, when the number is passed through Calculate(), there is an error, it triggers an "alert" which the main VC picks up on, then reveals the box so that the user knows. Each time Calculate() is called, it logs the user's calculation to a tableview in the RVC – idea being they can reload previous calculations. Is this an improper usage of Notification Center?
The way I want the timeline of events to be:
User clicks on cell in the right panel
Main view controller (the calculator) is pushed via segue
Calculate() is called on the selected number, if there is a remainder a notification is triggered
The main view displays the results from Calculate(), if an alert fired then it would unhide a popup box on main view.
What is currently happening:
User clicks on the cell in right panel
Calculate() is called on the number, if there is a remainder the notification fires
Before the main view is pushed via segue, I can see briefly in the animation the result of the trigger firing and the calculation
As soon as the segue animation completes the view hides
My main VC Code (PopUpView is the alert box)
class CalculatorVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "triggerAlert:", name: "alert", object: nil)
self.PopUpView.alpha = 0.0
}
the triggerAlert function:
func triggerAlert(notification: NSNotification) {
PopUpView.alpha = 1.0
let returnedRemainder = (notification.userInfo)
let sample: Double = (returnedRemainder!["userTotal"]as! Double)
self.label.text = "Warning! Remainder: \(Double(round(100 * sample)/100))"
}
the tableView didSelect of the right VC:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
newUser.calculate()
//code here assigns the value in the selected table cell to newUser
}
Then in the Calculate() function:
dispatch_async(dispatch_get_main_queue(), {
NSNotificationCenter.defaultCenter().postNotificationName("alert", object: nil, userInfo: userTotalDictionary)
}
Notification observation/de-observation should generally be balanced between viewDidAppear/viewWillDisappear or viewWillAppear/viewDidDisappear. Either pair is usually fine, but it's wise to keep things that happen "when just offscreen" separate from things that happen "when just onscreen".
viewDidLoad is a poor place to set up observation, because you don't have a good place to balance removing the observation (viewDidUnload no longer exists). You should only remove observations in deinit that you set up in init, and view controllers really should never be observing things when they are not on screen.

Resources