I'm trying to animate the status bar based on the scroll direction of my scroll view. Currently, I am able to hide the status bar, however I can't get the change to animate.
I thought there was a method that did handled this, considering one exists for the navigation bar, and I found the method setStatusBarHidden(_:with:), however this method appears to have been deprecated since ios9.
Right I am setting the status in an animation block and calling .setNeedsStatusBarAppearanceUpdate() as seen below
DispatchQueue.main.async {
UIView.animate(withDuration: 0.3, delay: 0, options: UIViewAnimationOptions.curveEaseOut, animations: {
UIApplication.shared.isStatusBarHidden = true
self.setNeedsStatusBarAppearanceUpdate()
}, completion: { (completed) in
})
}
One other thing, is it still possible to set the animation style? Before, using the setStatusBarHidden method, you could choose between a couple different animation styles (fade and slide).
What you're doing was never right. You should not be talking to the shared application. It is the top-level view controller that is in charge of the status bar visibility, through the value of its prefersStatusBarHidden property.
The following snippet demonstrates how the top-level view controller can toggle status bar visibility with animation (in response to the tapping of a button, for demonstration purposes):
var hide = false
override var prefersStatusBarHidden : Bool {
return self.hide
}
#IBAction func doButton(_ sender: Any) {
self.hide = !self.hide
UIView.animate(withDuration:0.4) {
self.setNeedsStatusBarAppearanceUpdate()
self.view.layoutIfNeeded()
}
}
The same is true of .fade and .slide; these are to be given as the value of the top level view controller's override of the preferredStatusBarUpdateAnimation property.
Related
I try to animate a left/right swipe of TabBarControllers views by myself. When you swipe, the new incoming view is shifted up by the height of the iOS status bar. So, its directly attached to the top of the screen and overlapping the status bar. When the animation finished, the view jumps down to the correct position.
This only occurs on first load of the views/when view has not been loaded yet. In case of failure isViewLoaded was false before animation starts.
UIView.transition(from: selectedViewController!.view, to: getTabView(index),
duration: 0.3, options: UIView.AnimationOptions.transitionCrossDissolve,
completion: { _ in selectedIndex = index })
...
func getTabView(_ index:Int) -> UIView {
if self.viewControllers![index].isViewLoaded {
//print "loaded" - all fine - view's position is correct
//tried loadView() but doesnt change anything
}
else {
//print "first load" - view is shifted up
}
return viewControllers![index].view //when accessing .view, the view is loaded anyway (docs)
}
By the way, the problem doesnt occur when I switch the tabs via the tabBar-buttons.
Thank you very much!
It's still strange but I fixed it.
First I set up a UITabBarControllerDelegate to CrossDissolve between tabs when touching the Tabbar Buttons.
Because this works without the glitch, I thought I could somehow call the delegate function
tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool
programmatically instead of
UIView.transition(from: selectedViewController!.view, to: getTabView(index),
duration: 0.3, options: UIView.AnimationOptions.transitionCrossDissolve,
completion: { _ in selectedIndex = index })
This does not work, there was another glitch. But it was somehow obvious before because I read its not intended to call this delegate function directly.
By trying this and that it worked suddenly. Accidentally I had uncommented both: delegate function call and right after the UIView.transition. This works.
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).
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])
I have NavigationController that handles navigation through my app.
According to my design, the very first view should have no visible NavigationBar. All the others after, will.
In this FirstView, I'm using this so far to hide the NavBar, inside the ViewDidLoad:
self.navigationController?.isNavigationBarHidden = true
From this FirstView I can access other Views. In these other views I show the NavBar using:
self.navigationController?.isNavigationBarHidden = false
My problem is that:
When I navigate from a View with Visible NavBar, back to the FirstView with the Hidden NavBar, the NavBar is now visible.
Basically the NavBar only hides the very first time then shows if I use the back button.
How Can I Prevent this ?
Thank you!
Move that code to viewWillAppear() instead of viewDidLoad().
viewDidLoad() is only called once per instantiated view controller, whereas viewWillAppear() is called whenever the view controller is about to be presented on screen.
You can read more about the view controller lifecycle here.
Write below code in your FirstViewController's viewWillAppear method.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated);
self.navigationController?.isNavigationBarHidden = true
}
And in your SecondViewController's viewWillAppear method write below code
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated);
self.navigationController?.isNavigationBarHidden = false
}
Do not try to hide and show nav bar in viewWillAppear and viewWillDisappear subsequetly in FirstViewController.
You can use this function to hide NavigationBar with cool animation:
func setupAnimationForNavigationBar(caseOfFunction: Bool) {
if caseOfFunction == true {
UIView.animate(withDuration: 0.5) {
self.navigationController?.navigationBar.transform = CGAffineTransform(translationX: 0, y: -200)
}
} else {
UIView.animate(withDuration: 0.5, animations: {
self.navigationController?.navigationBar.transform = CGAffineTransform.identity
})
}
}
If you want to hide NavigationBar, so set it "True" and if you want to call NavigationBar again, set it "False"
Issue:
Modally presented view controller does not move back up after in-call status bar disappears, leaving 20px empty/transparent space at the top.
Normal : No Issues
In-Call : No Issues
After In-Call Disappears:
Leaves a 20px high empty/transparent space at top revealing orange view below. However the status bar is still present over the transparent area. Navigation Bar also leaves space for status bar, its' just 20px too low in placement.
iOS 10 based
Modally presented view controller
Custom Modal Presentation
Main View Controller behind is orange
Not using Autolayout
When rotated to Landscape, 20px In-Call Bar leaves and still leaves 20px gap.
I opt-out showing status bar in landscape orientations. (ie most stock apps)
I tried listening to App Delegates:
willChangeStatusBarFrame
didChangeStatusBarFrame
Also View Controller Based Notifications:
UIApplicationWillChangeStatusBarFrame
UIApplicationDidChangeStatusBarFrame
When I log the frame of presented view for all four above methods, the frame is always at (y: 0) origin.
Update
View Controller Custom Modal Presentation
let storyboard = UIStoryboard(name: "StoryBoard1", bundle: nil)
self.modalVC = storyboard.instantiateViewController(withIdentifier: "My Modal View Controller") as? MyModalViewController
self.modalVC!.transitioningDelegate = self
self.modalVC.modalPresentationStyle = .custom
self.modalVC.modalPresentationCapturesStatusBarAppearance = true;
self.present(self.modalVC!, animated: true, completion: nil)
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
toViewController!.view.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.0, options: [.curveEaseOut], animations: { () -> Void in
toViewController!.view.transform = CGAffineTransform.identity
}, completion: { (completed) -> Void in
transitionContext.completeTransition(completed)
})
}
I've been looking for a solution for 3 days. I don't like this solution but didn't found better way how to fix it.
I'he got situation when rootViewController view has bigger height for 20 points than window, when I've got notification about status bar height updates I manually setup correct value.
Add method to the AppDelegate.swift
func application(_ application: UIApplication, didChangeStatusBarFrame oldStatusBarFrame: CGRect) {
if let window = application.keyWindow {
window.rootViewController?.view.frame = window.frame
}
}
After that it works as expected (even after orientation changes).
Hope it will help someone, because I spent too much time on this.
P.S. It blinks a little bit, but works.
I faced this problem too but after I put this method, problem is gone.
iOS has its default method willChangeStatusBarFrame for handling status bar. Please put this method and check it .
func application(_ application: UIApplication, willChangeStatusBarFrame newStatusBarFrame: CGRect) {
UIView.animate(withDuration: 0.35, animations: {() -> Void in
let windowFrame: CGRect? = ((window?.rootViewController? as? UITabBarController)?.viewControllers[0] as? UINavigationController)?.view?.frame
if newStatusBarFrame.size.height > 20 {
windowFrame?.origin?.y = newStatusBarFrame.size.height - 20
// old status bar frame is 20
}
else {
windowFrame?.origin?.y = 0.0
}
((window?.rootViewController? as? UITabBarController)?.viewControllers[0] as? UINavigationController)?.view?.frame = windowFrame
})
}
Hope this thing will help you.
Thank you
I had the same issue with the personnal hospot modifying the status bar.
The solution is to register to the system notification for the change of status bar frame, this will allow you to update your layout and should fix any layout issue you might have.
My solution which should work exactly the same for you is this :
In your view controller, in viewWillAppear suscribe to the UIApplicationDidChangeStatusBarFrameNotification
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(myControllerName.handleFrameResize(_:)), name: UIApplicationDidChangeStatusBarFrameNotification, object: nil)
Create your selector method
func handleFrameResize(notification: NSNotification) {
self.view.layoutIfNeeded() }
Remove your controller from notification center in viewWillDisappear
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIApplicationDidChangeStatusBarFrameNotification, object: nil)
You also need your modal to be in charge of the status bar so you should set
destVC.modalPresentationCapturesStatusBarAppearance = true
before presenting the view.
You can either implement this on every controller susceptible to have a change on the status bar, or you could make another class which will do it for every controller, like passing self to a method, keep the reference to change the layout and have a method to remove self. You know, in order to reuse code.
I think this is a bug in UIKit. The containerView that contains a presented controller's view which was presented using a custom transition does not seem to move back completely when the status bar returns to normal size. (You can check the view hierarchy after closing the in call status bar)
To solve it you can provide a custom presentation controller when presenting. And then if you don't need the presenting controller's view to remain in the view hierarchy, you can just return true for shouldRemovePresentersView property of the presentation controller, and that's it.
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return PresentationController(presentedViewController: presented, presenting: presenting)
}
class PresentationController: UIPresentationController {
override var shouldRemovePresentersView: Bool {
return true
}
}
or if you need the presenting controller's view to remain, you can observe status bar frame change and manually adjust containerView to be the same size as its superview
class PresentationController: UIPresentationController {
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
NotificationCenter.default.addObserver(self,
selector: #selector(self.onStatusBarChanged),
name: .UIApplicationWillChangeStatusBarFrame,
object: nil)
}
#objc func onStatusBarChanged(note: NSNotification) {
//I can't find a way to ask the system for the values of these constants, maybe you can
if UIApplication.shared.statusBarFrame.height <= 20,
let superView = containerView?.superview {
UIView.animate(withDuration: 0.4, animations: {
self.containerView?.frame = superView.bounds
})
}
}
}
I've been looking for a solution to this problem. In fact, I posted a new question similar to this one. Here: How To Avoid iOS Blue Location NavigationBar Messing Up My StatusBar?
Believe me, I've been solving this for a couple of days now and it's really annoying having your screen messed up because of the iOS's status bar changes by in-call, hotspot, and location.
I've tried implementing Modi's answer, I put that piece of code in my AppDelegate and modified it a bit, but no luck. and I believe iOS is doing that automatically so you do not have to implement that by yourself.
Before I discovered the culprit of the problem, I did try every solution in this particular question. No need to implement AppDelegate's method willChangeStatusBar... or add a notification to observe statusBar changes.
I also did redoing some of the flows of my project, by doing some screens programmatically (I'm using storyboards). And I experimented a bit, then inspected my previous and other current projects why they are doing the adjustment properly :)
Bottom line is: I am presenting my main screen with UITabBarController in such a wrong way.
Please always take note of the modalPresentationStyle. I got the idea to check out my code because of Noah's comment.
Sample:
func presentDashboard() {
if let tabBarController = R.storyboard.root.baseTabBarController() {
tabBarController.selectedIndex = 1
tabBarController.modalPresentationStyle = .fullScreen
tabBarController.modalTransitionStyle = .crossDissolve
self.baseTabBarController = tabBarController
self.navigationController?.present(tabBarController, animated: true, completion: nil)
}
}
I solve this issue by using one line of code
In Objective C
tabBar.autoresizingMask = (UIViewAutoResizingFlexibleWidth | UIViewAutoResizingFlexibleTopMargin);
In Swift
self.tabBarController?.tabBar.autoresizingMask =
UIViewAutoresizing(rawValue: UIViewAutoresizing.RawValue(UInt8(UIViewAutoresizing.flexibleWidth.rawValue) | UInt8(UIViewAutoresizing.flexibleTopMargin.rawValue)))`
You just need to make autoresizingMask of tabBar flexible from top.
In my case, I'm using custom presentation style for my ViewController.
The problem is that the Y position is not calculated well.
Let's say the original screen height is 736p.
Try printing the view.frame.origin.y and view.frame.height, you'll find that the height is 716p and the y is 20.
But the display height is 736 - 20(in-call status bar extra height) - 20(y position).
That is why our view is cut from the bottom of the ViewController and why there's a 20p margin to the top.
But if you go back to see the navigation controller's frame value.
You'll find that no matter the in-call status bar is showing or not, the y position is always 0.
So, all we have to do is to set the y position to zero.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let f = self.view.frame
if f.origin.y != 0 {
self.view.frame = CGRect(x: f.origin.x, y: 0, width: f.width, height: f.height)
self.view.layoutIfNeeded()
self.view.updateConstraintsIfNeeded()
}
}
Be sure to set the frame of the view controller's view you are presenting to the bounds of the container view, after it has been added to the container view. This solved the issue for me.
containerView.addSubview(toViewController.view)
toViewController.view.frame = containerView.bounds