4
votes

I have code that enters full screen mode by hiding the UINavigationController's navigation bar. I want a smooth animated zooming effect when entering full screen. I use setNavigationBarHidden(_:animated:). This has all worked fine up to now, even on iOS 11, but on iPhone X the animation is not working well. On hiding, there is no animation and the nav bar just disappears. On unhiding, it does animate but the nav bar appears at a slower rate than the navigation controller's content area reduces, so an ugly black background shows through the navigation bar area during the animation.

I can recreate this in a simple test app. I have a UIViewController embedded in a UINavigationController.

Storyboard

  • UINavigationController Navigation Bar: Style == Black; Translucent OFF
  • UIViewController: Extend Edges: all options OFF.

I have tried all the combinations of Adjust Scroll View Insets and Extend Edges that I can think of but they made no difference.

Code

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

    setFullScreen(on: fullScreen, animated: animated)
}

override var prefersStatusBarHidden: Bool
{
    return fullScreen
}

override var preferredStatusBarStyle: UIStatusBarStyle
{
    return .lightContent
}

@IBAction func onToggleNavBarVisibility(_ sender: Any) {

    if let navBarHidden = self.navigationController?.isNavigationBarHidden {
        // Toggle the state
        fullScreen = !navBarHidden

        setFullScreen(on: fullScreen, animated: true)
    }
}

private func setFullScreen(on : Bool, animated : Bool) {

    self.navigationController?.setNavigationBarHidden(on, animated: animated)
    self.setNeedsStatusBarAppearanceUpdate()
}

Result on iPhone X (slow animations)

2

2 Answers

3
votes

In your case you are using both barTintColor & navigationBarStyle with Show Hide animation. barTintColor overrides the value implied by the Style attribute You should select either barTintColor or navigationBarStyle In below code i have just used barTintColor & navigationBarStyle is default with Transulent.

enter image description here

    var fullScreen = false{
      didSet{
        self.setNeedsStatusBarAppearanceUpdate()
     }
   }
    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Navigation Bar"
        navigationController?.navigationBar.barTintColor = .red
    }
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(true)
        setFullScreen(on: fullScreen, animated: animated)
    }
    @IBAction func onToggleNavBarVisibility(_ sender: Any) {
        if let navBarHidden = 
          self.navigationController?.isNavigationBarHidden {
            // Toggle the state
            fullScreen = !navBarHidden
            setFullScreen(on: fullScreen, animated: true)
        }
    }
    private func setFullScreen(on : Bool, animated : Bool) {
        self.navigationController?.setNavigationBarHidden(on, animated: animated)
        self.setNeedsStatusBarAppearanceUpdate()
    }

EDIT: If you want to hide status bar- use prefersStatusBarHidden with the bool value. & use setNeedsStatusBarAppearanceUpdate

   override var prefersStatusBarHidden: Bool {
        return fullScreen
    }

https://developer.apple.com/documentation/uikit/uinavigationbar

0
votes

That's clearly a UIKit bug. I've filed FB8980917:

When hiding the navigation bar simultaneously with the status bar using a slide animation, the navigation bar hides without animation. In the opposite direction, the status bar appears with a fade animation instead of the specified slide animation.

To reproduce, run the attached sample project. Use Simulator's slow animations or record the device's screen and step through the frames.

I've also attached a "Screen video.mp4" for your reference.

Note 1: As a workaround, we could resort to the deprecated UIApplication.setStatusBarHidden(_:with:) API (see "Screen video legacy.mp4"). This mostly works except that the status bar animation duration is longer than the navigation bar animation duration. However, it requires setting UIViewControllerBasedStatusBarAppearance=NO in Info.plist so it's an all or nothing approach which opts out the whole app of the modern API.

Note 2: Returning .fade for preferredStatusBarUpdateAnimation doesn't work either. First, it's ugly because the navigation bar still slides out (and can't be configured to fade out), second, the problem of the missing hide animation of the navigation bar persists.

Note 3: Using UINavigationController's hidesBarsOnTap property doesn't work either. The problem remains. The sample app also has hidesBarsOnTap enabled.

Sample code:

class ViewController: UIViewController {
    var fullScreen = false

    override var prefersStatusBarHidden: Bool {
        return navigationController!.isNavigationBarHidden
    }

    override var preferredStatusBarStyle: UIStatusBarStyle {
        return .lightContent
    }
    
    override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
        return .slide
    }

    @IBAction func toggleFullscreen(_ sender: Any) {
        fullScreen = !fullScreen

        navigationController?.setNavigationBarHidden(fullScreen, animated: true)
        setNeedsStatusBarAppearanceUpdate()
    }
}

While the workaround described in Note 1 kind of works, I can't recommend it since the API is deprecated since iOS 9.0. So really, it's at the folks @Apple to fix this. The fact that apps such as Photos implement a similar behavior without that bug show that there is a way to do it, albeit with private API or ugly hacks.