I have a UIViewController A which presents modally another UIViewController B which takes up only the bottom half the screen height.
Back when I was still using Xcode 10, when the modal ViewController B is presented, a dark overlay will cover ViewController A and I will also set the view.alpha of ViewController A to 0.5 using these methods:
func presentBottomSheet() {
let viewController = BottomSheetModalVC()
viewController.modalPresentationStyle = .overCurrentContext
DispatchQueue.main.async { [weak self] in
self?.dimParent()
self?.parentViewController?.present(viewController, animated: true, completion: nil)
}
}
func dimParent() {
UIView.transition(with: parentVC.view, duration: 0.6, options: [.curveEaseOut], animations: {
parentVC.view.alpha = 0.5
})
}
in order to shift the user's focus to the modal view. However, when I tried to compile this with Xcode 11, the black overlay is done and I am left with a parent view who only becomes half visible when the modal view slides up from the bottom. Was there a change in this overlay behaviour?
My screen looks something similar to this. But after Xcode 11, the black overlay is no longer there leaving me with a completely transparent overlay.
Create a UIView in ViewControllerB. In your case its BottomSheetModalVC
var backgroundView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.black.withAlphaComponent(0.7)
return view
}()
Add the backgroundView as a subView to the presentingViewController's view
override func viewWillLayoutSubviews() {
presentingViewController?.view.addSubview(backgroundView)
backgroundView.frame = presentingViewController?.view.bounds ?? .zero
}
Remove this backgroundView in viewWillDisappear
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
UIView.animate(withDuration: 0.3) {
self.backgroundView.removeFromSuperview()
}
}
Found my own answer to this silly question.
So it turns out that the UINavigationController presenting ViewController A & B had its background color set to white so when ViewController A has its alpha set to 0.5, it just looks like it was disappearing but not dimming.
I have set my NavigationController background color to black now and everything is back to normal.
Related
Using Xcode 10+, Swift 4, iOS 11.4+
First let me say that I'm not using a Navigation Controller -
I'm adding a ViewController to another as a child using this basic code:
topController.addChildViewController(childVC)
topController.view.addSubview(childVC.view)
childVC.didMove(toParentViewController: topController)
The child is smaller than the parent and has a few buttons, one of which will animate it out of view.
I'm not using present/dismiss as it always covers the entire screen.
I'd like it to be modal - once it's animated into place, nothing else on screen (behind it) should be usable until it is animated out of view.
How can I make the childVC be modal?
You could try adding the controller to a UIWindow which has windowLevel = UIWindowLevelAlert + 1 instead. Then after the dismiss animation finishes you could remove the window. Here is a sample code snippet that seems to work:
func presentChildVC() {
modalWindow = UIWindow(frame: UIScreen.main.bounds)
let rootController = UIViewController()
rootController.view.backgroundColor = .clear
rootController.addChild(childController)
rootController.view.addSubview(childController.view)
childController.didMove(toParent: rootController)
modalWindow?.rootViewController = rootController
modalWindow?.windowLevel = .alert + 1
modalWindow?.makeKeyAndVisible()
modalWindow?.backgroundColor = .clear
UIView.animate(withDuration: 2, animations: {
self.childController.view.alpha = 1
})
}
func dismissChildVC() {
UIView.animate(withDuration: 2, animations: {
self.childController.view.alpha = 0
}, completion: { _ in
self.modalWindow?.isHidden = true
self.modalWindow = nil
})
}
1) The child is smaller than the parent:-
You just need to update your child view frame same like parent view.
topController.addChildViewController(childVC)
topController.view.addSubview(childVC.view)
**childVC.view.frame.size.height = self.view.frame.size.height**
childVC.didMove(toParentViewController: topController)
2) has a few buttons, one of which will animate it out of view :-
Set Click Event on buttons like this to remove child view from parent
self.willMove(toParentViewController: nil)
self.view.removeFromSuperview()
self.removeFromParentViewController()
I made a custom UIStoryboardSegue but when it unwinds, it seems to cause my UINavigationBar to go black, then back to it's correct color. See the GIF below.
My custom segue just makes the the new ViewController come down from the top and leave going back up to the top.
Here is the UIStoryboardSegue code:
import UIKit
class SlideDownSegue: UIStoryboardSegue {
var duration: Double = 0.5
override func perform() {
let screenHeight = UIScreen.main.bounds.size.height
let toVC = self.destination
let fromVC = self.source
toVC.view.transform = CGAffineTransform(translationX: 0, y: -screenHeight)
UIApplication.shared.keyWindow?.insertSubview(toVC.view, aboveSubview: fromVC.view)
UIView.animate(withDuration: duration, delay: 0, options: UIViewAnimationOptions.curveEaseInOut, animations: {
toVC.view.transform = CGAffineTransform.identity
}, completion: {
success in
fromVC.present(toVC, animated: false, completion: nil)
})
}
}
class UnwindSlideDownSegue: UIStoryboardSegue {
override func perform() {
let screenHeight = UIScreen.main.bounds.size.height
let toVC = self.destination
let fromVC = self.source.parent!
fromVC.view.superview?.insertSubview(toVC.view, at: 0)
UIView.animate(withDuration: 0.5, delay: 0, options: UIViewAnimationOptions.curveEaseInOut, animations: {
fromVC.view.transform = CGAffineTransform(translationX: 0, y: -screenHeight - 100)
}, completion: {
success in
fromVC.dismiss(animated: false, completion: nil)
})
}
}
If I let the unwind do the default where it just leaves by going to to the bottom of the screen but keep my custom for showing the new View, the UINavigationBar maintains it's correct color, it's only when I used my code provided for unwind that the UINavigationBar goes black during the animation.
Any hints would be much appreciated.
---EDIT---
I played with it a little, if I go into the AppDelegate and change UINavigationBar.appearance().isTranslucent = false to true, I instead get a white background, but then it just appears that the Navigation Bar is suddenly appearing. I'm wondering if it is for some reason being unloaded and then loaded back in once the View Controller is active.
---EDIT 2---
I am sorta able to fix it with a hack. In the AppDelegate inside func application(... didFinishLaunchingWithOptions ...) I added in self.window?.backgroundColor = UIColor.{your color} but all that does is make that black part now appear my color, the Navigation Bar is still disappearing during the segue for some reason.
This is a perfect opportunity to use snapshotView(afterScreenUpdates:). That's how UIKit performs many of their stock transitions (even in UINavigationController) and it's a tremendous way to perform custom transitions, particularly if you want to apply effects. Take a snapshot of the destination (which will have an intact navigation bar), insert it right below the source, perform the animation, dismiss the source so that the destination is ready, and finally remove the snapshot which you won't even notice because it will mirror the destination exactly.
If you want a simple trick ,try to set background color to window in AppDelegate's didFinishLaunchingWithOptions to your navigationbar's color, it works for me :)
like this
window?.backgroundColor = myColor
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
I'm trying to figure out how to hide the tab bar in my iOS swift app. I don't care about any fancy animations or anything. Just something I can put in the ViewDidLoad() function.
You can simply use this in your ViewDidLoad() method.
self.tabBarController?.tabBar.hidden = true
For Swift 3.0, 4.0, 5.0:
self.tabBarController?.tabBar.isHidden = true
Or you can change z position of tab bar this way:
self.tabBarController?.tabBar.layer.zPosition = -1
and if you want to show it again then:
self.tabBarController?.tabBar.layer.zPosition = 0
The accepted answer works, but the transition to other view has a choppy animation (The tab Bar animation)
Also wanted to add although Kalpesh's solution worked perfectly for me, I found out that every view controller has an attribute for hidesBottomBarWhenPushed (check out storyboard.) If you wish to hide tab bar, you should put a tick on that. And it would work great.
Update: Im not sure if this is a known thing, but here's what apple documentation page says:
A view controller added as a child of a navigation controller can
display an optional toolbar at the bottom of the screen. The value of
this property on the topmost view controller determines whether the
toolbar is visible. If the value of this property is true, the toolbar
is hidden. If the value of this property is false, the bar is
visible.
I think this means that you have to set the basic value of hidesBottomBarWhenPushed at the topmost view controller (the first one on the navigation stack.) Once you have set that to true, you can change to false or true for the other viewcontrollers on the stack. But, if your topmost view controller's hidesBottomBarWhenPushed value is false, it will not show a tab bar for other controllers on the navigation stack.
Before push set controller.hidesBottomBarWhenPushed = true
let objCreateEventVC = CreateEventVC()
objCreateEventVC.hidesBottomBarWhenPushed = true
self.navigationController?.pushViewController(objCreateEventVC, animated: false)
No need to set tabBar's isHidden property.
Simply, Go to ViewController (in StoryBoard) -> Attribute inspector -> Under 'View Controller' section select 'Hide Bottom Bar on Push' checkbox. This works like a charm.
If you go the 'isHidden' way you need to do a lot of handling, i.e. to make it appear again when you go back and also to remove the bottom empty space after hiding tabBar.
Swift 3.
self.tabBarController?.tabBar.isHidden = true
You can also set it in extension (use Dharmesh Kheni answer)
extension UITabBar {
func tabsVisiblty(_ isVisiblty: Bool = true){
if isVisiblty {
self.isHidden = false
self.layer.zPosition = 0
} else {
self.isHidden = true
self.layer.zPosition = -1
}
}
This is the way programmatically for Swift 4.0, 4.1, 4.2, 5.0 and later >:
tabBarController?.hidesBottomBarWhenPushed = true
or
hidesBottomBarWhenPushed = true
Hide & Show Tab Bar With Animation
For those looking to hide/show the tab bar with animation.
Since iOS 13, the behavior of the UITabBar has changed for animations. You can no longer use CGAffineTransform and instead you should animate its frame position.
See my full guide here: hide & show tab bar with animation
To hide the tab bar:
UIView.animate(withDuration: 0.7, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.7, options: .curveEaseOut) {
if let tabBarFrame = self.tabBarController?.tabBar.frame {
self.tabBarController?.tabBar.frame.origin.y = navigationController.view.frame.maxY + tabBarFrame.height
}
navigationController.view.layoutIfNeeded()
} completion: { _ in
self.tabBarController?.tabBar.isHidden = true
}
To show the tab bar:
UIView.animate(withDuration: 0.7, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.7, options: .curveEaseOut) {
if let tabBarFrame = self.tabBarController?.tabBar.frame {
self.tabBarController?.tabBar.frame.origin.y = navigationController.view.frame.maxY - tabBarFrame.height
}
navigationController.view.layoutIfNeeded()
}
Make sure to call layoutIfNeeded() inside the UIView.animate to trigger the changes with animation.
To hide the navigationBar and the tabBar I use the next function:
var tabBarHeight : CGFloat!
func fullScreenAction(){
if navigationController?.isNavigationBarHidden ?? false {
//Show navigationBar
navigationController?.setNavigationBarHidden(false, animated: false)
//Show tabBar
tabBarController?.tabBar.isHidden = false
//Update the height of tabBar
if (!(tabBarController?.tabBar.frame.size.height.isEqual(to: 0))!) {
tabBarHeight = self.tabBarController?.tabBar.frame.size.height
}
tabBarController?.tabBar.frame.size.height = tabBarHeight
} else {
//Hide navigationBar
navigationController?.setNavigationBarHidden(true, animated: false)
//Hide tabBar
tabBarController?.tabBar.isHidden = true
//Update the height of tabBar
tabBarHeight = tabBarController?.tabBar.frame.size.height
tabBarController?.tabBar.frame.size.height = 0
}
}
When the screen orientation has changed the height of tabBar change too, so I use the next function to exit of fullscreen to resize the height:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
if navigationController?.isNavigationBarHidden ?? false {
navigationController?.setNavigationBarHidden(false, animated: false)
tabBarController?.tabBar.isHidden = false
}
}
I hope it is useful for you.
Here is my code. it's just to hide its tabbar. (If no frames are well established there will be a black view at the bottom. )
var oldTabbarFr: CGRect = .zero
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
oldTabbarFr = self.tabBarController?.tabBar.frame ?? .zero
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.tabBarController?.tabBar.isHidden = true
self.tabBarController?.tabBar.frame = .zero
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.tabBarController?.tabBar.isHidden = false
self.tabBarController?.tabBar.frame = oldTabbarFr
}