iOS 13 Modals - Calling swipe dismissal programmatically - swift

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

Related

Check if Popped UIViewController gets dismissed by swipe

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.

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

How reload view controller like first time in Swift?

How reload viewcontraller like first time in Swift 4?
Details: when run app then enter viewcontraller then exit from it and I want back to same viewcontraller I want download viewcontraller from new same first time.
//code close viewcontraller :
self.dismiss(animated: true, completion: {})
//code on enter or back to viewcontraller :
DispatchQueue.main.async {
self.performSegue(withIdentifier: "sbmpo2", sender: nil)
}
Assign data to the ViewController or making API calls inside viewWillAppear that will call everytime when you came to that viewcontroller. Only you have to do to call data population functions/API call inside viewWillAppear
override func viewWillAppear(_: Bool) {
super.viewWillAppear(true)
//call your data populating/API calls from here
}
Hope this will help you
Take a look at the following methods of UIViewController:
viewWillAppear(_:)
viewDidAppear(_:)
viewWillDisappear(_:)
viewDidDisappear(_:)
The first two are called each time the view is shown. The last two are called each time the view goes away.
You can override these methods in your ViewController to setup your view as you wish.
self.dismiss(animated: true) {
//UIViewController is dismissed
DispatchQueue.main.async
self.performSegue(withIdentifier: "sbmpo2", sender: nil)
}
}
Hope this will help you

Perform UnwindSegue correctly

I am trying to create an Unwind segue that brings from the DetailViewcontroller to the SecondViewcontroller.
I have created an IBAction like this one that can be intercepted by Segues :
#IBAction func segueToSecondVC(sender: UIStoryboardSegue) {
print("segue intercepted by IBAction segueToSecondVC")
}
An placed it in the receiver ViewController (secondViewController).
Then I moved to the Detailviewcontroller and clicked to the third figure on the top of the viewcontroller (exit),clicked to the segueToSecondVC IBAction that appeared in the list of events and dragged it into the button "TURN BACK"(placed in the Detailviewcontroller) in order to associate the button to the event.
In this way the button should be connected with the event and should turn back to the secondVC with an automatic Hidden Segue.
But it doesn't work, the simulator crash and I am redirected to xCode editor with an error in the Appdelegate that says "Thread 1: signal SIGABRT"
I don't know why it doesn't work, anyone could help me?
you can use Unwind like this:
#IBAction func prepareForUnwind(segue: UIStoryboardSegue){}
#IBAction func closePressed(_ sender: Any) {
performSegue(withIdentifier: "unwindToAnotherVC", sender: nil)
}
good rule, is to add all your segues names to constants file:
Create swift file
import Foundation
// Segues
let TO_LOGIN = "toLogin"
let TO_CREATE_ACCOUNT = "toCreateAccount"
let UNWIND = "unwindToChannel"
and then use UNWIND in
#IBAction func closePressed(_ sender: Any) {
performSegue(withIdentifier: UNWIND, sender: nil)
}

open other view with segue only works by pressing button

I want to open an other view controller, after checking if it is the first run of the app.
It works when I press a button but not when I call the method openMap
class TutorialController: UIViewController {
override func viewDidLoad() {
//check if the app opens for the first time
if(UserDefaults.standard.bool(forKey: "HasLaunchedOnce"))
{
// app already launched
print("not first launch")
openMap()
}
else
{
// This is the first launch ever
UserDefaults.standard.set(true, forKey: "HasLaunchedOnce")
UserDefaults.standard.synchronize()
print("first launch")
openTutorial()
}
}
func openTutorial(){
}
#IBAction func openMap(){
print("openmap opened")
performSegue(withIdentifier: "openMap", sender: nil)
}
}
I assume, you've connected your button to #IBAction func openMap()
if so, you should not call openMap() action inside your viewDidLoad, but use the same code performSegue(withIdentifier: "openMap", sender: nil) instead in your viewDidAppear:
if(UserDefaults.standard.bool(forKey: "HasLaunchedOnce"))
{
// app already launched
print("not first launch")
performSegue(withIdentifier: "openMap", sender: nil)
}
...
If it doesn't work, you've probably made a mistake with creation of your segue and have connected Button to the destination ViewController directly in your storyboard instead of connecting two controllers:
If so, just remove the old segue, and re-crete it in the way as it is on the image above and assign the same segue id "openMap"
EDITED:
Please, move performing of your segue to the viewDidAppear instead of viewDidLoad, because viewDidLoad is called when the ViewController object is created and it's not yet attached to the window.
Ok, from what I understand is that you want to perform a segue "openMap" when it HasLaunchedOnce. Well what you're doing wrong is that you're calling an #IBAction func. This is my suggestion
if you still want to have that button
create a function and name if whatever you want. Inside this function perform this segue. Link this function to the if else statement and the button.
eg:
//if else statement
if(UserDefaults.standard.bool(forKey: "HasLaunchedOnce"))
{
// app already launched
print("not first launch")
anotherFunction()
}
//#ibaction (scrap this if you don't want the button)
#IBAction func openMap()
{
print("openmap opened")
anotherFunction()
}
//another function
func anotherFunction()
{
performSegue(withIdentifier: "openMap", sender: nil)
}
hope this helps