26

I have a side navigation controller and present it via a UIButton. When I make this NC the root view controller directly by [self presentviewcontroller: NC animated: YES completion: nil], for some reason the menu side of the NC is blocked by a UITransitionView that I cannot get to disappear.

Enter image description here

Enter image description here

I have tried the following:

UIWindow *window = [(AppDelegate *)[[UIApplication sharedApplication] delegate] window];
    window.backgroundColor = kmain;


    CATransition* transition = [CATransition animation];
    transition.duration = .5;
    transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    transition.type = kCATransitionPush;
    transition.subtype = kCATransitionFromTop;

    [nc.view.layer addAnimation:transition forKey:kCATransition];

    [UIView transitionWithView:window
                      duration:0.5
                       options:UIViewAnimationOptionTransitionNone
                    animations:^{ window.rootViewController = nc; }
                    completion:^(BOOL finished) {
                        for (UIView *subview in window.subviews) {
                            if ([subview isKindOfClass:NSClassFromString(@"UITransitionView")]) {
                                [subview removeFromSuperview];
                            }
                        }
                    }];

But it is very hacky, and as the rootviewcontroller of the window changes during the transition, it's a little choppy and part of the navigationcontroller and the top right corner turn black. It looks very bad.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Jameson
  • 4,198
  • 4
  • 17
  • 31
  • Did you find the root cause? I met the same issue here. Removing the view or disable touch on the view seems like a work around, but how do we avoid it? I don't understand why it came up. – RainCast Nov 12 '16 at 01:44
  • It was so long ago that I'm not quite sure, but I believe I completely switched libraries and went with JASidePannelController: https://github.com/gotosleep/JASidePanels Much easier to work with. – Jameson Nov 13 '16 at 17:26
  • this problem is FINALLY SOLVED: https://stackoverflow.com/a/53922625/294884 – Fattie Feb 16 '23 at 13:01

6 Answers6

8

To get tap events through the UITransitionView, set the containerView's userInteractionEnabled to false. This is if you're doing a custom transition animation by using UIViewControllerAnimatedTransitioning.

Example, in your animateTransition(_:):

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {

    let containerView = transitionContext.containerView
    containerView.isUserInteractionEnabled = false

    ...
}
kolyuchiy
  • 5,465
  • 2
  • 23
  • 31
SpencerBaker
  • 373
  • 2
  • 6
  • 10
    If I set the containerView's interactions to disabled, the clicks on the presented view fall through as well. – Alper Jan 11 '17 at 15:10
  • any solution to this? – yasirmturk Nov 16 '17 at 13:07
  • @Alper this is the work around: https://stackoverflow.com/a/63497131/4833705 – Lance Samaria Aug 20 '20 at 01:37
  • 1
    this is wrong. the issue is you can't get at the UITransitionView. here's the solution: https://stackoverflow.com/a/53922625/294884 (this answer is an important technique you have to use opften to "get through" views within a VC, but to go backwards you need this https://stackoverflow.com/a/53922625/294884 ) – Fattie Feb 16 '23 at 13:03
5

In my situation I needed a halfSize view controller. I followed this answer which worked great until I realized I needed to still be able to interact with the presenting vc (the vc behind the halfSizeVC).

The key is that you have to set both of these frames with the same CGRect values:

halfSizeVC.frame = CGRect(x: 0, y: UIScreen.main.bounds.height / 2, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)

containerView = CGRect(x: 0, y: UIScreen.main.bounds.height / 2, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)

Here is the code to go from ViewController to HalfSizeController and make HalfSizeController 1/2 the screen size. Even with halfSizeVC on screen you will still be able to interact with the top half of the vc behind it.

You also have to make a PassthroughView class if you want to be able to touch something inside the halfSizeVC. I included it at the bottom.

The presenting vc is white with a purple button at the bottom. Tapping the purple button will bring up the red halfSizeVC.

vc/presentingVC:

import UIKit

class ViewController: UIViewController {

    lazy var purpleButton: UIButton = {
        let button = UIButton(type: .system)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.setTitle("Tap to Present HalfSizeVC", for: .normal)
        button.setTitleColor(UIColor.white, for: .normal)
        button.backgroundColor = UIColor.systemPurple
        button.addTarget(self, action: #selector(purpleButtonPressed), for: .touchUpInside)
        button.layer.cornerRadius = 7
        button.layer.masksToBounds = true
        return button
    }()

    var halfSizeVC: HalfSizeController?

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white

        // tap gesture on vc will dismiss HalfSizeVC
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissHalfSizeVC))
        view.addGestureRecognizer(tapGesture)
    }


    // tapping the purple button presents HalfSizeVC
    @objc func purpleButtonPressed() {

        halfSizeVC = HalfSizeController()

        // *** IMPORTANT ***
        halfSizeVC!.view.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height / 2)

        halfSizeVC!.modalPresentationStyle = .custom

        present(halfSizeVC!, animated: true, completion: nil)
    }

    // dismiss HalfSizeVC by tapping anywhere on the white background
    @objc func dismissHalfSizeVC() {

        halfSizeVC?.dismissVC()
    }
}

halfSizeVC/presentedVC

import UIKit

class HalfSizeController: UIViewController {

    init() {
        super.init(nibName: nil, bundle: nil)
        modalPresentationStyle = .custom
        transitioningDelegate = self
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    lazy var topHalfDummyView: PassthroughView = {
        let view = PassthroughView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = .clear
        view.isUserInteractionEnabled = true
        return view
    }()

    var isPresenting = false
    let halfScreenHeight = UIScreen.main.bounds.height / 2

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .red

        setAnchors()
    }

    private func setAnchors() {
    
        view.addSubview(topHalfDummyView)
        topHalfDummyView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
        topHalfDummyView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        topHalfDummyView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        topHalfDummyView.heightAnchor.constraint(equalToConstant: halfScreenHeight).isActive = true
    }

    public func dismissVC() {
        dismiss(animated: true, completion: nil)
    }
}

extension HalfSizeController: UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning {

    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return self
    }

    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return self
    }

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 1
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

        let containerView = transitionContext.containerView

        // *** IMPORTANT ***
        containerView.frame = CGRect(x: 0, y: halfScreenHeight, width: UIScreen.main.bounds.width, height: halfScreenHeight)

        let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
        guard let toVC = toViewController else { return }
        isPresenting = !isPresenting

        if isPresenting == true {
            containerView.addSubview(toVC.view)

            topHalfDummyView.frame.origin.y += halfScreenHeight

            UIView.animate(withDuration: 0.4, delay: 0, options: [.curveEaseOut], animations: {

                self.topHalfDummyView.frame.origin.y -= self.halfScreenHeight

            }, completion: { (finished) in
                transitionContext.completeTransition(true)
            })

        } else {
            UIView.animate(withDuration: 0.4, delay: 0, options: [.curveEaseOut], animations: {
            
            }, completion: { (finished) in
                self.topHalfDummyView.frame.origin.y += self.halfScreenHeight
                transitionContext.completeTransition(true)
            })
        }
    }
}

PassthroughView needed for the topHalfDummyView in HalfSizeVC

import UIKit

class PassthroughView: UIView {
    
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        print("Passing all touches to the next view (if any), in the view stack.")
        return false
    }
}

Before purple button is pressed:

enter image description here

After purple button is pressed:

enter image description here

If you press the white background the red color will get dismissed

You can just c+p all 3 files and run your project

Lance Samaria
  • 17,576
  • 18
  • 108
  • 256
  • this should be an accepted answer – Leonid Silver Jan 26 '22 at 08:50
  • 1
    This is not really a solution and only works in a few rare cases. Fortunately the problem HAS been solved: https://stackoverflow.com/a/53922625/294884 – Fattie Feb 16 '23 at 13:02
  • 1
    @Fattie 1- It does work because I use it in my own live apps. 2- Have you tested it and it didn't work? If it didn't work then which part(s)? 3- You said "This is not really a solution" but then said "only works in a few rare cases". If it works in a few rare cases then it is a solution. – Lance Samaria Feb 16 '23 at 16:09
  • Lance .. *"In my situation I needed a halfSize view controller. "* it only works in that case – Fattie Feb 16 '23 at 20:42
  • @Fattie I can agree with that. – Lance Samaria Feb 16 '23 at 23:34
  • heh we rock, Lance :) – Fattie Feb 19 '23 at 16:28
  • just for the record, the new solution that is now possible (see link) is extremely simple, well it's a one liner, and it works in 100% of cases completely reliably - it's a lifesaver. its' great how things (sometimes!) get easier in iOS over the years! – Fattie Feb 19 '23 at 16:29
  • 1
    @Fattie I looked at it but haven't tried it yet. I'll try it later this week. Peace! – Lance Samaria Feb 19 '23 at 16:38
3

I had a similar issue where a UITransitionView kept blocking my views, preventing any user interaction.

In my case this was due to an uncompleted custom animated UIViewController transition.

I forgot to properly complete my transition with:

TransitionContext.completeTransition(transitionContext.transitionWasCancelled)

or

TransitionContext.completeTransition(!transitionContext.transitionWasCancelled)

In the

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {}

from the UIViewControllerAnimatedTransitioning protocol

Steefy
  • 39
  • 2
  • I met the same issue. I have a progress bar that shows animation. When it has done, I dismiss the progress bar and navigationController?.popToRootViewController. Maybe the progress bar is not dismissed before the ViewController was popped up. I fixed it by letting viewController popped up certainly after progress bar dismissed. – timyau Apr 01 '22 at 09:29
1

I had the same issue, but in a little different scenario. I ended up doing something very similar to find the view, but instead of removing the view which can be more problematic, I disabled the user interaction, so any touch events just go throw it and any other objects can handle to user's interaction.

In my case this was only present after updating the app to iOS 10, and the same code running in iOS 9 didn't fall into this.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
yaakov_g
  • 11
  • 1
  • 3
0

I was facing the same issue, and this solved issue for me,

navigationController.setNavigationBarHidden(true, animated: false)

This worked for me as I am having custom view as navigation bar in view controllers.

infiniteLoop
  • 2,135
  • 1
  • 25
  • 29
0

I’ve had this issue when I was setting accessibilityElements on a popover view controller. I fixed it by removing assigning an array of elements.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
StackRunner
  • 1,463
  • 2
  • 16
  • 22