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.
Related
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 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.
I wonder if its OK by iOS Human Interface Guidelines to style a UIView so it looks and acts like a navbar.
My problem is that I want to hide my current navbar once the user scrolls.
I have tried both self.navigationController?.setNavigationBarHidden(true, animated: true) and navigationController?.hidesBarsOnSwipe = true but the animation looks odd, once the navigation bar gets hidden I still have about 20px space under the status bar: You can look at my other question
So to make things easier, can I just init my view tih the navbar hidden and style my own and add the proper animation?
Try this out :
extension YourViewController {
override func prefersStatusBarHidden() -> Bool {
return barsHidden // Custom property
}
override func preferredStatusBarUpdateAnimation() -> UIStatusBarAnimation {
return .Slide
}
}
You have to update barsHidden somewhere in the code and call setNeedsStatusBarAppearanceUpdate() method.
I thought showing a screenshot would help understand the issue a bit better.
So the context is the following:
I'm in a navigation controller, on the settings screen of the app (which has a navigation item) and when we tap on the back button, we go back to the main screen of the app (for which I've hidden the navigation bar in the viewWillAppear of the main screen because I'm building a custom header view myself).
At soon as I tap on the back button, the navigation bar disappears immediately and I see a black rectangle appears instead until the animation to display the main screen is completed.
Do you know how I can avoid having this black rectangle appear?
Hope the questions makes sense.
Screenshots
Here is the initial settings screen:
When we tape on the back button, this happens... help :D
I know this piece of code is most likely responsible for the error, but I absolutely need to have the navigationBar hidden on the previous screen.
override func viewWillAppear(_ animated: Bool) {
navigationController?.isNavigationBarHidden = true
}
Have you tried the animated method of hiding the navigation bar setNavigationBarHidden(_ hidden: Bool, animated: Bool)?
For Swift3.0
Add below code in First ViewController
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
navigationController?.setNavigationBarHidden(true, animated: animated)
}
Add below code in Second ViewController
func backButtonPressed() {
navigationController?.setNavigationBarHidden(false, animated: false)
navigationController?.popViewController(animated: true)
}
Add below code in Second ViewController
the color can corresponding your custom
override func viewWillAppear(_ animated: Bool) {
self.navigationController?.view.backgroundColor = UIColor.white
}
In my app I have 4 ViewController and in two of them I am changing the status bar from white to black like this:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
UIApplication.sharedApplication().statusBarStyle = UIStatusBarStyle.Default
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
UIApplication.sharedApplication().statusBarStyle = .LightContent
}
The problem is if I switch between two ViewController that both have the code above the status bar will first change to black which is right, but then it changes to white again when entering the other ViewController.
How can I keep the status bar white on certain ViewController's ?
Try to add the following method in your VC's. Use .default or .lightContent to change the status bar color. I tested using Xcode 8 and swift 3:
override func preferredStatusBarStyle() -> UIStatusBarStyle {
return UIStatusBarStyle.default;
}
I created a new tabbed application with Xcode 7.3.1 and swift 2.3. I have two tabs with the classes associated FirstViewController and SecondViewController. In FirstViewController I added the following method:
override func preferredStatusBarStyle() -> UIStatusBarStyle {
return UIStatusBarStyle.Default;
}
And In the SecondViewController, I changed the background to black and I added the following method:
override func preferredStatusBarStyle() -> UIStatusBarStyle {
return UIStatusBarStyle.LightContent;
}
Finally, I added two buttons in the FirstViewController. One button present a controller modally and the other button present through push. When I presented the view modally the 'preferredStatusBarStyle' work but when I presented through push I need to add the following line of code:
self.navigationController?.navigationBar.barStyle = .Black
If you really don't want to override the delegate methods for preferredStatusBarStyle, you can still use:
UIApplication.sharedApplication().statusBarStyle = .LightContent
by removing:
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
UIApplication.sharedApplication().statusBarStyle = .LightContent
}
and let just let the status bar be set by whats happening in viewWillAppear. Obviously this is more prone to error, but if your navigation is relatively linear then it would be the simplest solution