I have a view controller A that shows the status bar on top. From that view controller I want to present another view controller B that hides the status bar. In order to achieve that I override the property
override var prefersStatusBarHidden: Bool {
return true
}
on B. For enforcing a smooth animation whenever the status bar (dis)appears I also override the property
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .slide
}
However, when I now present view controller B from A the status bar disappears abruptly while A is still visible, right before the animated modal transition begins.
I'm searching for a way to fix this "jumping status bar" behavior. Ideally, I would like to have a clean separation:
A: shows status bar
B: does not show status bar
so that, when I present B, the status bar is covered by it.
As the status bar seems to be a global view that doesn't belong to any particular view controller it's probably difficult to achieve that kind of behavior. So in case it's not possible to replicate this exact animation behavior, I'd also be happy if the status bar slides out smoothly during the view controller transition. How can I achieve that?
For animating the status bar during the transition, you could do something like this in view controller B:
var willAppear = false
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .slide
}
override var prefersStatusBarHidden: Bool {
return willAppear
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
willAppear = true
UIView.animate(withDuration: 0.5) {
self.setNeedsStatusBarAppearanceUpdate()
}
}
Then I guess you would need to do the opposite if you want the reverse effect when the modal controller is dismissed.
You can adjust the duration of the animation to whatever suits you, though I'm not sure how consistent the duration between viewWillAppear and the modal controller actually being fully presented will be.
EDIT:
"Opposite" ends up being something like this (in view controller A):
var willAppear = false
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .slide
}
override var prefersStatusBarHidden: Bool {
return willAppear
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if let _ = presentedViewController as? B {
willAppear = true
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
if let _ = presentedViewController as? B {
willAppear = false
UIView.animate(withDuration: 0.5) {
self.setNeedsStatusBarAppearanceUpdate()
}
}
}
Which I agree, is way too much code for something that I imagine many people want.
Related
I want to edit the status bar style of my application. However, I'm unable to edit it from my main UIViewController. So I think multiple status bars settings are stacked because of embed UIViewControllers.
Here is how I initiate my navigationController in the didFinishLaunchingWithOptions method:
let navigationVC = CustomNavigationController(rootViewController: MenuInstance)
navigationVC.setNavigationBarHidden(true, animated: false)
Then, I move some views of other UIViewControllers in the menu UIViewController (MenuInstance) for any reason like this:
let scannerVC = ScannerViewController()
override func viewDidLoad() {
super.viewDidLoad()
addChildViewController(scannerVC)
scannerVC.didMove(toParentViewController: self)
}
I tried to create class to set prefersStatusBarHidden = true
class ModalViewViewController: UIViewController {
override var prefersStatusBarHidden: Bool {
get {
return true
}
}
override func viewDidLoad() {
super.viewDidLoad()
setNeedsStatusBarAppearanceUpdate()
}
}
I also created a class for the UINavigationController
class CustomNavigationController: UINavigationController {
override var prefersStatusBarHidden: Bool {
get {
return true
}
}
override func viewDidLoad() {
super.viewDidLoad()
setNeedsStatusBarAppearanceUpdate()
}
}
The goal of trying to remove the status bar is to find where the status bar comes from. I want only one status bar in the MenuInstance UIViewController that I can edit. The fact that some views of UIViewControllers are embed in one UIViewController makes me confused.
If the MenuInstance view controller is the root view controller of the navigation controller, then the MenuInstance's implementation of prefersStatusBarHidden is all that matters. No other view controller's preference is consulted. The "embed" stuff is irrelevant (unless you want to make it relevant).
class MenuInstance : UIViewController {
override var prefersStatusBarHidden: Bool {
get {
return true
}
}
}
Note, however, that this will not work on an iPhone X. You cannot hide the status bar on an iPhone X.
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()
}
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.
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
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.
I have two ViewControllers, using a custom segue from the first I present the second, with custom animator and a percent based interactor.
I want to slide up the status bar alongside the controller transition.
In the second ViewController I have this in order to hide the status bar
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .slide
}
override var prefersStatusBarHidden: Bool {
return true
}
I know I should call setNeedsStatusBarAppearanceUpdate() in an animation block in order to animate it, and use UIViewControllerTransitionCoordinator method animateAlongsideTransition: in order to make it follow the transition, but I am not sure where should I call this method.
I tried in the viewWillAppear method of the second controller but it still disappeared immediately without animation.
What is the right place to call this?
The reason why you couldn't see the animation is because in your second view controller you always return true to prefersStatusBarHidden, then such view controller starts with that condition, hence doesn't have any "chance" for playing the animation.
So in the second view controller you might try doing so:
class ViewController2: UIViewController {
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { get { return .slide } }
override var prefersStatusBarHidden: Bool { return statusBarHidden }
var statusBarHidden = false
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.statusBarHidden = true
UIView.animate(withDuration: 0.35) {
self.setNeedsStatusBarAppearanceUpdate()
}
}
}
Moreover in your Info.plist be sure to have View controller-based status bar appearance= YES
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()
}
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.
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
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.
Background: I implemented a Custom UIViewController Transition where the first view controller (VC1) has a visible status bar
override func prefersStatusBarHidden() -> Bool {
return false
}
while the second presented view controller (VC2) has a hidden status bar:
override func prefersStatusBarHidden() -> Bool {
return true
}
Transitioning is controlled by the user since I implemented a pull to open transition with gesture controllers.
Objective: I want the status bar to be hidden during appearance transition AND disappearance transition (essentially like the Google Maps Slide Out Menu).
Problem: The status bar is correctly hidden during the entire appearance transition of ViewController VC2. But during the entire disappearance transition the status bar is visible. Any suggestions on how to properly implement this for iOS 9?
Just try to set status bar hidden on viewWillAppear and viewWillDisappear function.
You can create an instance var to hold the status bar hidden state and return this boolean from prefersStatusBarHidden(). When this value changes, call setNeedsStatusBarAppearanceUpdate().
For example:
var statusBarHidden = true {
didSet {
if oldValue != statusBarHidden {
setNeedsStatusBarAppearanceUpdate()
}
}
}
override var prefersStatusBarHidden: Bool {
return statusBarHidden
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
statusBarHidden = false
}