interactivePopGestureRecognizer corrupts navigation stack on root view controller - swift

In my UINavigationController I added custom back buttons with the side effect that it is not possible anymore to swipe left to right to pop the view controller and navigate back.
So I implemented interactivePopGestureRecognizer in my custom UINavigationController class:
class UINavigationControllerExtended: UINavigationController, UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
if self.respondsToSelector(Selector("interactivePopGestureRecognizer")) {
self.interactivePopGestureRecognizer?.delegate = self
}
}
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailByGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return gestureRecognizer.isKindOfClass(UIScreenEdgePanGestureRecognizer)
}
}
This works fine except when I am in my root view controller (RVC) which is a UICollectionViewController, i.e. the most bottom view controller in the navigation stack. When I do the swipe left to right gesture, nothing seems to happen, as expected. But when I then tap a UICollectionViewCell the destination view controller (DVC) does not get pushed over the RVC. Instead I only see the DVC's shadow on the right side of the screen.
The RVC is not responsive anymore, but as I swipe left to right again, the DVC interactively moves right to left into the screen. When I finish the gesture, the DVC moves completely into the screen just to quickly disappear left to right again. The RVC becomes responsive again.
So it seems the DVC gets pushed onto the navigation stack but not visibly into the screen.
Any suggestions where this strange behaviour originates?

Implement UINavigationControllerDelegate for your navigation controller and enable/disable the gesture recognizer there.
// Fix bug when pop gesture is enabled for the root controller
func navigationController(navigationController: UINavigationController, didShowViewController viewController: UIViewController, animated: Bool) {
self.interactivePopGestureRecognizer?.enabled = self.viewControllers.count > 1
}
Keeping the code independent from the pushed view controllers.

My current solution is to disable the interactivePopGestureRecognizer in the root view controller:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.navigationController?.interactivePopGestureRecognizer?.enabled = false
}
In the first child view controller I enable it again. But this seems to be more a workaround because I don't understand the actual problem why the navigation stack got messed up in the first place.

Here is my solution for this. Swift 4.2
Implement UIGestureRecognizerDelegate and UINavigationControllerDelegate protocols. In my case I did this in an UITabBarController that would be also my root view controller.
Then, on viewDidLoad, do:
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.interactivePopGestureRecognizer?.delegate = self
self.navigationController?.delegate = self
}
Then, add the delegate method for UINavigationControllerDelegate and check if it is the root view by counting the number of view on the navigation controller and disable it if it is or enable it if its not.
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
let enable = self.navigationController?.viewControllers.count ?? 0 > 1
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = enable
}
Lastly, add the UIGestureRecognizerDelegate method
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
This should fix the problem without the necessity of manually enabling/disabling the gesture recogniser in every view of your project.

func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
self.interactivePopGestureRecognizer?.isEnabled = self.viewControllers.count > 1
}
Updated #rivera solution for Swift 5

Solution from Guilherme Carvalho worked for me, but I had to assign delegate methods in viewWillAppear, in viewDidLoad was too late for my implementation.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.interactivePopGestureRecognizer?.delegate = self
navigationController?.delegate = self
}

Try this code
func navigationController(navigationController: UINavigationController, didShowViewController vc: UIViewController, animated: Bool) {
self.interactivePopGestureRecognizer?.enabled = self.vc.count > 1
}

Related

Reload main view after modal dismiss

In my Xcode-App, a modal can be opened from every view. Every 'base' view is having a different purpose, some are showing a table, some are doing not. How can I achieve to reload the 'base' view whenever the modal is dismissed?
It seems to be especially tricky as the views have such different structures and purposes. I tried viewWillAppear, viewDidAppear and viewDidLoad, but none of them seem to do the trick.
You can setup a delegate pattern so that your modal view can notify when it will or did disappear.
First you need to create a protocol for your delegate:
protocol ModalViewControllerDelegate: class {
func modalControllerWillDisapear(_ modal: ModalViewController)
}
Then your modal should have a delegate property (that will in the end be the presenting controller) and trigger the modalControllerWillDisapear method when needed:
final class ModalViewController: UIViewController {
weak var delegate: ModalViewControllerDelegate?
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
delegate?.modalControllerWillDisapear(self)
}
}
And all view controller that will present your modal controller must conform to that protocol and assign itself as a delegate of the modal when presenting:
final class SomeViewController: UIViewController {
private func presentModalController() {
let modal = ModalViewController()
modal.delegate = self
self.present(modal, animated: true)
}
}
extension SomeViewController: ModalViewControllerDelegate {
func modalControllerWillDisapear(_ modal: ModalViewController) {
// This is called when your modal will disappear. You can reload your data.
print("reload")
}
}
Note: If you are using segues to present your modal, you can assign the delegate property in the prepare(for:sender:) method instead of in a custom method.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
switch (segue.identifier, segue.destination) {
// Check that the segue identifer matches and destination controller is a ModalViewController
case ("showModalSegue", let destination as ModalViewController):
destination.delegate = self
case _:
break
}
}

iOS 13 UISplitView Problems

On iOS 13 Beta 5, I currently have problems with my UISplitView on iPhones.
My app starts with the detailsview off my splitview not with my masterview (look at the picture)
Does anyone know how i can fixed this problem under iOS 13? On iOS 12 everything works like a charm ☹️
Thx in advance Sebastian
Edit:
Sorry for the late answer I was on a short holiday trip without any internet :/
my Class looks like this:
class MyClass : UITableViewController, UISplitViewControllerDelegate, UIPickerViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
if (UIDevice.current.userInterfaceIdiom == .pad){
navigationController?.navigationBar.isTranslucent = false
}
/*SplitView*/
splitViewController?.preferredDisplayMode = .allVisible
splitViewController?.delegate = self
self.definesPresentationContext = true
}
// SplitView
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
return true
}
}
I think it's look like the normal procedure for this problem :/
I had the same issue.
After a bit of investigation it seems like viewDidLoad is too late to set it to all visible.
I subclassed the UISplitViewController and changed the setting in awakeFromNib method. Now it is working as expected.
Alxlives's answer helped me. By using the debugger, I notice that in the master view controller, viewDidLoad is not called. So the delegate is never set, thus the collapse method is never called.
I fixed this by setting the delegate in awakeFromNib():
override func awakeFromNib() {
self.splitViewController?.delegate = self
}
Now the splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool is called, and if you return true, the master will be displayed.
Warren Milward's answer helped me to guide me in the right direction, but actually I got it working with viewDidLoad().
I ended up using a subclass for the UISplitViewController and set the required values in viewDidLoad() as well as the delegate calls here.
class CustomSplitViewController: UISplitViewController, UISplitViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
self.preferredDisplayMode = .allVisible
}
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
return true
}
}
did you try this one (UISplitViewControllerDelegate):
self.preferredDisplayMode = .allVisible
&
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
return true;
}
Try MasterViewController_Instance.view.layoutIfNeeded() inside Splitviewcontroller ViewDidLoad() method. It fixed my problem.
class CustomSplitController: UISplitViewController{
override public func viewDidLoad() {
MASTER_CONTROLLER_INSTANCE.view.layoutIfNeeded()
//If you are using navigation controller in master controller try
MASTER_CONTROLLER_INSTANCE.navigationController?.view.layoutIfNeeded()
}
}
I set the spliteViewController's delegate on appdelegate, and it worked
For those who use storyboard and configure the new controller in a subclass from UIStoryboardSegue, it'll be simpler:
just before
[source presentViewController:destination animated:YES completion:nil];,
just set destination.modalPresentationStyle = UIModalPresentationFullScreen;, because the default is now UIModalPresentationPageSheet.
As many answers here leads to change in the Split View Controller starting sequence, I realized, that on compact size it performs vewDidLoad on detail controller ONLY. But awakeFromNib is called on every controller. So - I've just transfer my code from viewDidLoad to awakeFromNib and everything now is works perfectly!
override func awakeFromNib() {
super.awakeFromNib()
if let splitController = splitViewController {
splitController.delegate = self
}
}

self.tabBarController.selectedIndex not calling viewDidAppear

I've been looking at and trying all the solutions others have posted to this problem, but I'm still not getting it.
My use case is very simple. I have a viewController with a button, when that button is pressed I want to navigate to another tab and reload the data including an api call.
When using the button, I navigate to the tab fine, but viewDidAppear is not being called.
If on another tab, and navigate using the tab bar, viewDidAppear works fine. Also viewWillAppear is working, but I have to add a manual delay to the functions I want to call so it's not ideal.
So what do I need to do to navigate using self.tabBarController.selectedIndex = 0 and get the functionality of viewDidAppear?
Update: The viewWillAppear method I added gets called but I have to add a delay to my functions in order for them to work, and it's a bit clunky, not ideal. Not sure why viewDidAppear will not work :(
Here is a screenshot of the structure:
I appreciate any help on this one!
The "current" ViewController is my tab index 2:
import UIKit
class PostPreviewVC: UIViewController {
//Here I create a post object and post it to the timeline with the below button
#IBAction func postButtonPressed(_ sender: Any) {
//create the post via Firebase api
self.tabBarController?.selectedIndex = 0
}
}
In my destination viewController:
import UIKit
import Firebase
import SDWebImage
import AVFoundation
class HomeVC: UIViewController {
// MARK: - PROPERTIES
var posts = [Post]()
let refreshControl = UIRefreshControl()
//more properties...
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
configureTableView()
reloadTimeline()
UserFirebase.timeline { (posts) in
self.posts = posts
self.tableView.reloadData()
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("viewDidAppear")
_ = self.view
setupUI()
configureTableView()
reloadTimeline()
UserFirebase.timeline { (posts) in
self.posts = posts
self.tableView.reloadData()
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("viewWillAppear")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.reloadTimeline()
self.configureTableView()
}
}
//All the tableview code below here...
}
Added a custom class for my tab bar controller:
import UIKit
class TabBarController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("viewDidAppear in tabBar custom Class called")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("viewWillAppear in tabBar custom Class called")
}
}
When you are using UITabBarController, the method viewDidLoad will called only once when any UIViewController is loaded in memory. After that, when you are navigating the same UIViewController, then it will load from memory.
In order to overcome this problem, you must divide your code in viewDidLoad & viewDidAppear. So, in viewDidLoad, you only put that code which you want to intialize once throughout the app such as adding UIView's or other things, while in viewDidAppear / viewWillAppear, you can make API calls or some functions which fetches dynamic data.
Finally, when you are calling self.tabBarController.selectedIndex = 0, it will call viewDidLoad only once and viewDidAppear / viewWillAppear every time when you are navigating that UIViewController.
Hope this helps to understand like how UITabBarController works.
For UITabBarController viewDidLoad only gets called once. and your viewWillAppear and viewDidAppear get called multiple times. you can either check if your viewWillAppear gets called or not. because your view will appear gets called before your viewDidAppear it's just like going through the reverse engineering process.
You can also add viewDidAppear method into your UITabBarController custom class. and call its superclass method into it in that way I think it will solve your problem.
Note: In the case of UITabbarController, Always do your UI update task and API calling a task in either
viewWillAppear or viewDidAppear

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.

Opening a side menu on click on a TAB bar in SWIFT

I want to make something like below snapshots.
When I click on profile tab bar instead of opening a new view controller it shows a side menu. Is it something that has been handled on click of tabbar ?
if you want to achieve something like your screenShot then you are using a wrong library, because when you show your right viewController the front viewController go to left by amount of width of your right viewController, but anyways here is the code for what you need to do
first you need to put your viewController as delegate of your TabBarViewController and in func tabBarController(tabBarController: UITabBarController, shouldSelectViewController viewController: UIViewController) -> Bool you need to return false and call the method of SWRevealViewController to show right viewController rightRevealToggleAnimated(true)
class FirstViewController: UIViewController,SWRevealViewControllerDelegate,UITabBarControllerDelegate {
#IBOutlet weak var sliderControl: UISlider!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.revealViewController().delegate = self
self.tabBarController?.delegate = self
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
//self.view.removeGestureRecognizer(self.revealViewController().panGestureRecognizer())
//self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tabBarController(tabBarController: UITabBarController, shouldSelectViewController viewController: UIViewController) -> Bool {
//checking for specific viewController
if(viewController is DesiredViewControllerClass) {
self.revealViewController().rightRevealToggleAnimated(true)
}
return false
}
}
I hope this helps you, regards
You can use the tab bar delegate:
extension ViewController: UITabBarDelegate {
func tabBar(tabBar: UITabBar, didSelectItem item: UITabBarItem) {
// Present hamburger menu
}
}