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).
Related
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()
I'm trying to make tab bar with sliding (or may be swipe is right word?) effect of changing ViewControllers.
I create two ViewControllers with TableView on whole screen, but with restrictions - top edge of table not overlap top layout guide.
I link this ViewControllers with TabBarController and when I use default animation - it's OK, work fine. But I want sliding animation and do something like (swift3):
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
animateSliding(fromController: selectedViewController, toController: viewController)
return true
}
func animateSliding(fromController: UIViewController?, toController: UIViewController?) {
let fromView: UIView = fromController!.view;
let toView: UIView = toController!.view;
fromView.superview?.addSubview(toView);
toView.frame.origin.x = UIScreen.main.bounds.size.width;
UIView.animate(withDuration: 0.3,
animations: {
toView.frame.origin.x = 0;
fromView.frame.origin.x -= UIScreen.main.bounds.size.width;
},
completion: nil);
}
(it's not complete animation, just sample)
Now I have animation I wanted but second's ViewController's table overlap top guide when appear (when slide over first viewController). If I change position ViewControllers in TabBar (first became second and second became first) situation change - not first controller's table overlap top guide (when appear)
Have you tried XLPagerTabStrip? I believe that it is pretty similar to what you want to achive.
If not, you can base your code in that implementation.
I have a scrollView contains 2 child viewController. You can see VC2 not layout properly.
I found if view is not yet visible onscreen.
safeAreaInsets is always 0.
I can call vc2.view.setNeedsLayout() to fix this problem when scroll ended.
But the layout is not correct until scroll ended.
The document says
If the view is not currently installed in a view hierarchy, or is not
yet visible onscreen, the edge insets in this property are 0.
So how can I fix this situation.
Autolayout
instead of referencing the current view's safeAreaInsets, set it to the UIApplication:
(UIApplication.shared.delegate?.window??.safeAreaInsets.bottom)
In your child view controllers if you set the view controllers additionalSafeAreaInsets equal to the window's safe area insets they will layout correctly respecting the safe areas.
I found I had to do this inside of viewDidLoad() and viewWillTransition(to size: CGSize, with coordinator: UIVIewControllerTransitionCoordinator
Inside of viewWillTransition you will want to set the additionalSafeAreaInsets in the animation block of the coordinator:
coordinator.animate(alongsideTransition: { _ in
if #available(iOS 11.0, *) {
self.additionalSafeAreaInsets = UIApplication.shared.delegate?.window??.safeAreaInsets
}
}, completion: nil)
I was building a custom paging view controller and ran into this issue as well #PowHU.
The only solution that seemed to work for me was to set the view controller's view class in the storyboard to a custom class I created called AlwaysSafeAreaInsetsView.
import UIKit
class AlwaysSafeAreaInsetsView: UIView {
#available(iOS 11.0, *)
override var safeAreaInsets: UIEdgeInsets {
if let window = UIApplication.shared.keyWindow {
return window.safeAreaInsets
}
return super.safeAreaInsets
}
}
If I see properly your container view is pinned to the top and bottom of it's superview. Pin it to the Safe Area, and your child view controllers will be laid out properly.
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 making an app where the iPad can be used in both landscape and portrait mode.
The configuration of my UIViewControllers is the following :
I have a UIViewController with a UICollectionView which has the following constraints : top, leading, trailing, and bottom set to 0 with its superview (the superview is the UIView of my UIViewController).
When I tap a cell, I push another UIViewController in the UINavigationController which is a detail viewController of the cell. Simple.
When I change the device orientation in the first view controller, the UICollectionView bounds are set properly.
Here come my problem :
If I select a cell in the UICollectionView, the second detail viewController is displayed. Then if in this second ViewController I change the device orientation and then I go back into the first viewController, the view of my UIViewController hasn't been rotated. I don't know why, I tried this without success :
self.view.layoutIfNeeded() in viewDidAppear() of my firstViewController to update constraints according the current orientation, it didn't work. This didn't work because I noticed one thing :
The frame view of my first viewController is still the same as the previous orientation, it didn't change... But if I rotate again the device, the UIViewController detect the orientation changes, and the view bounds are set properly.
So my problem is : when I'm in the second view controller, and that I'm rotating the device, and then that I go back in the previous view controller, the previous controller hasn't detected the orientation change I just made in the second view controller.
GIF Demo :
After that I made a rotation from portrait to landscape mode, the view is not changed as the debug view hierarchy tool shows below :
The UIView is still in portrait, so the UICollectionView constraint's can't be updated with the new orientation...
Any ideas?
Resolved by resetting the frame of the view in my first viewController with the screen bounds in the viewWillAppear method :
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
self.view.frame = UIScreen.main.bounds
self.view.layoutIfNeeded()
}
Like that the main UIView of my viewController have the right frame, and the constraints can be updated automatically when I call layoutIfNeeded() on my UIView.
Found need to reset the frame size of collectionview. add self.collectionview.frame.size = size
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
if let layout = self.collectionview.collectionViewLayout as? UICollectionViewFlowLayout {
let width = (size.width - 30) / 3
layout.itemSize = CGSize(width:width, height:width)
self.collectionview.frame.size = size
layout.invalidateLayout()
}
}