16

When presenting a view controller using a custom animation, none of Apple's documentation or example code mentions or includes constraints, beyond the following:

// Always add the "to" view to the container.
// And it doesn't hurt to set its start frame.
[containerView addSubview:toView];
toView.frame = toViewStartFrame;

The problem is that the double-height status bar is not recognized by custom-presented view controllers (view controllers that use non-custom presentations don't have this problem). The presented view controller is owned by the transition's container view, which is a temporary view provided by UIKit that we have next to no dominion over. If we anchor the presented view to that transient container, it only works on certain OS versions; not to mention, Apple has never suggested doing this.

UPDATE 1: There is no way to consistently handle a double-height status bar with custom modal presentations. I think Apple botched it here and I suspect they will eventually phase it out.

UPDATE 2: The double-height status bar has been phased out and no longer exists on non-edge-to-edge devices.

trndjc
  • 11,654
  • 3
  • 38
  • 51

2 Answers2

5

My answer is: You should not use constraints in case of custom modal presentations

Therefore I know your pain, so I will try to help you to save time and effort by providing some hints which I suddenly revealed.


Example case:

Card UI animation like follows:

enter image description here

Terms for further use:

  • Parent - UIViewController with "Detail" bar button item
  • Child - UIViewController with "Another"

Troubles you mentioned began, when my animation involved size change along with the movement. It causes different kinds of effects including:

  • Parent's under-status-bar area appeared and disappeared
  • Parent's subviews were animated poorly - jumps, duplication and other glitches.

After few days of debugging and searching I came up with the following solution (sorry for some magic numbers ;)):

UIView.animate(withDuration: transitionDuration(using: transitionContext),
                       delay: 0,
                       usingSpringWithDamping: 1,
                       initialSpringVelocity: 0.4,
                       options: .curveEaseIn, animations: {
            toVC.view.transform = CGAffineTransform(translationX: 0, y: self.finalFrame.minY)
            toVC.view.frame = self.finalFrame
            toVC.view.layer.cornerRadius = self.cornerRadius
            
            fromVC.view.layer.cornerRadius = self.cornerRadius
            var transform = CATransform3DIdentity
            transform = CATransform3DScale(transform, scale, scale, 1.0)
            transform = CATransform3DTranslate(transform, 0, wdiff, 0)
            fromVC.view.layer.transform = transform
            fromVC.view.alpha = 0.6
        }) { _ in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        }

Main point here is, that You have to use CGAffineTransform3D to avoid animation problems and problems with subviews animation (2D Transforms are not working for unknown reasons).

This approach fixes, I hope, all your problems without using constraints.

Feel free to ask questions.

UPD: According to In-Call status bar

After hours of all possible experiments and examining similar projects like this and this and stackoverflow questions like this, this (it's actually fun, OPs answer is there) and similar I am totally confused. Seems like my solution handles Double status bar on UIKit level (it adjusts properly), but the same movement is ignoring previous transformations. The reason is unknown.


Code samples:

You can see the working solution here on Github

P.S. I'm not sure if it's ok to post a GitHub link in the answer. I'd appreciate for an advice how to post 100-300 lines code In the answer.

Community
  • 1
  • 1
fewlinesofcode
  • 3,007
  • 1
  • 13
  • 30
  • 1
    I appreciate the answer and the effort but the project in the Github link is not a working solution. It doesn't address the problem posed in the question, the adaptation of the presented view to frame changes. In your Github project, toggling the double-height status bar breaks the UI. That is the problem I want to fix. – trndjc Nov 09 '18 at 16:38
  • @narddog, got it. Sorry, I don't even know how did I skip that point. So I played a bit more. We can listen status bar frame changes using `NotificationCenter.default.addObserver(self, selector: #selector(SELECTOR), name: UIApplication.willChangeStatusBarFrameNotification, object: nil)` and updating `ViewController`s frame respectively. There will once again be problem with the **scale** in my solution. I will continue playing with it and will share the results. – fewlinesofcode Nov 09 '18 at 17:05
0

I've been struggling with double-height statusBar in my current project and I was able to solve almost every issue (the last remaining one is a very strange transformation issue when the presentingViewController is embedded inside a UITabBarController).

When the height of the status bar changes, a notification is posted.
Your UIPresentationController subclass should subscribe to that particular notification and adjust the frame of the containerView and its subviews:

UIApplication.willChangeStatusBarFrameNotification

Here is an example of code I'm using:

final class MyCustomPresentationController: UIPresentationController {

    // MARK: - StatusBar

    private func subscribeToStatusBarNotifications() {
        let notificationName = UIApplication.willChangeStatusBarFrameNotification
        NotificationCenter.default.addObserver(self, selector: #selector(statusBarWillChangeFrame(notification:)), name: notificationName, object: nil)
    }

    @objc private func statusBarWillChangeFrame(notification: Notification?) {
        if let newFrame = notification?.userInfo?[UIApplication.statusBarFrameUserInfoKey] as? CGRect {
            statusBarWillChangeFrame(to: newFrame)
        } else {
            statusBarWillChangeFrame(to: .zero)
        }
    }

    func statusBarWillChangeFrame(to newFrame: CGRect) {
        layoutContainerView(animated: true)
    }

    // MARK: - Object Lifecycle

    deinit {
        // Unsubscribe from all notifications
        NotificationCenter.default.removeObserver(self)
    }

    // MARK: - Layout

    /// Called when the status-bar is about to change its frame.
    /// Relayout the containerView and its subviews
    private func layoutContainerView(animated: Bool) {
        guard let containerView = self.containerView else { return }

        // Retrieve informations about status-bar
        let statusBarHeight = UIApplication.shared.statusBarFrame.height
        let normalStatusBarHeight = Constants.Number.statusBarNormalHeight // 20
        let isStatusBarNormal = statusBarHeight ==~ normalStatusBarHeight

        if animated {
            containerView.frame = …
            updatePresentedViewFrame(animated: true)
        } else {
            // Update containerView frame
            containerView.frame = …
            updatePresentedViewFrame(animated: false)
        }
    }

    func updatePresentedViewFrame(animated: Bool) {
        self.presentedView?.frame = …
    }
}

result image

Vin Gazoil
  • 1,942
  • 2
  • 20
  • 24
  • Are you using `scale` transform along with `translation`? In my case it also breaks the UI. – fewlinesofcode Nov 14 '18 at 13:12
  • I do use a scale transform during the presentationTransition but I'm not using any translation. For notched devices, I'm directly setting the vertical origin of the frame (for the view to be below the top safe area inset). – Vin Gazoil Nov 14 '18 at 13:20
  • will try to set frame directly, without translation. Thanks. – fewlinesofcode Nov 14 '18 at 13:21