4-level ViewController navigation in iOS app using custom transitions - ios

I'm trying to build a menu with 4 different levels, where each level represents a UIViewController. I want the menu and each of its levels to be presented modally. Currently, I'm doing this by letting a base ViewController (VC) present another, which in turn presents a 3rd which presents a 4th (the structure would be A-B-C-D where each - represents a call to UIViewController.present(_:animated:completion:) for the respective level).
I'm then implementing the UIViewControllerTransitioningDelegate and supplying it with a custom transition object. This works well for the first 3 levels (ie. the menu works as expected for level A-B-C), but when I'm on the 4th level and trying to dismiss it (ie. going from A-B-C-D to A-B-C ), the B level is briefly showing instead of the C level while the dismissal-animation is playing. After the animation is done, level C pops up over B again.
I suspect the problem lies in presenting more than 3 VC:s in a hierarchy somehow, but this does not occur when using the default dismissal animation instead of the custom one I'm using.
Maybe I should go for a different navigation approach, or is there some way to circumvent this behavior? I do need the menu to be "modal-style" which from what I can tell would prevent me from using a UINavigationController (I don't want the navigation bars).
Help is highly appreciated.
--- EDIT ---
Here is a "pseudo code" example of what I'm trying to accomplish, as the project is quite big and I'd have trouble pasting it all:
class A: UIViewController: UIViewControllerTransitioningDelegate {
func viewDidLoad(){...}
.
.
.
#objc func presentB(){
self.present(B, animated: true, completion: nil)
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CustomTransition(presenting: true)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CustomTransition(presenting: false)
}
}
class B: UIViewController: UIViewControllerTransitioningDelegate {
func viewDidLoad(){...}
.
.
.
#objc func dismiss(){
self.dismiss(animated: true, completion: nil)
}
#objc func presentC(){
self.present(C, animated: true, completion: nil)
}
func animationController(forPresented:presenting:source:) -> UIViewControllerAnimatedTransitioning? { ... }
func animationController(forDismissed:) -> UIViewControllerAnimatedTransitioning? {...}
}
class C: UIViewController: UIViewControllerTransitioningDelegate {
func viewDidLoad(){...}
.
.
.
#objc func dismiss(){
self.dismiss(animated: true, completion: nil)
}
#objc func presentD(){
self.present(D, animated: true, completion: nil)
}
func animationController(forPresented:presenting:source:) -> UIViewControllerAnimatedTransitioning? { ... }
func animationController(forDismissed:) -> UIViewControllerAnimatedTransitioning? {...}
}
class D: UIViewController {
func viewDidLoad(){...}
.
.
.
#objc func dismiss(){
self.dismiss(animated: true, completion: nil)
}
}

Related

Generic function to pop or dismiss ViewController in iOS & iPadOS just like show() & showDetail()

Just like we have show(_ vc: UIViewController, sender: Any?) & showDetailViewController(_ vc: UIViewController, sender: Any?) to push & present ViewControllers irrespective of the case whether they are embedded in UINavigationController or UISplitController.
Do we have something generic just like these to pop/dismiss a ViewController ?
I'm not sure I understand what you're asking...
There is a UIViewController function called dismiss(animated: Bool, completion: (() -> Void)?) and navigationController?.popViewController(animated: Bool).
if you're looking for something that would do either, I'd imagine it would look something like
extension UIViewController {
func dismissPop(animated: Bool) {
if let navigationController = navigationController {
navigationController.popViewController(animated: animated)
} else {
dismiss(animated: animated)
}
}
}
A completion handler could be added as well if necessary.

How to transition out of parent UINavigationController

I have the following code, which setups and shows a UIViewController to be embedded into a UINavigationController:
private func transitionToMainVC() {
let vc = UINavigationController(rootViewController: SpacesVC())
DispatchQueue.main.async {
self.show(vc, sender: self)
}
}
Now, inSpacesVC I want to show() another UIViewController, but outside of the "parent" UINavigationController.
When I use the following code in SpacesVC:
// Called by a button touch up inside
#objc private func transitionToMainVC() {
let vc = NextVC()
self.show(vc, sender: self)
}
It transitions to NextVC but it shows the navigation bar at the top of the screen; i.e. the new view controller is still embedded under the UINavigationController defines in the first snippet.
I am aware of the possibility of hiding the navigation bar in NextVC as such:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navController.isNavigationBarHidden = true
}
But I want to show() the NextVC without embedding it in the navigation controller, since I won't need that anymore. How can I do that?
Use this method instead of show.
func present(_ viewControllerToPresent: UIViewController,
animated flag: Bool,
completion: (() -> Void)? = nil)

SFSafariViewController not dismissing properly (iOS 10.3)

In my app I have a SFSafariViewController that I am displaying modally. Upon dismissal, the presenting ViewController does not have its dismiss method called. Code for my subclass of UIViewController:
override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
print("will present")
super.present(viewControllerToPresent, animated: flag) {
completion?()
print("did present")
}
}
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
print("will dismiss)")
super.dismiss(animated: flag) {
completion?()
print("did dismiss")
}
}
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
print("finished with \(controller)")
}
func testSVC() {
let svc = SFSafariViewController(url: URL(string: "https://www.stackoverflow.com")!)
svc.delegate = self
self.present(svc, animated: true) {
print("presented \(svc)")
}
}
Calling testSVC() and then tapping "Done" in the SafariViewController produces the following output:
will present
presented <SFSafariViewController: 0x7fd981b2f200>
did present
finished with <SFSafariViewController: 0x7fd981b2f200>
And that's it. The dismiss print statements are missing. Can anyone help me figure out why dismiss is not being called? I thought that all UIViewController dismissals were forwarded to the presenting UIViewController.
When you dismiss the SFSafariViewController the dismiss method for svc is fired not the ViewController you're in. If you want to override the dismiss for the SFSafariViewController then you should subclass it and override the dismiss func in that subclass.

Pop a navigation controller

I have two views, V1 and V2. I want to "present" V2 when the add button is pressed on V1, and "pop" V2 off when the stop button is pressed, so that the original V1 is the top of the stack.
From what I have read, I need a separate view controller for V2. From the limited information I could find, I need V1's view controller to conform to V2's protocol, V2delegate. This is what I have, but it is not working:
ViewController1 with V1
class HomeController: UICollectionViewController, UICollectionViewDelegateFlowLayout, FormViewControllerDelegate {
let form = FormViewController()
func addTapped() {
form.delegate = self
let nav = UINavigationController(rootViewController: form)
navigationController?.present(nav, animated: true)
}
func popForm() {
navigationController?.popViewController(animated: true)
navigationController?.popToViewController(self, animated: true)
print("popped")
}
}
ViewController2 with V2
class FormViewController: UIViewController {
var delegate: FormViewControllerDelegate?
func stopTapped() {
print("pop it")
delegate?.popForm()
}
}
protocol FormViewControllerDelegate {
func popForm()
}
What am I doing wrong here?
In your VC2, Change to use this code
func stopTapped() {
print("pop it")
self.dismiss(animated: true, completion: nil)
}
Use this in ViewController1 to present FormViewController
func addTapped() {
let nav = UINavigationController(rootViewController: form)
self.present(nav, animated: true)
}
Within FormViewController when want to dismiss use this
func stopTapped() {
self.dismiss(animated: true)
}
You have presented the ViewController not pushed the ViewController, so what you need is to dismiss the Controller instead of pop the controller from navigation stack.
func popForm() {
navigationController?.dismiss(animated: true)
print("popped")
}
Better if you renamed the method name to dissmissForm instead of popForm.
You need to dismiss VC2 instead of pop. in self class as below :
func stopTapped() {
self.dismiss(animated: true, completion: { _ in })
}
When you are presenting any viewcontroller then you must use dismissViewController method to remove presented view controller. popViewController is used when you hqve push any viewcontroller.
When you use present then you have to use dismiss to remove that currently presented class in stack, when you dismiss it, your just next previous class will be in top of the stack. Thats all.. hope, it may helps you.
for pop a UIViewController you nee to push and not present. If you need to present a UIViewVontroller then on click on "X" you need to dismiss that viewController.
For push view controller
func addTapped() {
self.navigationController?.pushViewController(from, animated: true)
}
func stopTapped() {
self.navigationController?.popViewController(animated: true)
}
For presenting a view controller
func addTapped() {
self.present(from, animated: true, completion: nil)
}
func stopTapped() {
self.dismiss(animated: true, completion: nil)
}
You don't need to code for any protocol to push or present a UIViewController

UIPresentationController is being presented after huge delay

This is how I present my custom UIPresentationController:
func presentOverlayController(controller: UIViewController) {
controller.modalPresentationStyle = .Custom
controller.transitioningDelegate = self
presentViewController(controller, animated: true, completion: nil)
}
//MARK: - UIViewControllerTransitioningDelegate
public func presentationControllerForPresentedViewController(presented: UIViewController, presentingViewController presenting: UIViewController, sourceViewController source: UIViewController) -> UIPresentationController? {
return OverlayPresentationController(presentedViewController: presented, presentingViewController: presenting)
}
I present my controller once I tap on UITableViewCell. It is presented very quickly only if I tap on cell twice. However, when I perform single tap, then it works as well but with huge delay (between 15-60 seconds).
What is the reason?
How to workaround it?
There is a known bug with didSelectRowAtIndexPath that occurred to me as well, and this is the workaround I used.
Try to do this inside your didSelectRowAtIndexPath:
dispatch_async(dispatch_get_main_queue(), {
presentOverlayController(......)
})
References:
UITableView didSelectRowAtIndexPath sometimes called after second tap
UITableViewCell selection Storyboard segue is slow - double tapping works though
Try this:
dispatch_async(dispatch_get_main_queue()) {
self.presentViewController(controller, animated: true, completion: nil)
}

Resources