Check if Popped UIViewController gets dismissed by swipe - swift

I want to check whenever the user swipes a popped viewController away. So for example when in whatsApp a user exits the current chat by swiping from the edge. How is that possible in Swift?
I don't want to use viewDidDisappear, because this method also gets called when another viewController is presented over the current viewController.

As I wrote in comment, a simple workaround would be in viewDidDisappear, check if the navigationController is nil.
class MyVc: UIViewController {
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if navigationController == nil {
print("view controller has been popped")
}
}
}
Of course, this solution works only if the view controller is embedded into a navigation controller, otherwise the if statement will always be true.

This "swipe" is handled by the interactivePopGestureRecognizer of the UINavigationController. It is possible to set the delegate of this gesture recognizer to your UIViewController as follows:
navigationController?.interactivePopGestureRecognizer?.delegate = self
Then, you can implement the UIGestureRecognizerDelegate in your UIViewController. This would look like this:
extension YourViewController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer.isEqual(self.navigationController?.interactivePopGestureRecognizer) else { return true }
print("The interactive pop gesture recognizer is being called.")
return true
}
}
I haven't tested the code, but this should print every time the interactivePopGestureRecognizer is used.
For more information, refer to the documentation of the interactivePopGestureRecognizer and UIGestureRecognizerDelegate.

Related

Close a modal view with a button in Swift?

Learning some view controller basics and am stuck on how to dismiss a modal with a button.
In my slimmed-down example, I have a two view setup with the initial view and the modal. The first view has a button that successfully pops up the modal. On the modal, there is a button that should dismiss itself.
According to other posts and documentation, I should be able to run simple code attached to the button like this:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func CloseModal(_ sender: Any) {
self.dismiss(animated: true, completion: nil)
}
}
When I tap the "Close Modal" button nothing happens. What am I missing here? Am I putting this code in the wrong place? Currently, it's in the main ViewController.swift file.
The other approach is to use an unwind segue to your main view controller. Just add an “unwind action” in the first view controller:
class ViewController: UIViewController {
#IBAction func unwindHome(_ segue: UIStoryboardSegue) {
// this is intentionally blank
}
}
Just give this unwind action a meaningful name, unwindHome in this example, so that, if and when you have multiple unwind actions later on, you can easily differentiate between them.
Then you can control-drag from the button in the second scene to the “exit” control and select the appropriate unwind action:
This has a few nice aspects:
The “close” button no longer cares how you presented it, it will unwind however is appropriate (e.g. if you later change the initial segue to be a show/push segue, the unwind segue will still work without any code changes).
Because you’re now using a segue to unwind, the presented view controller can use its prepare(for:sender:) to send data back, should you eventually need to do that.
If you want, you can unwind multiple scenes. For example if A presents B, and B presents C, you can obviously unwind from C to B, but you can also unwind all the way from C to A if you want.
So, while dismiss works, unwind segues are another alternative.
You actually have two ViewController screens, but it looks like you have one ViewController class? And is the 2nd screen connected to a class?
Must be in the class that belongs to the second screen of the closeModal method.
//This is First ViewController, OpenModal Button is here
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
// The name of the class and the Viewcontroller in the storyboard have to be the same, and CloseModol Button and function need to be here
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func CloseModal(_ sender: Any) {
self.dismiss(animated: true, completion: nil)
}
}
Don't forget to set the name of the ViewController in Storyboad;
from FirstViewController to SecondViewController

iOS 13 Modals - Calling swipe dismissal programmatically

I'd like to detect a modal dismissal in the view controller that's presenting the modal.
This method works amazing for detecting the new iOS 13 swipe dismissal on the new card modals:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "MyIdentifier" {
segue.destination.presentationController?.delegate = self
}
}
extension MyController: UIAdaptivePresentationControllerDelegate {
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
//clean up UI (de-selecting stuff) once modal has been dismissed
}
}
However, presentationControllerDidDismiss is NOT called if the modal dismisses itself programmatically through an action:
#IBAction func btnDismissTap(_ sender: Any) {
self.dismiss(animated: true, completion: nil)
}
Is this a bug or is there a way I can programmatically call whatever the "swipe" dismiss is so I can detect all dismissals the same way? Currently I'm writing extra "dismiss" delegate methods into my modals as a work around and it seems unnecessary.
However, presentationControllerDidDismiss is NOT called if the modal dismisses itself programmatically through an action
self.dismiss(animated: true, completion: nil)
It doesn’t need to be called, because you dismissed the modal yourself, in code. You cannot not know that the modal was dismissed. You don’t need to receive a dismissal signal, because you gave the dismissal signal in the first place.
You typically don’t get a delegate method call reporting something your own code did. Delegate methods report user actions. It would be crazy if everything you yourself did in code came back as a delegate method call.
Mojtaba Hosseini, answer is something I was looking for.
Currently, I need to write a delegate function to let the presenting view know that the user dismissed the modal PLUS do the presentationControllerDidDismiss handler for swipe dismissals:
#IBAction func btnDismissTap(_ sender: Any) {
self.dismiss(animated: true, completion: {
self.delegate?.myModalViewDidDismiss()
})
}
I wanted to handle both of these the same way and Mojtaba's answer works for me. However, presentationControllerDidDismiss does not get invoked if you call it inside of the self.dismiss completion block, you need to call it before.
I adapted my code to use "presentationControllerWillDismiss" (for clarity) and simply called the delegate before I dismiss programmatically in my modals and it works great.
#IBAction func btnDismissTap(_ sender: Any) {
if let pvc = self.presentationController {
pvc.delegate?.presentationControllerWillDismiss?(pvc)
}
self.dismiss(animated: true, completion: nil)
}
Now, I no longer need to create delegate functions to handle modal dismissals in code and my swipe handler takes care of all scenarios.
FYI, what I'm "handling" is doing some UI clean up (de-selections, etc) on the presenting UI once the modal is dismissed.
As #matt mentioned, there is no need to inform the one who dismissed a view by delegate. Because it is already known. BUT if you need that delegate method to be called, you should call it manually your self after dismissing the view:
#IBAction func btnDismissTap(_ sender: Any) {
self.dismiss(animated: true) {
presentationController?.delegate?.presentationControllerDidDismiss?(presentationController!)
}
}

Disable All buttons except Home in Tab Controller

Ive been searching and i cant seem to find out how to disable all tab bar items EXCEPT the home button from being used until a user logs in or creates and account
You can do something like below, create a custom class(TabBarController), extend it from UITabBarController, and write code inside TabBarController class.
Assign TabBarController class to your UITabBarController
extension TabBarController: UITabBarControllerDelegate{
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
// allow your desired controller to be tapped
if tabBarController.selectedIndex == indexOfHomeControllerInTabBar {
return true
}
return false
}
}
Note: Apple doesn't recommend blocking tabbars, for more info check this link https://developer.apple.com/design/human-interface-guidelines/ios/bars/tab-bars/
here is my approach
note that it is kinda a hack way so you might modify as per your needs
you will conform to UITabBarControllerDelegate and make your VC the delegate for it in the viewDidLoad
then in the "didSelect viewController" delegate method callback you will do your logic and override the selected index as below code
class ViewController: UIViewController, UITabBarControllerDelegate {
// MARK: Lifecycle Methods
override func viewDidLoad() {
super.viewDidLoad()
self.tabBarController?.delegate = self
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if /* your logged in logic */ {
self.tabBarController?.selectedIndex = 0 /* assuming that the home is at index 0 */
}
}
}
note if you did this step in base VC it will be much better and code saving for you
You can loop for all items in your Tabbar and disable items you want
for i in 0..<tabbarController.tabBar.items!.count {
let item = tabbarController.tabBar.items![i]
item.isEnabled = i == indexOfHomeTab
}
Put this somewhere in viewDidLoad()
if let viewControllers = self.tabBarController?.viewControllers {
for viewController in viewControllers {
if viewController != viewControllers[0] { // assuming your homeViewController index is 0
tabBarController?.tabBarItem.isEnabled = false
}
}
}
The short answer is probably "Don't do that." Tabs in a tab bar are supposed to let the user switch between always-available screen at the top level of your UI. If you read Apple's HIG I suspect you will find that what you are trying to do is not recommended.
Better to have each of the other screens show some sort of disabled/inactive state.

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)
}

How to stop loading the view on tabbar item selection?

I have a tabbar controller with three tabs . In first tab I have a navigation controller . Now User navigates in the first tab to do some payment so I have disabled the default back buttons cause I dont want user to use back button in between transaction. But when user presses the tab again he/she navigates to the root view . How can I detect the tabbar selection or how do I avoid loading the tab again ?
Please help me on this !! Thank You !!
Note: I am not sure if my question has been already answered on stackoverflow in some other post but I did search and did not get any answer . If so , please feel free to redirect me to that answer and delete this post . Thanx !
Check the UITabBarControllerDelegate Protocol Reference.
The basic idea is that the tabBarController:shouldSelectViewController: selector in your UITabBarController delegate is called whenever the user clicks on tab item.
Thus, by appropriately defining that method, you get a chance to do your own processing before the current view controller is replaced by the one the user selected by clicking in the tab bar.
So, simply return NO from this selector in case you wish to prevent the current view controller to be replaced, i.e. when a transaction is ongoing.
In this way you can open any controller or perform any action on the selection of specific index.
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
let selectedIndex = tabBarController.viewControllers?.firstIndex(of: viewController)!
if selectedIndex == 1{
//Do any thing.
return false
}
return true
}
You have to do it this way..
- (BOOL)tabBarController:(UITabBarController *)tbc shouldSelectViewController:(UIViewController *)vc
{
UIViewController *tbSelectedController = tbc.selectedViewController;
if ([tbSelectedController isEqual:vc])
{
return NO;
}
return YES;
}
In Swift 5:
Continuing Talha Rasool's answer, don't forget to delegate self in viewDidLoad function. This will let current MainTabBarController to handle all the delegate methods
import UIKit
class MainTabBarController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self // Delegate self to handle delegate methods.
}
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
let selectedIndex = tabBarController.viewControllers?.firstIndex(of: viewController)!
if selectedIndex == 1{
//Do anything.
return false
}
return true
}
}
When user navigate to payment controller , you can hide the tabbar.
Use this code in your code , when you navigate to another view
yourcontroller.hidebottombarwhenpushed=YES;