18
votes

Issue:

Modally presented view controller does not move back up after in-call status bar disappears, leaving 20px empty/transparent space at the top.


Normal : No Issues

enter image description here


In-Call : No Issues

enter image description here


After In-Call Disappears:

Leaves a 20px high empty/transparent space at top revealing orange view below. However the status bar is still present over the transparent area. Navigation Bar also leaves space for status bar, its' just 20px too low in placement.

enter image description here

enter image description here


  • iOS 10 based
  • Modally presented view controller
  • Custom Modal Presentation
  • Main View Controller behind is orange
  • Not using Autolayout
  • When rotated to Landscape, 20px In-Call Bar leaves and still leaves 20px gap.
  • I opt-out showing status bar in landscape orientations. (ie most stock apps)

I tried listening to App Delegates:

willChangeStatusBarFrame
didChangeStatusBarFrame

Also View Controller Based Notifications:

UIApplicationWillChangeStatusBarFrame
UIApplicationDidChangeStatusBarFrame

When I log the frame of presented view for all four above methods, the frame is always at (y: 0) origin.


Update

View Controller Custom Modal Presentation

    let storyboard = UIStoryboard(name: "StoryBoard1", bundle: nil)
    self.modalVC = storyboard.instantiateViewController(withIdentifier: "My Modal View Controller") as? MyModalViewController
    self.modalVC!.transitioningDelegate = self
    self.modalVC.modalPresentationStyle = .custom
    self.modalVC.modalPresentationCapturesStatusBarAppearance = true;
    self.present(self.modalVC!, animated: true, completion: nil)


    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let containerView = transitionContext.containerView
        let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
        let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
        toViewController!.view.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)

        UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.0, options: [.curveEaseOut], animations: { () -> Void in

            toViewController!.view.transform = CGAffineTransform.identity

        }, completion: { (completed) -> Void in

           transitionContext.completeTransition(completed)

        })
 }
8
What do you mean by 'Custom Modal Presentation'? Could you elaborate on how you presents the modal VC?The Dreams Wind
Updated OP. In the transition animation, I just scale it down and animate it to 1:1 scale.Gizmodo
Your modal VC takes over control of status bar appearance by settings this propery modalPresentationCapturesStatusBarAppearance. Did you override preferredStatusBarStyle or prefersStatusBarHidden methods for the modal VC?The Dreams Wind
modalPresentationCapturesStatusBarAppearance is true as you can see. I also did override preferredStatusBarStyle and prefersStatusBarHidden methods. Status bar shows up properly at the right times. Only when In-Call goes away, view doesn't resize.Gizmodo
Unfortunately, had no time to check this behaviour myself. However the question is quite interesting and I favorited it :) Any luck so far?The Dreams Wind

8 Answers

6
votes

I've been looking for a solution for 3 days. I don't like this solution but didn't found better way how to fix it.

I'he got situation when rootViewController view has bigger height for 20 points than window, when I've got notification about status bar height updates I manually setup correct value.

Add method to the AppDelegate.swift

func application(_ application: UIApplication, didChangeStatusBarFrame oldStatusBarFrame: CGRect) {
        if let window = application.keyWindow {
            window.rootViewController?.view.frame = window.frame
        }
    }

After that it works as expected (even after orientation changes). Hope it will help someone, because I spent too much time on this.

P.S. It blinks a little bit, but works.

5
votes

I faced this problem too but after I put this method, problem is gone.

iOS has its default method willChangeStatusBarFrame for handling status bar. Please put this method and check it .

func application(_ application: UIApplication, willChangeStatusBarFrame newStatusBarFrame: CGRect) {
    UIView.animate(withDuration: 0.35, animations: {() -> Void in
        let windowFrame: CGRect? = ((window?.rootViewController? as? UITabBarController)?.viewControllers[0] as? UINavigationController)?.view?.frame
        if newStatusBarFrame.size.height > 20 {
            windowFrame?.origin?.y = newStatusBarFrame.size.height - 20
            // old status bar frame is 20
        }
        else {
            windowFrame?.origin?.y = 0.0
        }
        ((window?.rootViewController? as? UITabBarController)?.viewControllers[0] as? UINavigationController)?.view?.frame = windowFrame
    })
}

Hope this thing will help you.
Thank you

3
votes

I had the same issue with the personnal hospot modifying the status bar. The solution is to register to the system notification for the change of status bar frame, this will allow you to update your layout and should fix any layout issue you might have. My solution which should work exactly the same for you is this :

  • In your view controller, in viewWillAppear suscribe to the UIApplicationDidChangeStatusBarFrameNotification

        NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(myControllerName.handleFrameResize(_:)), name: UIApplicationDidChangeStatusBarFrameNotification, object: nil)
    
  • Create your selector method

    func handleFrameResize(notification: NSNotification) {
    self.view.layoutIfNeeded() }
    
  • Remove your controller from notification center in viewWillDisappear

        NSNotificationCenter.defaultCenter().removeObserver(self, name: UIApplicationDidChangeStatusBarFrameNotification, object: nil)
    
  • You also need your modal to be in charge of the status bar so you should set

    destVC.modalPresentationCapturesStatusBarAppearance = true before presenting the view.

You can either implement this on every controller susceptible to have a change on the status bar, or you could make another class which will do it for every controller, like passing self to a method, keep the reference to change the layout and have a method to remove self. You know, in order to reuse code.

3
votes

I think this is a bug in UIKit. The containerView that contains a presented controller's view which was presented using a custom transition does not seem to move back completely when the status bar returns to normal size. (You can check the view hierarchy after closing the in call status bar)

To solve it you can provide a custom presentation controller when presenting. And then if you don't need the presenting controller's view to remain in the view hierarchy, you can just return true for shouldRemovePresentersView property of the presentation controller, and that's it.

func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
    return PresentationController(presentedViewController: presented, presenting: presenting)
}

class PresentationController: UIPresentationController {
    override var shouldRemovePresentersView: Bool {
        return true
    }
}

or if you need the presenting controller's view to remain, you can observe status bar frame change and manually adjust containerView to be the same size as its superview

class PresentationController: UIPresentationController {
    override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
        super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(self.onStatusBarChanged),
                                               name: .UIApplicationWillChangeStatusBarFrame,
                                               object: nil)
    }

    @objc func onStatusBarChanged(note: NSNotification) {
        //I can't find a way to ask the system for the values of these constants, maybe you can
        if UIApplication.shared.statusBarFrame.height <= 20,
            let superView = containerView?.superview {
            UIView.animate(withDuration: 0.4, animations: {
                self.containerView?.frame = superView.bounds
            })
        }
    }
}
3
votes

I've been looking for a solution to this problem. In fact, I posted a new question similar to this one. Here: How To Avoid iOS Blue Location NavigationBar Messing Up My StatusBar?

Believe me, I've been solving this for a couple of days now and it's really annoying having your screen messed up because of the iOS's status bar changes by in-call, hotspot, and location.

I've tried implementing Modi's answer, I put that piece of code in my AppDelegate and modified it a bit, but no luck. and I believe iOS is doing that automatically so you do not have to implement that by yourself.

Before I discovered the culprit of the problem, I did try every solution in this particular question. No need to implement AppDelegate's method willChangeStatusBar... or add a notification to observe statusBar changes.

I also did redoing some of the flows of my project, by doing some screens programmatically (I'm using storyboards). And I experimented a bit, then inspected my previous and other current projects why they are doing the adjustment properly :)

Bottom line is: I am presenting my main screen with UITabBarController in such a wrong way.

Please always take note of the modalPresentationStyle. I got the idea to check out my code because of Noah's comment.

Sample:

func presentDashboard() {
    if let tabBarController = R.storyboard.root.baseTabBarController() {
        tabBarController.selectedIndex = 1
        tabBarController.modalPresentationStyle = .fullScreen
        tabBarController.modalTransitionStyle = .crossDissolve

        self.baseTabBarController = tabBarController
        self.navigationController?.present(tabBarController, animated: true, completion: nil)
    }
}
1
votes

I solve this issue by using one line of code

In Objective C

tabBar.autoresizingMask = (UIViewAutoResizingFlexibleWidth | UIViewAutoResizingFlexibleTopMargin);

In Swift

self.tabBarController?.tabBar.autoresizingMask = 
  UIViewAutoresizing(rawValue: UIViewAutoresizing.RawValue(UInt8(UIViewAutoresizing.flexibleWidth.rawValue) | UInt8(UIViewAutoresizing.flexibleTopMargin.rawValue)))`

You just need to make autoresizingMask of tabBar flexible from top.

1
votes

In my case, I'm using custom presentation style for my ViewController. The problem is that the Y position is not calculated well. Let's say the original screen height is 736p. Try printing the view.frame.origin.y and view.frame.height, you'll find that the height is 716p and the y is 20. But the display height is 736 - 20(in-call status bar extra height) - 20(y position). That is why our view is cut from the bottom of the ViewController and why there's a 20p margin to the top. But if you go back to see the navigation controller's frame value. You'll find that no matter the in-call status bar is showing or not, the y position is always 0.

So, all we have to do is to set the y position to zero.

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    let f = self.view.frame

    if f.origin.y != 0 {
        self.view.frame = CGRect(x: f.origin.x, y: 0, width: f.width, height: f.height)
        self.view.layoutIfNeeded()
        self.view.updateConstraintsIfNeeded()
    }
}
0
votes

Be sure to set the frame of the view controller's view you are presenting to the bounds of the container view, after it has been added to the container view. This solved the issue for me.

containerView.addSubview(toViewController.view)
toViewController.view.frame = containerView.bounds