7
votes

When performing some network operations, I present a modal view controller (similar to MBProgressHUD but as a view controller) to prevent user interaction and indicate progress.

The view controller has modalPresentationStyle = .Custom and is animated using a transitioning delegate and custom presentation controller. Beyond animating the transitions they have no custom actions driving the presentation.

The problem I have is that whenever the view controller is presented, it turns the status bar color black. I could override preferredStatusBarStyle to make it always return .LightContent but sometimes this view controller is presented over a view controller with .Default and I don't want to change it there either. Basically, I want to have the same behavior as UIAlertController.

Screenshot showing presented view controller causing dark status bar content

I have tried configuring the presentation controller to move the presented view controller out of the status bar space:

private class SEUIProgressControllerPresentationController: UIPresentationController {

    override func shouldPresentInFullscreen() -> Bool {
        return false
    }

    private override func frameOfPresentedViewInContainerView() -> CGRect {
        return super.frameOfPresentedViewInContainerView().insetBy(dx: 40, dy: 100)
    }

    ...
}

These settings do move the top of the presented controller out of the status bar but the status bar is still affected. Is there a property I am missing that would stop my view controller from updating the status bar style?

1

1 Answers

6
votes

Update

It looks like this has been fixed in iOS 10. The default behavior is to ignore the status bar rules from the presented view controller unless either the presented view controller has modalPresentationCapturesStatusBarAppearance == true or you use one of several built-in presentation controllers that extend into the status bar space (not .custom).

Basically, the behavior for custom has changed to default opt-out rather than forced opt-in.


For iOS 9.x and lower

After much digging, the internal logic for setting the application's status bar color looks like this:

var viewController = window.rootViewController!

while let presentedViewController = viewController.valueForKey("_presentedStatusBarViewController") as? UIViewController {
    viewController = presentedViewController
}

while let childViewController = viewController.childViewControllerForStatusBarStyle() {
    viewController = childViewController
}

let style = viewController.preferredStatusBarStyle()

The view controller's property _presentedStatusBarViewController is assigned during presentation based on the value of its presentation controller's private method _shouldChangeStatusBarViewController(). The default implementation of this method is to return true, with _UIAlertControllerPresentationController and a handful of other presentation controllers returning false.

That means the most direct way to not change that status bar is simply to add this method to my presentation controller:

private class SEUIProgressControllerPresentationController: UIPresentationController {

    @objc func _shouldChangeStatusBarViewController() -> Bool {
        return false
    }

    ...
}

Unfortunately, this won't pass an App Store review.

Instead, what I am doing is recreating the logic that would be applied to the presenting view controller in my view controller:

public class SEUIProgressController: UIViewController {

    ...
    public override func preferredStatusBarStyle() -> UIStatusBarStyle {

        guard var targetViewController = presentingViewController else {
            return .LightContent
        }

        while let parentViewController = targetViewController.parentViewController {
            targetViewController = parentViewController
        }

        while let childViewController = targetViewController.childViewControllerForStatusBarStyle() {
            targetViewController = childViewController
        }

        return targetViewController.preferredStatusBarStyle()
    }

    public override func prefersStatusBarHidden() -> Bool {

        guard var targetViewController = presentingViewController else {
            return false
        }

        while let parentViewController = targetViewController.parentViewController {
            targetViewController = parentViewController
        }

        while let childViewController = targetViewController.childViewControllerForStatusBarHidden() {
            targetViewController = childViewController
        }

        return targetViewController.prefersStatusBarHidden()
    }
}