iosswiftuipresentationcontroller

Why UIPresentationController's height is changed when present another controller?


I use UIPresentationController to show bottom tip. Sometimes the presentationController may present another controller. And When the presented controller is dismissed, the presentationController's height was changed. So why does it changed and how can i solve this problem. The code likes below:

class ViewController: UIViewController {

    let presentVC = UIViewController()
    override func viewDidLoad() {
        super.viewDidLoad()
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
            guard let `self` = self else { return }
            self.presentBottom(self.presentVC)
        }

        DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
            guard let `self` = self else { return }
            let VC = UIViewController()
            VC.view.backgroundColor = .red
            self.presentVC.present(VC, animated: true, completion: {

                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                    VC.dismiss(animated: true, completion: nil)
                }

            })
        }
    }

}
public class PresentBottom: UIPresentationController {

    public override var frameOfPresentedViewInContainerView: CGRect {
        let bottomViewHeight: CGFloat = 200.0
        let screenBounds              = UIScreen.main.bounds
        return CGRect(x: 0, y: screenBounds.height - bottomViewHeight, width: screenBounds.width, height: bottomViewHeight)
    }

}
extension UIViewController: UIViewControllerTransitioningDelegate {

    public func presentBottom(_ vc: UIViewController ) {
        vc.view.backgroundColor = .purple
        vc.modalPresentationStyle = .custom
        vc.transitioningDelegate = self
        self.present(vc, animated: true, completion: nil)
    }

    public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
        let vc = PresentBottom(presentedViewController: presented, presenting: presenting)
        return vc
    }
}

My codes like below: enter image description here

The presentationController height is right in the below image:

enter image description here

What confused me is that the present view controller's height is changed:

enter image description here


Solution

  • By default, when a view controller is presented, UIKit removes the presenting VC's once the animation has finished. The presenting VC's view is only temporary added to the container view during the time of the transition.

    You can change this behavior using UIModalPresentationStyle.overFullScreen or using shouldRemovePresentersView in a case of a custom presentation. In that case, only the presented view is moved to the temporary containerView and all the animation are performed above the presenting VC's view - none of the underneath views are manipulated.

    Here you're performing a fullScreen transition above a VC presented with a custom transition. UIKit keeps the view hierarchy intact and only moved the presenting VC's view (the purple one) alongside the presented VC's view (the red one) to the container view during the dismissal & presentation transitions. It then removes the purple one once the animation is done.

    Problem, apparently (I used breakpoints in the didMoveToSuperview & setFrame methods), when the dismissal transition occurs, the default "built-in" animation controller doesn't care about the previous frame of the presenting VC's view when it temporary adds it in to the containerView. It sets it to the containerView's frame right after moving to the containerView.

    I don't know if it's a bug or an intentional choice.

    Here is the code I used:

    class BottomVCView: UIView {
        override var frame: CGRect {
            set {
                super.frame = newValue // BREAKPOINT : newValue.height == 568.0
            }
            get {
                return super.frame
            }
        }
    }
    
    class BottomVC: UIViewController {
    
        lazy var myView = BottomVCView()
    
        override func loadView() {
            view = BottomVCView()
        }
    }
    

    And here is the stack where UIViewControllerBuiltinTransitionViewAnimator sets the frame of presenting VC's view.

    * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
      * frame #0: 0x0000000102ca0ce8 Project`BottomVCView.frame.setter(newValue=(origin = (x = 0, y = 0), size = (width = 320, height = 568)), self=0x00007ffde5e059f0) at ViewController.swift:35:13
        frame #1: 0x0000000102ca0c9f Project`@objc BottomVCView.frame.setter at <compiler-generated>:0
        frame #2: 0x0000000108e2004f UIKitCore`-[UIViewControllerBuiltinTransitionViewAnimator animateTransition:] + 1149
        frame #3: 0x0000000108d17985 UIKitCore`__56-[UIPresentationController runTransitionForCurrentState]_block_invoke + 3088
        frame #4: 0x0000000109404cc9 UIKitCore`_runAfterCACommitDeferredBlocks + 318
        frame #5: 0x00000001093f4199 UIKitCore`_cleanUpAfterCAFlushAndRunDeferredBlocks + 358
        frame #6: 0x000000010942132b UIKitCore`_afterCACommitHandler + 124
        frame #7: 0x00000001056fe0f7 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23
        frame #8: 0x00000001056f85be CoreFoundation`__CFRunLoopDoObservers + 430
        frame #9: 0x00000001056f8c31 CoreFoundation`__CFRunLoopRun + 1505
        frame #10: 0x00000001056f8302 CoreFoundation`CFRunLoopRunSpecific + 626
        frame #11: 0x000000010d0dd2fe GraphicsServices`GSEventRunModal + 65
        frame #12: 0x00000001093f9ba2 UIKitCore`UIApplicationMain + 140
        frame #13: 0x0000000102ca60fb NavBar`main at AppDelegate.swift:12:7
        frame #14: 0x0000000106b9f541 libdyld.dylib`start + 1
        frame #15: 0x0000000106b9f541 libdyld.dylib`start + 1
    

    If you use containerViewWillLayoutSubviews, you can set the purple view's frame back to its correct value but it will only occur once the dismissal transition is done and the view is removed from the fullScreen transition containerView:

    class PresentBottom: UIPresentationController {
    
        override func containerViewWillLayoutSubviews() {
            super.containerViewWillLayoutSubviews()
            presentedView?.frame = frameOfPresentedViewInContainerView
        }
    }
    

    I think you can simply use:

    let VC = UIViewController()
    VC.view.backgroundColor = .red
    VC.modalPresentationStyle = .overFullScreen
    

    To keep all the view hierarchy in place. The animator won't touch the presenting VC's view - viewController(for: .from) returns nil as its view is not moved to the container view.