Get destination view controller when pressing back on navigation bar - swift

When we perform a segue, it is easy to get the destination view controller so we can pass data to it using the prepare(for:) method.
I'd like to know the correct way to do this when the back button of a navigation controller is pressed.
I've managed to piece together something that works, but it feels wrong to be using my knowledge of the hierarchy of the view controllers within the navigation controller rather than getting the destination dynamically. Maybe i'm overthinking it?
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
// This method is called more than once - parent is only nil when back button is pressed
if (parent == nil) {
guard let destination = self.navigationController?.viewControllers.first as? MyTableViewController else {
return
}
print("destination is \(destination)")
// set delegate of my networking class to destination here
// call async method on networking class to retrieve updated data for my table view
}
}

As a general rule, trying to do that makes for too tight of a coupling. The VC you are navigating away from shouldn't need to know about where it's headed.
Probably better to implement your networking class methods in viewWillAppear or viewDidAppear inside your MyTableViewController class.
However, you could give this a try:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// make sure we're in a navigation controller
guard let navC = navigationController else { return }
if navC.viewControllers.firstIndex(of: self) != nil {
// we're still in the nav stack, so we've either
// pushed to another VC, or presented a full-screen VC
// (or maybe something else)
return
}
// we've been removed from the nav stack
// so user tapped Back button
print("back button tapped")
// see if we're navigating back to an instance of MyTableViewController
guard let destVC = navC.viewControllers.last as? MyTableViewController else {
return
}
// do something with destVC...
print("on our way to", destVC)
}
Note: I just did this as an example... it would need plenty of testing and possibly additional case handling.

Related

Is it possible to observe changes in presentingViewController?

Is there any equivalent in Swift to RACObserve(self, presentingViewController)?
Or any other why to imitate this behaviour?
My issue is that I want to be notified whenever a view controller is "hidden" by another view controller. In objc what I'd do is to check if self.presentingViewController is nil.
Note that in this scenario there's no knowledge of which view controller is presented, so it's impossible to notify from within its viewDidAppear/viewDidDisappear.
As I understand your question: you need to to know which view controller is presented now and you need notification inviewDidAppear/viewDidDisappear.
So we can get this in several way.
The simple way is:
Get information of which is the top ViewController right now.
2.Call this method in your viewDidAppear/viewDidDisappear
Like this :
Get Which is The Top ViewController
func getTopViewController() -> UIViewController? {
if var topVC = UIApplication.shared.keyWindow?.rootViewController {
while let presentedViewController = topVC.presentedViewController {
topVC = presentedViewController
return topVC
}
return topVC
}
return nil
}
Call in viewDidAppear:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
if let top = getTopViewController() {
print("topView Controller name \(top.title)")
top.view.backgroundColor = .red
}
}
Hope it will help you !

How to invoke a method from a modal view controller class in Swift?

Basically for this simple game app I have 2 different UIViewControllers called ViewController and PreviewController. PreviewController is opening view with the title screen and a label titled "Start game". When the label is tapped, it initiates a modal view controller (the ViewController class that has all the views for the actual game itself) and calls the "EnterNewGame" method from ViewController that sets up the game. Right now the issue I have is when calling this method, only part of the method seems to be running.
Here is the function in PreviewController that is being initiated upon tap:
#objc func handleButtonTap(_ recognizer: UITapGestureRecognizer) {
self.present(ViewController(), animated: true, completion: {() -> Void in
ViewController().enterNewGame()
})
}
And here is the EnterNewGame() method from ViewController
func enterNewGame() {
//show suit indicators when starting a new game
bluePlayerSuitsHidden = false
redPlayerSuitsHidden = false
game.blueTurn = true
self.setBackground()
self.cleanUpBoard()
self.createBoard()
self.displayBoard()
self.setSuitIndicators()
self.highlightCards()
playButton.isEnabled = false
}
Right now, when the label is tapped the screen transitions to the modal view controller but only displays a black screen with only one of the game setups (setting a few images on the top of the screen) working properly. I am sure that the EnterNewGame method works properly to actually start the game because I have tested it in isolation, so I think I am just not setting up the modal view controller properly or I have to call the method differently. Any help is appreciated, thanks.
Controller on which you're calling your method ins't the same instance as controller which you're presenting, you need constant (also your code can be simplified by avoiding using self references and writing name of completion parameter with specifing closure's parameter and return type)
#objc func handleButtonTap(_ recognizer: UITapGestureRecognizer) {
let controller = ViewController()
present(controller, animated: true) {
controller.enterNewGame()
}
}
Also, you can call this method on some other method inside your certain controller like viewDidLoad, viewWillAppear or you can create factory method which would return you certain set controller.
This last part leads me to idea: look how you instantiate your controller and look carefully if you don't need to instantiate it through storyboard or nib file.
class ViewController: UIViewController {
class func instantiate() -> ViewController {
let controller = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Identifier") as! ViewController
// let controller = ViewController(nibName: "ViewController", bundle: nil)
controller.enterNewGame()
return controller
}
}
Usage:
#objc func handleButtonTap(_ recognizer: UITapGestureRecognizer) {
present(ViewController.instantiate(), animated: true)
}

Destination of navigation back button swift

I connected UITableViews with Show segue. I want to change some variable of destination tableView whenever I tap the Back button on navigation bar. Let's say I'm not allowed to create a new back button, how can I detect/call the destination tableView when the back button is tapped? Like in prepareforsegue we have segue.destination, is there some thing like "backbutton.destination"?
Thanks!
What you should be doing here (I think) is not trying to find the destination of the back button but passing in the interested object when going forwards.
On you detail view you could have something like...
protocol DetailViewDelegate {
func detailViewDidChange()
}
Then in the detail view controller...
class DetailViewController: UIViewController {
weak var delegate: DetailViewDelegate?
// you could call this from viewWillDisappear or something
func doThisWhenYouWantToUpdateTheTableView() {
delegate?.detailViewDidChange()
}
}
Now in the tableview...
func prepareForSegue(...) {
if let vc = segue.destination as? DetailViewController {
vc.delegate = self
}
}
and...
extension MyTableViewController: DetailViewDelegate {
func detailViewDidChange() {
// respond to the change here.
}
}

How to detect if a pushed viewcontroller appears again?

assuming I have a viewcontroller (vcA) that pushes QRCodeScannerViewcontroller (vcB). When (vcB) scanned something, It will push ResultviewController (vcC).
-Those 3 views is connected to a UInavigation controller
-the user clicks on the back button on (vcC)
my question is:
1)how can I know if (vcB) is visible without changing code on (vcB)? (vcB) is a pod
2)where will I put this code? I can only access (vcA)
i tried adding this code on (vcA) but nothing happened
override func viewDidDisappear(_ animated: Bool) {
if (vcB.isViewLoaded && (vcB.view.window != nil)){
print("vcb did appear!")
}
}
To know if an instance of cvB's class exists in the navigation stack, you could use this piece of code:
let result = self.navigationController?.viewControllers.filter({
if let vcB = $0 as? UIViewController { // Replace UIViewController with your class, for example ViewControllerB
return true
}
return false
})
if result.isEmpty {
print("An instance of vcB's class hasn't been pused before")
} else {
print("An instance of vcB's class has been pused before")
}

Detail View Controller transition from Master View Contoller

I am getting "unexpectedly found nil while unwrapping an Optional value" because in my code below I am trying to assign value to webview before its initialize. I am trying to transition from Master to Detail view controller.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showDetail" {
if let indexPath = self.tableView.indexPathForSelectedRow() {
let object = self.fetchedResultsController.objectAtIndexPath(indexPath) as NSManagedObject
let controller = (segue.destinationViewController as UINavigationController).topViewController as DetailViewController
controller.detailItem = object
controller.navigationItem.leftBarButtonItem = self.splitViewController?.displayModeButtonItem()
controller.navigationItem.leftItemsSupplementBackButton = true
}
}
}
Detail View Code:
var detailItem: AnyObject? {
didSet {
// Update the view.
self.configureView()
}
}
func configureView() {
// Update the user interface for the detail item.
if let detailContent = detailItem?.valueForKey("content") as? String{
self.webView.loadHTMLString(detailContent as String, baseURL:nil)
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.configureView()
}
It is failing because my Webview in Nil. How do I come around this situation where my outlets are not initialized while setting them.
Please help.
Thanks.
Stop and think about the order in which things happen:
prepareForSegue - The destination view controller exists, but that's all. It has no view and its outlets have not been set. You can set its non-outlet properties but that's all you can do.
The segue starts to happen.
The destination view controller gets viewDidLoad. Now it has a view and its outlets are set.
The segue completes and the destination view controller gets viewWillAppear: and later, viewDidAppear:. Now its view is actually in the interface.
So clearly you cannot permit configureView to assume that the web view exists, because the first time it is called, namely in prepareForSegue, it doesn't exist. configureView needs to test explicitly whether self.webView is nil, and if it is, it should do nothing:
func configureView() {
// Update the user interface for the detail item.
if self.webView == nil { return } // no web view, bail out
if let detailContent = detailItem?.valueForKey("content") as? String{
self.webView.loadHTMLString(detailContent as String, baseURL:nil)
}
}
After that, everything will be fine. viewDidLoad will subsequently be called, and configureView will be called again - and this time, both detailItem and the web view exist, so all will be well.