Avoid iOS status bar jitter when switching between pages of UIPageViewController - ios

I'm building a simple app that uses a UIPageViewController with two pages. On one page, I want to show the status bar, but on the other page I do not. To make the transition look nice, I slide the status bar in and out after the user fully arrives on a page of the UIPageViewController, depending on which page it is.
This works fine on devices with the sensor housing (notch) like the iPhone XR, but it looks very glitchy on devices with a normal rectangular-shaped screen, like the iPhone 8.
iPhone XR example — Height of navigation bar and content below it stay consistent.
iPhone 8 example — When status bar starts animating (sliding) in, the content jumps up, but then returns to it's original position as the navigation bar animates down.
Best Possible Solution?
It seems like one of the best solutions would be to somehow force the navigation bar to always be the full height of the navigation bar + status bar height, so the navigation bar and content stay in the same place while the status bar animates down. This would be similar to the behavior on iPhone XR screen. How can I achieve this?
Existing Code
On my UIPageViewController, I keep track of a currentViewController attribute, and update the overridden prefersStatusBarHidden variable accordingly.
private var currentViewController: UIViewController?
override var prefersStatusBarHidden: Bool {
if let current = currentViewController,
current == settingsNavigationController {
return false
} else {
return true
}
}
To set the currentViewController variable and call the necessary setNeedsStatusBarAppearanceUpdate() method, I override the didFinishAnimating method of UIPageViewControllerDelegate:
extension PageViewController: UIPageViewControllerDelegate {
func pageViewController(_ pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool) {
currentViewController = pageViewController.viewControllers?.first
UIView.animate(withDuration: 0.2) { () -> Void in
self.setNeedsStatusBarAppearanceUpdate()
}
}
}
To make it less jarring, I use the .slide animation (if I leave this out so there's no animation, it's still jumpy on rectangular screens):
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .slide
}
UPDATE:
Thanks to Leon Deriglazov's answer, I've been able to avoid the jumpiness of the content by subclassing the UINavigationController. I added the following code to trigger an animation (with the same duration as the status bar animation) that moves the content up while the status bar slides down, making the content appear to stay in the same place:
class SettingsNavigationController: UINavigationController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 0.2) {
self.additionalSafeAreaInsets.top = 0
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
additionalSafeAreaInsets.top = 20
}
}
Note: this does not contain the additional logic necessary to detect what type of device it is.
Here's what it looks like:
While I'd ideally like to avoid having the navigation bar animated down with the status bar, like on the iPhone XR screen, this is a much smoother experience. If anyone has any ideas about how to keep the navigation bar fixed in the same location I'd still appreciate that.

I would consider employing additionalSafeAreaInsets in your Settings controller to make it always account for the height of the status bar. Then you might have to switch it out when you present the status bar (not 100% sure about that).

Related

How do I hide the status bar in an iOS app without causing the UINavigationBar to jump? [duplicate]

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.

Accessibility set focus to navigation bar title item

Overview:
I would like to set the accessibility focus to the navigation bar's title item.
By default the focus is set from top left, meaning the back button would be on focus.
I would like the title item to be in focus.
Attempts made so far:
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification,
navigationController?.navigationBar.items?.last)
Problem:
The above code makes no difference, the back button is still in focus.
Possible Cause:
Not able to get the item corresponding to the title to be able to set the focus.
Solution 1
I don't like it, but it was the minimum amount of hacking that does not rely on digging through hidden subviews (internal implementation of UINavigationBar view hierarchy).
First in viewWillAppear, I store a backup reference of the back button item,
and then remove the back button item (leftBarButtonItem):
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
backButtonBackup = self.navigationItem.leftBarButtonItem
self.navigationItem.leftBarButtonItem = nil
}
Then I restore the back item, but only after I dispatch the screen changed event in viewDidAppear() :
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
self?.navigationItem.leftBarButtonItem = self?.backButtonBackup
}
}
Solution 2:
Disable all accessibility on the nav bar and view controller up until viewDidAppear() is finished:
self.navigationController.navigationBar.accessibilityElementsHidden = true
self.view.accessibilityElementsHidden = true
, and then in viewDidAppear manually dispatching the layout element accessibility focused event to the label subview of UINavigationBar:
UIAccessibilityPostNotification( UIAccessibilityLayoutChangedNotification, self.navigationController.navigationBar.subviews[2].subviews[1])
// The label buried inside the nav bar. Not tested on all iOS versions.
// Alternately you can go digging for the label by checking class types.
// Then use DispatchAsync, to re-enable accessibility on the view and nav bar again...
I'm not a fan of this method either.
DispatchAsync delay in viewDidAppear seems to be needed in any case - and I think both solutions are still horrible.
I invoked UIAccessibilityScreenChangedNotification on navigation title from viewDidLoad and it worked
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification,
self.navigationItem.title);
First, we'll need to create an useful extension:
extension UIViewController {
func setAccessibilityFocus(in view: UIView) {
UIAccessibility.post(notification: .screenChanged, argument: view)
}
}
Then, we'll be able to set our focus in the navigation bar title like this:
setAccessibilityFocus(in: self.navigationController!.navigationBar.subviews[2].subviews[1])

How to fix why status bar space behind ScrollView in Xcode?

I added a scroll view to my view which is hover the status bar (I hid it). The scroll view is working fine, but when I'm scrolling to the top, I have a white space which disappears when I tap on my screen, and appears again when I scroll down then top.
I noticed that the scroll bar is not going to the top of my view, but stopped at the status bar.
Here are screenshots which show you what I mean.
Here I'm at the top of my view but the scroll bar isn't:
Here is the same view with the white status bar which appears when I scroll top again:
It disappear when I tap on my screen or scroll down.
Here are my constraints:
I think it's a problem of Layout Margin or something like that, but I don't what I should change?
I hide the status bar like that in my view controller:
override func viewWillAppear(_ animated: Bool) {
UIApplication.shared.keyWindow?.windowLevel = UIWindowLevelStatusBar
super.viewWillAppear(animated)
}
EDIT: Even if I comment the line which hides the status bar, I still have the same problem with my scroll view. So the problem doesn't come from how I hide it.
As Sam said, I changed the content insets to "Never" on the scroll view and it works.
While unrelated to your question, I have to react to the way you hide the status bar - the proper way is to override prefersStatusBarHidden in your view controller and call self.setNeedsStatusBarAppearanceUpdate() in your viewWillAppear:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.setNeedsStatusBarAppearanceUpdate()
}
override var prefersStatusBarHidden: Bool {
return true
}
UPDATE
Since your view controller is inside of a UINavigationViewController, you need to override childViewControllerForStatusBarHidden in UINavigationViewController to use visibleViewController as the controller to determine status bar hidden (I added override to childViewControllerForStatusBarStyle for the consistence):
extension UINavigationController {
open override var childViewControllerForStatusBarStyle: UIViewController? {
return visibleViewController
}
open override var childViewControllerForStatusBarHidden: UIViewController? {
return visibleViewController
}
}

setNavigationBarHidden animation not working as expected on 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.

Hide navigation bar on scroll

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.

Resources