Swift: How to dismiss a ViewController programmatically? - swift

I got a little problem.
On my main view controller I got a bar button that opens a slide menu, which is a regular view controller using a slide in transition. The slide menu has a button to open another view controller. When the new view controller is opened, you have the option to cancel, which dismisses the current view controller. The problem is, that the user ends up in the menu view once again, instead of the main view controller. Would be very happy to know what I am doing wrong :)
func openSupport() {
guard let creditViewContoller = storyboard?.instantiateViewController(withIdentifier: "support") as? CreditViewController else { return }
present(creditViewContoller, animated: true)
}
#IBAction func buttonSupport(_ sender: UIButton) {
let menuView = MenuViewController()
menuView.dismiss(animated: true, completion: nil)
openSupport()
print("Tap on Support")
}

you can dismiss view controller simply by using
self.dismiss(animated: true, completion: nil)

Consider
#IBAction func buttonSupport(_ sender: UIButton) {
let menuView = MenuViewController() // (1)
menuView.dismiss(animated: true, completion: nil) // (2)
openSupport() // (3)
print("Tap on Support")
}
This:
Creates new MenuViewController but never presents it;
Calls dismiss on view controller that was never presented; and
Calls openSupport from this MenuViewController instance (which was never dismissed).
Bottom line, you want to let the main view controller that presented the menu do the presenting. So, the menu view controller should:
Define a protocol for it to inform the presenting view controller to transition to the next scene:
protocol MenuViewControllerDelegate: class {
func menu(_ menu: MenuViewController, present viewController: UIViewController)
}
And then the menu view controller can, when it’s done dismissing, tell its delegate what it should present:
class MenuViewController: UIViewController {
weak var delegate: MenuViewControllerDelegate?
#IBAction func didTapSupport(_ sender: Any) {
dismiss(animated: true) {
guard let controller = self.storyboard?.instantiateViewController(withIdentifier: "support") else { return }
self.delegate?.menu(self, present: controller)
}
}
#IBAction func didTapCancel(_ sender: Any) {
dismiss(animated: true)
}
}
Then the main view controller needs to
Make sure to set the delegate of the menu view controller:
class ViewController: UIViewController {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destination = segue.destination as? MenuViewController {
destination.delegate = self
}
}
}
and
Make sure to present the view controller that the menu controller asked it to:
extension ViewController: MenuViewControllerDelegate {
func menu(_ menu: MenuViewController, present viewController: UIViewController) {
present(viewController, animated: true)
}
}
There are lots of different ways of achieving this, so don’t get lost in the details here. But the idea is to have some system by which the menu view controller can request whomever is to present the support view to do so, not try to do it itself.

Related

Perform function on Dismiss of a View Controller

i am calling a GET(method) API on viewDidAppear function of a view controller. i am presenting a new view controller using navigation controller over my first view controller. on the second view controller i am calling an API of Post Method to add another entry into my previous screen Get method API. But when I dismiss the second View Controller the Get API data remains the same and when i again runs the code the data was updated on the first view controller. Can someone tell me that how to check on first view controller that my second view controller is dismissed so that i can call API there.
I got the solution for this. It didn't work by calling the API on viewDidAppear() or viewWIllAppear() . This will be done by using swift closures.
Below is the code:
class 1stViewController: UIViewController {
#IBAction func buttonTapped(_ sender: UIButton) {
guard let secondViewController = self.storyboard?.instantiateViewController(withIdentifier: "SecondViewController") as? SecondViewController else { return }
secondController.callbackClosure = { [weak self] in
print("call API")
}
self.navigationController?.pushViewController(secondController, animated: true)
}
}
On Second view Controller:
class SecondViewController: UIViewController {
var callbackClosure: ((Void) -> Void)?
override func viewWillDisappear(_ animated: Bool) {
callbackClosure?()
}
}

do something after first view controller appears

I have a View Controller with a button..
This button calls a "Present as Popover Seague" to a second view controller.
The second view controller has a close button with this function:
#IBAction func exit(_ sender: UIButton) {
self.dismiss(animated: true, completion: nil)
}
Now I would like to do something in the first controller, after the second Controller is dismissed.
In the first view controller I tried this functions:
override func viewDidAppear(_ animated: Bool) {
print("viewDidAppear")
}
override func viewWillAppear(_ animated: Bool) {
print("viewWillAppear")
}
but no console log will shown.
Where is my mistake?
FirstViewContorller
import UIKit
class firstVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//ERROR
secondVC.dismissCompletion = {
print("dismissCompletion")
}
}
}
SecondVC (popover)
import UIKit
class secondVC: UIViewController {
var dismissCompletion: (() -> Void)?
// EXIT POPOVER
#IBAction func exit(_ sender: UIButton) {
self.dismiss(animated: true, completion: dismissCompletion)
}
}
The viewDidAppear() method of the main view controller won't be called because of the popover presentation style you use. If you choose to present the second view controller full screen - those methods will fire.
If we're sticking with the popover, the first thing you need to do is in your second view controller, the one that's being presented, add a property for a closure that will be executed upon its dismiss:
class PopoverViewController: UIViewContoller {
var dismissCompletion: (() -> Void)?
#IBAction func exit(_ sender: UIButton) {
self.dismiss(animated: true, completion: dismissCompletion)
}
}
And in your main view controller you define what needs to be done upon the popover's dismiss:
popoverViewContoller.dismissCompletion = {
// do stuff
}
UPDATE:
I assume you've setup the segue in your storyboard. I also assume that in your storyboard you've given the view controllers their respective class names:
This is what your code should look like:
class FirstViewController: UIViewController {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let secondVC = segue.destination as? SecondViewController else {
return
}
secondVC.dismissCompletion = {
print("Popover dismissed")
}
}
}
class SecondViewController: UIViewController {
var dismissCompletion: (() -> Void)?
#IBAction func exit(_ sender: Any) {
dismiss(animated: true, completion: dismissCompletion)
}
}
Please note the classes naming and the way I got the secondVC instance.

How to reload data in UITableView when popping view controller

If I have two ViewControllers one which contains a UITableView and another which updates data in the tableView. How would I reload the table data once I pop of the viewController and go back to the view with the tableView?
I already tried using viewDidAppear
You could use viewWillAppear just like Rajesh suggested:
override func viewWillAppear(_ animated: Bool) {
tableView.reloadData()
}
Or you could use a callback function to pass data and reload view controller 1's tableview.
In ViewController 2, define your callback function:
// Callback function
var callbackResult: ((data) -> ())?
And call it before going back to ViewController 1:
callbackResult?(data)
self.navigationController?.popViewController(animated: true)
In ViewController 1, use the callback function's closure to collect the result and reload your tableView. This can happen inside prepareForSegue, for example:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "goToViewController2" {
let destinationVC = segue.destination as! ViewController2
// Set any variable in ViewController2
destinationVC.callbackResult = { result in
// assign passing data etc..
self.tableView.reloadData()
}
}
}
You may try doing something like this:
class TableViewController: UITableViewController {
func showUpdatingViewController() {
let vc = UpdatingViewController()
vc.onUpdate = { [weak self] in
self?.tableView.reloadData()
}
navigationController?.pushViewController(vc, animated: true)
}
}
class UpdatingViewController: UIViewController {
var onUpdate: (() -> Void)?
func updatesFinished() {
onUpdate?()
dismiss(animated: true, completion: nil)
}
}
I was having similar issue and using viewWillAppear or viewDidAppear did not help me to reload the tableview.
I solved my situation by putting the reloadData() call within the unwindSegue
#IBAction func unwindToVCSetupDataScreen(_ unwindSegue: UIStoryboardSegue) {
/// Nothing is actually needed here
/// https://www.youtube.com/watch?v=WaSlHXNah7E #6:25
/// CTRL-Drag from Back button to the "exit" square at the very top of VC
tableView.reloadData()
}

Send data only if view controller is being popped off stack by swipe or back button press but not by switching tabs

I looked through SO and compiled these methods below but none of them works for me.
I have a TabBarController with 2 tabs. In the Second tab I have a NavigationController > TableViewController > DetailViewController.
In my DetailViewController I have a custom delegate to send some data to the TableViewController when the Back Button is pressed or the view is Swiped to Dismiss (right swipe). I only want the data sent when the Back Button or Swipe to Dismiss is fully finished and not get sent when the tab is switched or if swiping 3/4 of the way but the user decides NOT to complete the back swipe (basically they stay on the same DetailVC scene).
I tried all of these methods below and they either get triggered when the tab is switched to the first tab, when the DetailVC gets pushed on and popped off, or during the 1/2 way Swipe to Dismiss the DetailVC they still run meaning the data should not have been sent.
DetailViewController:
protocol DetailViewDelegate: class {
func sendSomeData(value: Bool)
}
class DetailViewController: UIViewController{
weak var delegate: DetailViewDelegate?
//1. runs when Tab switches, the Back Button is pressed, and Swipe to Dismiss is triggered
override func viewWillDisappear(_ animated : Bool) {
super.viewWillDisappear(animated)
if (self.isMovingFromParentViewController) || (self.isBeingDismissed){
//doesn't run at all
}else{
//runs whenever view is no longer on scene
sendData()
}
}
//2. runs when Tab switches, Back Button is pressed, Swipe to Dismiss is triggered, and when the view is Pushed on AND Popped off
override func didMove(toParentViewController parent: UIViewController?) {
if parent != nil {
sendData()
}else{
//if parent == nil doesn't run at all
}
}
//3. if switching from the second tab it doesn't run but when switching back to the second tab it does run, also runs when view is being Pushed on and Not Popped on
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
sendData()
}
//4. if switching from the second tab it doesn't run but when switching back to the second tab it does run, also runs when view is being Pushed on and Not Popped on
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
sendData()
}
//MARK:- Custom Func
fileprivate func sendData(){
let value = true
delegate?.sendSomeData(value: value)
}
}
TableViewController:
class TableVC: UIViewController, DetailViewDelegate, UITableViewData..., UITableViewDele...{
var setValue = false
func sendSomeData(value: Bool){
//setValue should only update to true if DetailVC's Back Button is pressed or Right Swipe to Dismiss is fully complete
self.setValue = value
}
}
The TableView never has a problem receiving the data. The problem is when I switch tabs (data still gets sent) or a swipe to dismiss on the DetailVC isn't fully completed (data still gets sent).
What's the best way to send the data from the DetailVC but making sure the Back Button is pressed or Right Swipe to Dismiss is fully complete?
You need to use custom back button and a delegate to call the parent.
this is your parent ViewController:
import UIKit
class ViewController: UIViewController, ViewControllerSecondDelegate {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "Next" {
let vc = segue.destination as? ViewControllerSecond
vc?.delegate = self
}
}
func secondDelegate() {
print("delegate") //GetData()
}
}
and this is the child view controller, which you want to back from it to your parent:
import UIKit
protocol ViewControllerSecondDelegate {
func secondDelegate()
}
class ViewControllerSecond: UIViewController, UIGestureRecognizerDelegate {
var isTouched = false
var isPopTouch = true
var delegate: ViewControllerSecondDelegate?
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.setNavigationBarHidden(false, animated:false)
let myBackButton:UIButton = UIButton.init(type: .custom)
myBackButton.addTarget(self, action: #selector(ViewControllerSecond.popToRoot(sender:)), for: .touchUpInside)
myBackButton.setTitle("Back", for: .normal)
myBackButton.setTitleColor(.blue, for: .normal)
myBackButton.sizeToFit()
let myCustomBackButtonItem:UIBarButtonItem = UIBarButtonItem(customView: myBackButton)
self.navigationItem.leftBarButtonItem = myCustomBackButtonItem
self.navigationController?.interactivePopGestureRecognizer?.delegate = self
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isTouched {
isPopTouch = true
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.isPopTouch = false
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if isPopTouch {
delegate?.secondDelegate()
}
}
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer == self.navigationController?.interactivePopGestureRecognizer {
self.isTouched = true
}
return true
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.isTouched = false
}
func popToRoot(sender:UIBarButtonItem){
delegate?.secondDelegate()
self.navigationController?.popToRootViewController(animated: true)
}
}
the above code, handle back button and back gesture.

nothing happens on trying to dismiss popover viewcontroller Swift

I have a viewcontroller that is presented as popover when the user clicks on an ImageView.
The problem is, I added a button to dismiss it but when I tap on it nothing happens.
The code I have is:
#IBAction func onCloseTapped(_ sender: Any) {
presentedViewController?.dismiss(animated: true, completion: nil)
}
I've also tried dismiss(animated: true, completion: nil) and other methods, but still nothing.
Can anyone tell me what I am doing wrong?
Edit: Posting a screenshot:
Edit 2: I'm presenting it from the storyboard. I've added a gesture recognizer on the image, then added segue from the storyboard that says present as popover, then anchor to the image.
it won't work because when you show as popover, the viewController doesn't have the navigationController. You have to create a delegate method and use the dismiss function on the viewController that make the call to the popover.
Here an exemple:
make the popover delegate, in the popover viewController:
protocol PopoverViewControllerDelegate: NSObjectProtocol {
func dismiss()
}
then you create a delegate variable and call when the button is tapped:
var delegate: PopoverViewControllerDelegate?
#IBAction func onCloseTapped(_ sender: Any) {
delegate?.dismiss()
}
Now in the viewController that call the popover you override the prepare for segue method to set the popover delegate:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "popover" {
if let vc = segue.destination as? PopoverViewController {
vc.delegate = self
}
}
}
Now you just need to use the delegate to dismiss your popover viewController:
extension ViewController: PopoverViewControllerDelegate {
func dismiss() {
navigationController?.dismiss(animated: true, completion: nil)
}
}
Don't forget to put the identifier for you segue, the identifier that we use is this = "popover"
Hope that help you.