Inside of a ContainerView, I have a UITabBarController subclass where I have modified the UITabBar Y Position. Instead of it being on the bottom, I have moved it closer to the top using this code:
override func viewDidLayoutSubviews() {
tabBar.frame = CGRect(x: 0, y: 0, width: tabBar.frame.size.width, height: tabBar.frame.size.height)
super.viewDidLayoutSubviews()
delegate = self
selectedViewController?.view.frame.origin = CGPoint(x: 0, y: tabBar.frame.size.height)
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
selectedViewController?.view.frame.origin = CGPoint(x: 0, y: tabBar.frame.size.height)
}
It works great initially: the first tab view contains a UITableView, and it seems to adjust its frame for the new position of the UITabBar. However, if I push a ViewController on the stack, halfway through the push animation, the bottom of the view containing the UITableView jumps up like the UITabBar is still there. So, a portion of the top of the view ends up behind the UITabBar and upon popping this new ViewController off the stack, back to the UITabBarController, the view(with the tableView) remains in this position behind the new position of the tabBar. I've tried changing the view origin in ViewWillAppear, ViewWillDisappear, ViewDidDisappear, and none of these methods were able to reset the position of the view(containing the tableView) to beneath the new location of the UITabBar.
I'm not sure what's causing this to happen, or what lifecycle method to investigate.
Any ideas or suggestions?
EDIT: The TableView is the one actually changing it's Origin, NOT the view.
Seems like you might need to put the super.viewDidLayoutSubviews() call at the top of your override. Have you tried that? I would think that the superclass method actually also adjusts the position so your change might be getting overwritten.
I solved this layout issue by executing this code in both ViewWillAppear & ViewWillDisappear. I cannot explain why this works, but I do know that it does.
tabBar.invalidateIntrinsicContentSize()
tabBar.superview?.setNeedsLayout()
tabBar.superview?.layoutSubviews()
Related
I'm trying to add a UIView beneath the UINavigationBar in my UINavigationController.
The view will serve as a placeholder for information messages (for example if we are having issues and content is not getting updated).
Adding the view it self and setting it's constraints is not an issue, but it is overlapping the content of the views that is contained in the navigation controller, which is not want I want. How can I set the content of the contained viewcontroller to respect the space which this new view takes up?
The screenshot is showing my custom (orange) view overlapping the content of the viewController that was pushed on to the navigation controller.
Try Subclassing the UINavigationController and then add your orange view's height constraint to it. and call the function whenever you need it
import UIKit
class CustomNavigationController: UINavigationController{
#IBOutlet weak var topViewHeight: NSLayoutConstraint!
func animateHeight(height: CGFloat){
UIView.animate(withDuration: 0.2) {
self.viewControllers.forEach{ vc in
let v = vc.view.frame
vc.view?.frame = CGRect(x: 0, y: height, width: v.width, height: v.height)
}
self.topViewHeight.constant = height
}
}
}
how to use it?
in your VC where you want to show/hide it:
(self.navigationController as? CustomNavigationController)?.animateHeight(height: 50)
I have a UIPageViewController subclass that allows the user to swipe upward from the initial view controller to a second view controller. The initial view controller does not have a navigation bar (it's not connected to a UINavigationController), but the second one does. Currently, when I swipe up to present the second view controller, there is no space in the navigation bar for the status bar. When the transition is complete, the entire view controller layout updates instantly to add the status bar space. I want to make the second view controller anticipate that the status bar will be there and add the space for it so the view doesn't jolt after the transition.
Short video clip showing the jolt
Similar, but unanswered question
EDIT:
After closely looking at the way the view controller behaves during the transition, I was able to get a good look at what is going on here.
When I swipe up, the second view controller moves upward and aligns the top edge of the navigation bar (no status bar) with the top of the screen. It appears that the size/layout of the content is correct, so there's whitespace at the bottom that's the height of the status bar. See the screenshot:
When the transition is finished, space for the status bar is added and the view gets shifted downward.
I found a workaround that seems to work pretty well. I need to test this with the iPhone X to see if I need to make adjustments. If someone finds a better solution, I'll make it the accepted answer.
I basically shift my page view controller down by 20pt when it is loaded:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let f = view.frame
view.frame = CGRect(x: 0, y: 20, width: f.size.width, height: f.size.height)
view.setNeedsLayout()
view.layoutIfNeeded()
}
After the transition, I shift the page view controller back up to its original position at exactly the same time that the second view controller adds status bar space. This causes the second view controller to slide right below the status bar, where it should be. There's no noticeable jolt. I used this in my page view controller's delegate:
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
guard previousViewControllers.count > 0 else { return }
// if the transition completed and the view controller we transitioned from is the first view controller
if completed && viewControllers!.index(of: previousViewControllers.first!) == 0 {
let f = view.frame
view.frame = CGRect(x: 0, y: 0, width: f.size.width, height: f.size.height)
view.setNeedsLayout()
view.layoutIfNeeded()
}
}
I've a custom transition between two controller that works close to the iOS mail app, which one stays on top of the other with some implemented scrolling behavior.
If I present a new view controller from the Presented view controller which isn't full screen sized, and then I dismiss this new presented view controller, the previous Presented view controller changes its height and then resizes itself.
I know this might be a little confusing but check the gif example below.
As you can see, If I present this custom image picker and then dismiss it, the view controller which presented it warps to full screen and then resizes to the initial value.
How can I prevent this from happening? I want the ViewController which presents the image picker keeps its height.
After the dismiss you can see the resize happening.
Setting the presenting view controllers size
Since it's a UIViewControllerAnimatedTransitioning I create a custom presentation and the size it's set has it's own identity
class CustomPresentationController: UIPresentationController {
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController!) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
override var frameOfPresentedViewInContainerView: CGRect {
let containerBounds = self.containerView?.bounds
let origin = CGPoint(x: 0.0, y: ((containerBounds?.size.height)! * 0.05))
let size = CGSize(width: (containerBounds?.size.width)! , height: ((containerBounds?.size.height)! * 0.95))
// Applies the attributes
let presentedViewFrame: CGRect = CGRect(origin: origin, size: size)
return presentedViewFrame
}
override func containerViewWillLayoutSubviews() {
presentedView?.frame = frameOfPresentedViewInContainerView
}
}
Any hint?
thanks
I think that is where the issue is. You are forcing the frame size which is not working out. You should use something like preferredContentSize.
You can simply add this to the viewDidLoad of your CustomPresentationController.
Alternatively you may also try modalPresentationStyle as "Over Current Context"
You can refer very good examples of how you can keep some part of VC as transparent here
I have a custom UITableView with custom cells (70px height for each cell).
I have also a 49px UITabBar, but it's hides the tableView.
I've tried to add this line to the TableView awakeFromNib method but it didn't work:
self.commentsTableView.contentInset = UIEdgeInsetsMake(0, 0, 49, 0)
Any idea how can I solve this?
Thanks!
i don't know what you did exactly, but try like this:
self.edgesForExtendedLayout = UIRectEdgeAll;
self.tableview.contentInset = UIEdgeInsetsMake(0.0f, 0.0f, CGRectGetHeight(self.tabBarController.tabBar.frame), 0.0f);
I hope, this will work.
I ran into this issue when dealing with a table view in a navigation controller that did not have translucent bars. I performed a setup similar to the following:
override func viewDidLoad() {
super.viewDidLoad()
// Without this there is some extra fast inertia when slowly
// scrolling to the top.
extendedLayoutIncludesOpaqueBars = true
// Don't extend the tableview past the bottom bar, though.
// If we do then a tab bar or bottom nav bar will block
// content.
edgesForExtendedLayout = [.top, .left, .right]
}
However, I later discovered that a couple of checkboxes were unchecked in a storyboard higher up the hierarchy. Specifically these two:
Checking these two boxes removed the need to care about the content insets and the layout extending behavior in that view controller
This did the trick for me in Swift 3.
if let tabBarController = tabBarController {
self.tableView.contentInset = UIEdgeInsetsMake(0.0, 0.0, tabBarController.tabBar.frame.height, 0.0);
}
You should config the corresponding view controller with following code to remove the edges extend (It defaults to UIRectEdgeAll)
edgesForExtendedLayout = []
Try this
self.commentsTableView.contentInset = UIEdgeInsetsMake(49, 0, 0, 0)
self.commentsTableView.setContentOffset(CGPoint.init(x: 0, y: -49), animated: false)
If you changed self.hidesBottomBarWhenPushed=true in previous view controller, then make sure to change it to self.hidesBottomBarWhenPushed=false inside override func prepare(for segue: UIStoryboardSegue, sender: Any?) {}
Try to use constraints for tableView and TabBar like:
I have an app with two tabs, each one containing a table view. The whole is wrapped in a UINavigationController. Here is my storyboard:
When I run my app, the first tab is OK:
But on the second one, the table view starts under the navigation bar:
Even worse, when I rotate the screen from the second tab, the second tab is now OK, but the first isn't any more, the table view has an extra margin top:
If I rotate back to portrait from the first tab, I return to the initial state (first tab OK, second tab with table view starting under the navigation bar). In fact, each time I rotate the screen, the displayed tab is OK after the rotation, but the other one isn't.
It seems that I need to do something when my views are shown after a tab change, and that thing seems to be done when the screen rotates, but what is it??
EDIT: I forgot to mention that I don't want to uncheck the "Extend Edges: Under Top/Bottom Bars" checkboxes, because I want my table views to scroll under the nav and tab bars.
OK, I got it: first uncheck "Adjust Scroll View Insets" on the UITabBarController, and then add this code on each UITableViewController:
override func viewWillAppear(animated: Bool) {
// Call super:
super.viewWillAppear(animated);
// Update the table view insets:
updateTableViewInsets();
}
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
// Animate:
coordinator.animateAlongsideTransition({ (UIViewControllerTransitionCoordinatorContext) -> Void in
// Update the table view insets:
self.updateTableViewInsets();
}, completion: { (UIViewControllerTransitionCoordinatorContext) -> Void in })
// Call super:
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
}
private func updateTableViewInsets() {
// Get the bars height:
let navigationBarHeight = self.navigationController!.navigationBar.frame.size.height;
let statusBarHeight = UIApplication.sharedApplication().statusBarFrame.size.height;
let tabBarHeight = self.tabBarController!.tabBar.frame.size.height;
// Create the insets:
let insets: UIEdgeInsets = UIEdgeInsetsMake(navigationBarHeight + statusBarHeight, 0, tabBarHeight, 0);
// Update the insets:
self.tableView.scrollIndicatorInsets = insets;
self.tableView.contentInset = insets;
}
Now I'm handling the insets myself, and everything is smooth. That should work out of the box IMHO, though...
EDIT: It works out of the box with iOS 9, Apple have probably fixed the bug. The code above works with iOS 8 & 9 (and maybe lower, I didn't try).