navigationController?.navigationBar.isUserInteractionEnabled not working as expected - swift

For the following viewController hierarchy, isUserInteractionEnabled doesn't appear to be working as expected.
NavigationController(ViewController A) --- pushes to ---> NavigationController(ViewController B)
In ViewController A's viewDidAppear method I set navigationController?.navigationBar.isUserInteractionEnabled to false and set it to true in ViewController B's viewDidAppear method. However, upon popping ViewController B and returning to ViewController A, the navigation bar remains enabled for user interaction. Any thoughts as why this may be happening are greatly appreciated, thanks in advance!

That seems to be a bug for which you could get around by doing that on the main thread:
override func viewDidAppear(_ animated: Bool) {
//...
DispatchQueue.main.async {
self.navigationController?.navigationBar.isUserInteractionEnabled = false
}
}
But this still leaves a millisecond window where the navigationBar's interaction is enabled.
You have to be really quick.
However...
I wouldn't recommend what you're doing; i.e. disabling the navigationBar.
You could lose the back ability, if it had one, because you're just disabling the navigationBar entirely.
Suggestion:
Since every viewController in the navigation stack has it's own navigationItem, that contains it's own set of barButtonItems, I would recommend you keep references of the UIBarButtonItem and enable/disable them explicitly.
i.e.
#IBOutlet var myBarButtonItem: UIBarButtonItem!
override func viewDidAppear(_ animated: Bool) {
//...
myBarButtonItem.isEnabled = false
}
Furthermore, the state of this barButtonItem is handled in this viewController itself and you need not do things like self.navigationController?.navigationBar.isUserInteractionEnabled = true elsewhere.

Related

swift xcode iOS: can I re-use a loaded modal fullscreen view controller?

I have a storyboard with two view controllers. First one, VC_1, has one button that opens 2nd one - VC_2.
VC_2 also has a button that opens VC_1.
Both controllers have almost identical code:
class VC_1: UIViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
print(“VC_1 loaded")
}
override func viewDidAppear(_ animated: Bool){ print(“VC_1 appeared") }
override func viewDidDisappear(_ animated: Bool){ print(“VC_1 disappeared") }
#IBAction func btnShowVC_2(_ sender: UIButton)
{
let storyboard = UIStoryboard(name: "Main", bundle: nil)
secondVC = storyboard.instantiateViewController(identifier: “VC_2”)
secondVC.modalPresentationStyle = .fullScreen
show(secondVC, sender: self)
}
}
The difference is only in "VC_2" instead of "VC_1" in the 2nd controller code.
I have seen this View Controller creation code in Apple documentation and many other examples around the Internet.
When I press the button on the VC_1, I see in the debug window, that VC_2 is loaded and appeared, and VC_1 is disappeared. And same, of course, happens when I press the button on VC_2 - it disappears, and VC_1 is loaded again.
My questions are:
what happens with View Controller object after "viewDidDisappear" has been called? Does it really disappear from memory, or "disappear" only means "you cannot see it on the screen?". I do not see "viewDidUnload" in the documentation...
I suppose that "viewDidLoad" means that new View Controller object was created in memory. Is there any way to load the View Controller object only once, and then hide and show it without causing "viewDidLoad" to be called? I tried to do it with global variable "secondVC" but got "Application tried to present modally an active controller" error.
viewDidDisappear: called after the view is removed from the windows’
view hierarchy. No, View controller object just left the view property. By the way the amount of memory used by view controllers is negligible. So dont think about too much. If you want to catch when Your View controller object release from the memory put
deinit { print("vc deallocated") }
viewDidUnload, it has been deprecated since the iOS
6, which used to do some final cleaning.
Partly true. Keep in mind ViewDidload called one time for the life cycle of view controller. There is a method called before viewdidload but this is not related with your question.
In addition to "There is a method before viewdidload" -> loadView( ) is a method managed by the viewController. The viewController calls it when its current view is nil. loadView( ) basically takes a view (that you create) and sets it to the viewController’s view (superview).

Table view not updating when called from another View Controller using NotificationCenter

I have a table view in one view controller. I want to update the table view from another VC. I'm doing this using NotificationCenter like this:
In the table view VC:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(self.updateTableView), name: NSNotification.Name(rawValue: "updateTableView"), object: nil)
}
#objc func updateTableView() {
print("in func")
DispatchQueue.main.async {
self.listTV.reloadData()
}
}
In the opened VC:
func reloadTV() {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "updateTableView"), object: nil)
}
The print works, but the table view doesn't reload. I can only see it changing if I dismiss the VC with the table view and open it again.
I've followed the suggestions of other answers which is to use DispatchQueue, but that didn't make a difference. What am I missing?
Many thanks!
I would consider doing this by protocol and delegate instead of notifications...
This is how I do it.
When you call tableView.reloadData(). It has no effect if the tableView is hidden (isHidden == true). I believe the behavior is also the same when the tableView is effectively hidden by another ViewController (so effectively hidden).
Solution: A suggestion is to reload the tableView when the view is shown again. Few ways to do that.
one is in viewDidAppear (depends on how you transitioned between ViewControllers),
or
you can keep a reference to the tableView and after you dismiss the second ViewController (in which you were sending the notification) you provide the callback to reload the tableView. Roughly like the snippet, with self being the secondViewController you transitioned to. Dismiss that view
self.dismiss(animated: true, completion: {
self.tableView.reloadData()
}
or you can still use your notification and setup a flag and when the viewDidAppear, you then reloadData() inside viewDidAppear. However for this to work, you need to ensure that viewDidAppear is always called when the view is shown again (this will depend on how you transitioned between views).Simply override that method and print to verify.
There are other ways but the bottom line is tableView.reloadData() will be ignored if it is called when the tableView is not visible on the screen. My guess is it's an optimization made by Apple (no need to reload if no table is showing) or maybe a bug. I think they'll say it's a feature 😄
I suggest instead of using NotificationCenter, to instead set a Bool, tableViewNeedsUpdate, and check for the value of that Bool when the view with the UITableView is presented, and the UITableView is visible and able to accept reloadData() calls. After the reload is complete, set the variable back to false.

Edit button does not have any functionality, no red circle with a minus appears on the left side of cells

I have looked at another question that was asking the exact same question as mine, but the answer told them to call the setEditing function (which I tried, and say later in the question). But I don't see how you could call this function only when the edit button is clicked. I suppose I could create my own BarButtonItem and run this method when my bar button item is clicked, but I figured this would be far easier since I need the basic functionality of the edit button.
I have a UIViewController that has a table on it named peersTable. When I click the edit button it switches to done, but nothing happens on the table. I have also added my own UITableViewRowActions and when I swipe to the left on the cells, my custom actions do show up.
Here is some of my code:
class PeerViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, ConnectionManagerDelegate {
#IBOutlet weak var peersTable: UITableView!
...
override func viewDidLoad() {
...
peersTable.delegate = self
peersTable.dataSource = self
self.navigationItem.rightBarButtonItem = self.editButtonItem
...
}
I have also tried calling the peersTable.setEditing(true, animated: true) method myself, and in this case, the red minus does appear. I've never seen this issue before, so I don't understand why it's happening. Am I somehow setting the delegate wrong? Or possibly I'm doing something wrong since this is a regular View and not a TableView (even though I've done this before in a previous project).
Thanks in advance! If you need any more information let me know!
Since this is a not a TableViewController it won't automatically set your tableView to editing mode when you press the edit button.
You need to override setEditing method so you can set the tableView to editing mode.
Add this to your ViewController class:
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
peersTable.setEditing(editing, animated: animated)
}

Where in view lifecycle to update controller after modal UIViewController dismissed

I have a UIViewController with a UILabel that needs to display either "lbs" or "kg". My app has a settings screen (another UIViewController) that is presented modally over the first view controller and the user can select either of the two units and save their preference. If the units are changed and the modal settings screen is dismissed, I of course want the label on the first view controller to be updated with the new units value (but without refreshing the whole view). I thought I knew how to make it work, but evidently I don't.
On my modal settings screen, I have a UISegmentedControl to allow the user to select units. Anytime it's changed, this function updates userDefaults:
func saveUnitsSelection() {
if unitsControl.selectedSegmentIndex == 0 {
UserDefaultsManager.sharedInstance.preferredUnits = Units.pounds.rawValue
} else {
UserDefaultsManager.sharedInstance.preferredUnits = Units.kilograms.rawValue
}
}
Then they would likely dismiss the settings screen. So, I added this to viewDidLoad in my first view controller:
override func viewDidLoad() {
super.viewDidLoad()
let preferredUnits = UserDefaultsManager.sharedInstance.preferredUnits
units.text = preferredUnits
}
That didn't work, so I moved it to viewWillAppear() and that didn't work either. I did some research and some caveman debugging and found out that neither of those functions is called after the view has been loaded/presented the first time. It seems that viewWillAppear will be called a second time if I'm working within a hierarchy of UITableViewControllers managed by a UINavigationController, but isn't called when I dismiss my modal UIViewController to reveal the UIViewController underneath it.
Edit 1:
Here's the view hierarchy I'm working with:
I'm kinda stuck at this point and not sure what to try next.
Edit 2:
The user can tap a 'Done' button in the navigation bar and when they do, the dismissSettings() function dismisses the Settings view:
class SettingsViewController: UITableViewController {
let preferredUnits = UserDefaultsManager.sharedInstance.preferredUnits
// some other variables set here
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBar.topItem?.title = "Settings"
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Done", style: .Plain, target: self, action: #selector(self.dismissSettings(_:)))
if preferredUnits == Units.pounds.rawValue {
unitsControl.selectedSegmentIndex = 0
} else {
unitsControl.selectedSegmentIndex = 1
}
}
func dismissSettings(sender: AnyObject?) {
navigationController?.dismissViewControllerAnimated(true, completion: nil)
}
}
THE REAL PROBLEM
You misspelled viewWillAppear. You called it:
func viewWillAppear()
As far as Cocoa Touch is concerned, this is a random irrelevant function that hooks into nothing. You meant:
override func viewWillAppear(animated: Bool)
The full name of the first function is: "viewWillAppear"
The full name of the second function is: "viewWillAppear:animated"
Once you get used to this, the extreme method "overloading" that Cocoa Touch uses gets easier.
This is very different in other languages where you might at least get a warning.
The other lesson that everyone needs to learn when posting a question is: Include All Related Code!
Useful logging function I use instead of print or NSLog, to help find these things:
class Util {
static func log(message: String, sourceAbsolutePath: String = #file, line: Int = #line, function: String = #function, category: String = "General") {
let threadType = NSThread.currentThread().isMainThread ? "main" : "other"
let baseName = (NSURL(fileURLWithPath: sourceAbsolutePath).lastPathComponent! as NSString).stringByDeletingPathExtension ?? "UNKNOWN_FILE"
print("\(NSDate()) \(threadType) \(baseName) \(function)[\(line)]: \(message)")
}
}
[Remaining previous discussion removed as it was incorrect guesses]

How to detect view controller dismissed or not

In iOS app, there may be several view controllers. They may perform segues from one to another. the question is how to detect each view controller about whether it is dismissed or not when implementing segue. Thanks.
You have access to:
override func viewWillDisappear(animated: Bool) {
}
override func viewDidDisappear(animated: Bool) {
}
// Called when the view controller will be removed from memory.
deinit {
}
Which can help you managed things based on that state of a view controller.
I'm not sure if you can detect whether or not it was dismissed, but you can set a variable "viewControllerDismissed = true" in performSegueWithIdentifier that will be detected in the VC behind the one being dismissed.