iOS 11 iPhone X simulator UITabBar icons and titles being rendered on top covering eachother - iphone

Anyone having issue with the iPhone X simulator around the UITabBar component?
Mine seem to be rendering the icons and title on top of each other, I'm not sure if I'm missing anything, I also ran it in the iPhone 8 simulator, and one actual devices where it looks fine just as shown on the story board.
iPhone X:
iPhone 8

I was able to get around the problem by simply calling invalidateIntrinsicContentSize on the UITabBar in viewDidLayoutSubviews.
-(void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
[self.tabBar invalidateIntrinsicContentSize];
}
Note: The bottom of the tab bar will need to be contained to the bottom of the main view, rather than the safe area, and the tab bar should have no height constraint.

Answer provided by VoidLess fixes TabBar problems only partially. It fixes layout problems within the tabbar, but if you use viewcontroller that hides the tabbar, the tabbar is rendered incorrectly during animations (to reproduce it is best 2 have 2 segues - one modal and one push. If you alternate the segues, you can see the tabbar being rendered out of place). The code bellow fixes both problems. Good job apple.
class SafeAreaFixTabBar: UITabBar {
var oldSafeAreaInsets = UIEdgeInsets.zero
#available(iOS 11.0, *)
override func safeAreaInsetsDidChange() {
super.safeAreaInsetsDidChange()
if oldSafeAreaInsets != safeAreaInsets {
oldSafeAreaInsets = safeAreaInsets
invalidateIntrinsicContentSize()
superview?.setNeedsLayout()
superview?.layoutSubviews()
}
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
var size = super.sizeThatFits(size)
if #available(iOS 11.0, *) {
let bottomInset = safeAreaInsets.bottom
if bottomInset > 0 && size.height < 50 && (size.height + bottomInset < 90) {
size.height += bottomInset
}
}
return size
}
override var frame: CGRect {
get {
return super.frame
}
set {
var tmp = newValue
if let superview = superview, tmp.maxY !=
superview.frame.height {
tmp.origin.y = superview.frame.height - tmp.height
}
super.frame = tmp
}
}
}
Objective-C code:
#implementation VSTabBarFix {
UIEdgeInsets oldSafeAreaInsets;
}
- (void)awakeFromNib {
[super awakeFromNib];
oldSafeAreaInsets = UIEdgeInsetsZero;
}
- (void)safeAreaInsetsDidChange {
[super safeAreaInsetsDidChange];
if (!UIEdgeInsetsEqualToEdgeInsets(oldSafeAreaInsets, self.safeAreaInsets)) {
[self invalidateIntrinsicContentSize];
if (self.superview) {
[self.superview setNeedsLayout];
[self.superview layoutSubviews];
}
}
}
- (CGSize)sizeThatFits:(CGSize)size {
size = [super sizeThatFits:size];
if (#available(iOS 11.0, *)) {
float bottomInset = self.safeAreaInsets.bottom;
if (bottomInset > 0 && size.height < 50 && (size.height + bottomInset < 90)) {
size.height += bottomInset;
}
}
return size;
}
- (void)setFrame:(CGRect)frame {
if (self.superview) {
if (frame.origin.y + frame.size.height != self.superview.frame.size.height) {
frame.origin.y = self.superview.frame.size.height - frame.size.height;
}
}
[super setFrame:frame];
}
#end

There is a trick by which we can solve the problem.
Just put your UITabBar inside a UIView.
This is really working for me.
Or you can follow this Link for more details.

override UITabBar sizeThatFits(_) for safeArea
extension UITabBar {
static let height: CGFloat = 49.0
override open func sizeThatFits(_ size: CGSize) -> CGSize {
guard let window = UIApplication.shared.keyWindow else {
return super.sizeThatFits(size)
}
var sizeThatFits = super.sizeThatFits(size)
if #available(iOS 11.0, *) {
sizeThatFits.height = UITabBar.height + window.safeAreaInsets.bottom
} else {
sizeThatFits.height = UITabBar.height
}
return sizeThatFits
}
}

I added this to viewWillAppear of my custom UITabBarController, because none of the provided answers worked for me:
tabBar.invalidateIntrinsicContentSize()
tabBar.superview?.setNeedsLayout()
tabBar.superview?.layoutSubviews()

I had the same problem.
If I set any non-zero constant on the UITabBar's bottom constraint to the safe area:
It starts working as expected...
That is the only change I made and I have no idea why it works but if anyone does I'd love to know.

Fixed by using subclassed UITabBar to apply safeAreaInsets:
class SafeAreaFixTabBar: UITabBar {
var oldSafeAreaInsets = UIEdgeInsets.zero
#available(iOS 11.0, *)
override func safeAreaInsetsDidChange() {
super.safeAreaInsetsDidChange()
if oldSafeAreaInsets != safeAreaInsets {
oldSafeAreaInsets = safeAreaInsets
invalidateIntrinsicContentSize()
superview?.setNeedsLayout()
superview?.layoutSubviews()
}
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
var size = super.sizeThatFits(size)
if #available(iOS 11.0, *) {
let bottomInset = safeAreaInsets.bottom
if bottomInset > 0 && size.height < 50 {
size.height += bottomInset
}
}
return size
}
}

Moving the tab bar 1 point away from the bottom worked for me.
Of course you'll get a gap by doing so which you'll have to fill in some way, but the text/icons are now properly positioned.

Aha! It's actually magic!
I finally figured this out after hours of cursing Apple.
UIKit actually does handle this for you, and it appears that the shifted tab bar items are due to incorrect setup (and probably an actual UIKit bug). There is no need for subclassing or a background view.
UITabBar will "just work" if it is constrained to the superview's bottom, NOT to the bottom safe area.
It even works in Interface builder.
Correct Setup
In interface builder, viewing as iPhone X, drag a UITabBar out to where it snaps to the bottom safe area inset. When you drop it, it should look correct (fill the space all the way to the bottom edge).
You can then do an "Add Missing Constraints" and IB will add the correct constraints and your tab bar will magically work on all iPhones! (Note that the bottom constraint looks like it has a constant value equal to the height of the iPhone X unsafe area, but the constant is actually 0)
Sometimes it still doesn't work
What's really dumb is that you can actaully see the bug in IB as well, even if you add the exact constraints that IB adds in the steps above!
Drag out a UITabBar and don't snap it to the bottom safe area inset
Add leading, trailing and bottom constraints all to superview (not safe area)
Weirdly, this will fix itself if you do a "Reverse First And Second Item" in the constraint inspector for the bottom constraint. ¯_(ツ)_/¯

Solved for me by calling [tabController.view setNeedsLayout]; after dismissing the modal in completion block.
[vc dismissViewControllerAnimated:YES completion:^(){
UITabBarController* tabController = [UIApplication sharedApplication].delegate.window.rootViewController;
[tabController.view setNeedsLayout];
}];

The UITabBar is increasing in height to be above the home button/line, but drawing the subview in its original location and overlaying the UITabBarItem over the subview.
As a workaround you can detect the iPhone X and then shrink the height of the view by 32px to ensure the tab bar is displayed in the safe area above the home line.
For example, if you're creating your TabBar programatically replace
self.tabBarController = [[UITabBarController alloc] init];
self.window.rootViewController = self.tabBarController;
With this:
#define IS_IPHONEX (([[UIScreen mainScreen] bounds].size.height-812)?NO:YES)
self.tabBarController = [[UITabBarController alloc] init];
self.window.rootViewController = [[UIViewController alloc] init] ;
if(IS_IPHONEX)
self.window.rootViewController.view.frame = CGRectMake(self.window.rootViewController.view.frame.origin.x, self.window.rootViewController.view.frame.origin.y, self.window.rootViewController.view.frame.size.width, self.window.rootViewController.view.frame.size.height + 32) ;
[self.window.rootViewController.view addSubview:self.tabBarController.view];
self.tabBarController.tabBar.barTintColor = [UIColor colorWithWhite:0.98 alpha:1.0] ;
self.window.rootViewController.view.backgroundColor = [UIColor colorWithWhite:0.98 alpha:1.0] ;
NOTE: This could well be a bug, as the view sizes and tab bar layout are set by the OS. It should probably display as per Apple's screenshot in the iPhone X Human Interface Guidelines here: https://developer.apple.com/ios/human-interface-guidelines/overview/iphone-x/

My case was that I had set a custom UITabBar height in my UITabBarController, like this:
override func viewWillLayoutSubviews() {
var tabFrame = tabBar.frame
tabFrame.size.height = 60
tabFrame.origin.y = self.view.frame.size.height - 60
tabBar.frame = tabFrame
}
Removing this code was the solution for the TabBar to display correctly on iPhone X.

The simplest solution I found was to simply add a 0.2 pt space between the bottom of the tab bar and the bottom of the safeAreaLayoutGuide.bottomAnchor like so.
tabBar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -0.2)

I was having the same issue when I was trying to set the frame of UITabBar in my custom TabBarController.
self.tabBar.frame = CGRectMake(0, self.view.frame.size.height - kTabBarHeight, self.view.frame.size.width, kTabBarHeight);
When I just adjusted it to the new size the issue went away
if(IS_IPHONE_X){
self.tabBar.frame = CGRectMake(0, self.view.frame.size.height - kPhoneXTabBarHeight, self.view.frame.size.width, kPhoneXTabBarHeight);
}
else{
self.tabBar.frame = CGRectMake(0, self.view.frame.size.height - kTabBarHeight, self.view.frame.size.width, kTabBarHeight);
}

I encountered this today with a a UITabBar manually added to the view controller in the storyboard, when aligning to the bottom of the safe area with a constant of 0 I would get the issue but changing it to 1 would fix the problem. Having the UITabBar up 1 pixel more than normal was acceptable for my application.

I have scratched my head over this problem. It seems to be associated with how the tabBar is initialized and added to view hierarchy. I also tried above solutions like calling invalidateIntrinsicContentSize, setting the frame, and also bottomInsets inside a UITabBar subclass. They seem to work however temporarily and they break of some other scenario or regress the tab bar by causing some ambiguous layout issue. When I was debugging the issue I tried assigning the height constraints to the UITabBar and centerYAnchor, however neither fixed the problem. I realized in view debugger that the tabBar height was correct before and after the problem reproduced, which led me to think that the problem was in the subviews. I used the below code to successfully fix this problem without regressing any other scenario.
- (void) viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
if (DEVICE_IS_IPHONEX())
{
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
for (UIView *view in self.tabBar.subviews)
{
if ([NSStringFromClass(view.class) containsString:#"UITabBarButton"])
{
if (#available (iOS 11, *))
{
[view.bottomAnchor constraintEqualToAnchor:view.superview.safeAreaLayoutGuide.bottomAnchor].active = YES;
}
}
}
} completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
[self.tabBar layoutSubviews];
}];
}
}
Assumptions: I am doing this only for iPhone X, since it doesn't seem to reproduce on any other device at the moment.
Is based on the assumption that Apple doesn't change the name of the UITabBarButton class in future iOS releases.
We're doing this on UITabBarButton only when means if apple adds more internal subviews in to UITabBar we might need to modify the code to adjust for that.
Please lemme know if this works, will be open to suggestions and improvements!
It should be simple to create a swift equivalent for this.

From this tutorial:
https://github.com/eggswift/ESTabBarController
and after initialization of tab bar writing this line in appdelegate class
(self.tabBarController.tabBar as? ESTabBar)?.itemCustomPositioning = .fillIncludeSeparator
Solves my problem of tab bar.
Hope its solves your problem
Thanks

Select tabbar and set "Save Area Relative Margins" checkbox in Inspector Editor like this:

I had the similar problem, at first it was rendered correctly but after setting up badgeValue on one of the tabBarItem it broke the layout.
What it worked for me without subclassing UITabBar was this, on my already created UITabBarController subclass.
-(void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
NSLayoutYAxisAnchor *tabBarBottomAnchor = self.tabBar.bottomAnchor;
NSLayoutYAxisAnchor *tabBarSuperviewBottomAnchor = self.tabBar.superview.bottomAnchor;
[tabBarBottomAnchor constraintEqualToAnchor:tabBarSuperviewBottomAnchor].active = YES;
[self.view layoutIfNeeded];
}
I used tabBar superview to make sure that the constraints/anchors are on the same view hierarchy and avoid crashes.
Based on my understanding, since this seems to be a UIKit bug, we just need to rewrite/re-set the tab bar constraints so the auto layout engine can layout the tab bar again correctly.

If you have any height constraint for the Tab Bar try removing it .
Faced the same problem and removing this solved the issue.

I created new UITabBarController in my storyboard and pushed all view controllers to this new UITabBarConttoller. So, all work well in iPhone X simulator.

For iPhone you can do this, Subclass UITabBarController.
class MyTabBarController: UITabBarController {
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if #available(iOS 11, *) {
self.tabBar.heightAnchor.constraint(equalToConstant: 40).isActive = true
self.tabBar.invalidateIntrinsicContentSize()
}
}
}
Goto Storyboard and allow Use Safe Area Layout Guide and change class of UITabbarController to MyTabBarController
P.S This solution is not tested in case of universal application and iPad.

try to change splash screen with #3x size is (3726 × 6624)

For me, remove [self.tabBar setBackgroundImage:] work, maybe it's UIKit bug

For me this fixed all the issues:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let currentHeight = tabBar.frame.height
tabBar.frame = CGRect(x: 0, y: view.frame.size.height - currentHeight, width: view.frame.size.width, height: currentHeight)
}

I was using a UITabBarController in the storyboard and at first it was working alright for me, but after upgrading to newer Xcode version it started giving me issues related to the height of the tabBar.
For me, the fix was to delete the existing UITabBarController from storyboard and re-create by dragging it from the interface builder objects library.

For those who write whole UITabBarController programmatically, you can use UITabBarItem.appearance().titlePositionAdjustment to adjust the title position
So in this case that you want add a gap between Icon and Title use it in viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
// Specify amount to offset a position, positive for right or down, negative for left or up
let verticalUIOffset = UIOffset(horizontal: 0, vertical: hasTopNotch() ? 5 : 0)
UITabBarItem.appearance().titlePositionAdjustment = verticalUIOffset
}
detecting if device has Notch screen:
func hasTopNotch() -> Bool {
if #available(iOS 11.0, tvOS 11.0, *) {
return UIApplication.shared.delegate?.window??.safeAreaInsets.top ?? 0 > 20
}
return false
}

For me the solution was to select the Tab Bar in the view hierarchy, then go to:
Editor -> Resolve Auto Layout Issues, and under "Selected Views" (not "All views in view") choose "Add missing constraints".

I was having the same issue which was solved by setting the items of the tabBar after the tab bar was laid out.
In my case the issue happened when:
There is a custom view controller
A UITabBar is created in the initializer of the view controller
The tab bar items are set before view did load
In view did load the tab bar is added to the main view of the view controller
Then, the items are rendered as you mention.

I think this is a bug UIKit from iPhoneX.
because it works:
if (#available(iOS 11.0, *)) {
if ([UIApplication sharedApplication].keyWindow.safeAreaInsets.top > 0.0) {
self.tabBarBottomLayoutConstraint.constant = 1.0;
}
}

Related

How to hide NSCollectionView Scroll indicator

I have an NSCollectionView and I would like to hide the horizontal scroll indicators.
I've tried
collectionView.enclosingScrollView?.verticalScroller?.isHidden = true
But it is not working.
Thank you in advance.
hidden didn't work for me too.
The only way I found to hack this, is by changing inset:
(scrollViewCollectionView is of type NSScrollView, this example is while creating NSCollectionView programmatically)
scrollViewCollectionView.documentView?.enclosingScrollView?.scrollerInsets = NSEdgeInsets.init(top: 0, left: 0, bottom: 100, right: 0)
Please note: My NSCollectionView is horizontal, and less then 100 height, this is why this hack resolved in a hidden indicator.
Override hasHorizontalScroller or horizontalScroller to false and nil will cause NSScroller not to be displayed, and NSScrollView will not respond to scroll events.
This means that you can't scroll through NSScrollView's many scrolling methods.
When hasHorizontalScroller = true, horizontalScroller != nil will cause a drawing error.
class MyScrollView : NSScrollView {
// !!! Don't use this !!!
override var hasHorizontalScroller: Bool {
get {
// return false will cause NSScroller not to be displayed
// and NSScrollView will not respond to scroll events,
// this means that you can't scroll through NSScrollView's many scrolling methods.
false
}
set {
super.hasHorizontalScroller = newValue
}
}
// !!! Don't use this !!!
override var horizontalScroller: NSScroller? {
get {
// return nil will cause NSScroller not to be displayed,
// but it still occupies the drawing area of the parent view.
nil
}
set {
super.horizontalScroller = newValue
}
}
}
This is the way to hide NSScroller and respond to scroll events correctly. Only useful in versions above 10.7:
class HiddenScroller: NSScroller {
// #available(macOS 10.7, *)
// let NSScroller tell NSScrollView that its own width is 0, so that it will not really occupy the drawing area.
override class func scrollerWidth(for controlSize: ControlSize, scrollerStyle: Style) -> CGFloat {
0
}
}
Create an outlet for the ScrollView which contains the CollectionView as seen here. I've named mine #IBOutlet weak var collectionViewScrollView: NSScrollView!
in viewDidAppear() function add:
collectionViewScrollView.scrollerStyle = .legacy
collectionViewScrollView.verticalScroller?.isHidden = true - for vertical scroll
collectionViewScrollView.horizontalScroller?.isHidden = true - for horizontal scroll
For some reason, in my case it only works if I set the collectionViewScrollView.scrollerStyle to .legacy. Not sure why, but it works.
Setting "Show Vertical Scroller" or "Show Horizontal Scroller" in storyboard doesn't remove the scrollers without setting constrains (height and width) of Bordered Scroll View of Collection View. After I did that and unchecked "Show Vertical Scroller" and "Show Horizontal Scroller" in Attributes Panel in storyboard they disappeared.
I got same problem and just solve it. You can write your own custom NSScrollView and override 2 stored property: hasHorizontalScroller, horizontalScroller, and 1 function scrollWheel(with:). Here's my code:
class MyScrollView: NSScrollView {
override var hasHorizontalScroller: Bool {
get {
return false
}
set {
super.hasHorizontalScroller = newValue
}
}
override var horizontalScroller: NSScroller? {
get {
return nil
}
set {
super.horizontalScroller = newValue
}
}
//comment it or use super for scrroling
override func scrollWheel(with event: NSEvent) {}
}
And don't forget to set Border Scroll View class to MyScrollView in .xib or storyboard.
Enjoy it!
You can also achieve that via storyboard
I also meet the same problem. MCMatan is right. Please adjust the position of scroller to some place invisible.
scrollView.scrollerInsets = NSEdgeInsets.init(top: 0, left: 0, bottom: -10, right: 0)
for Swift 4 & 5 in UIKit:
for Horizontal:
collectionView.showsHorizontalScrollIndicator = false
For Vertical:
collectionView.showsVerticalScrollIndicator = false
In my case, the horizontal and vertical scroller of the collection scroll view are only hidden if do exactly as follow:
1. In Interface Builder.
1.a. Select Scroll View —> Attributes Inspector:
+ Uncheck Show Horizontal Scroller.
+ Uncheck Show Vertical Scroller.
+ Uncheck Automactically Hide Scroller.
1.b. Select Size Inspector:
+ Uncheck Automatically Adjust.
1.c. Select Clip View —> Size Inspector:
+ Uncheck Automatically Adjust.
2. In code do exactly as follow:
[self.scrollView setBackgroundColor:[NSColor clearColor]];
[self.scrollView setBorderType:NSNoBorder];
[self.scrollView setHasVerticalScroller:NO];
[self.scrollView setHasHorizontalScroller:NO];
[self.scrollView setAutomaticallyAdjustsContentInsets:NO];
[self.scrollView setContentInsets:NSEdgeInsetsMake(0.0, 0.0, -NSHeight([self.scrollView horizontalScroller].frame), -NSWidth([self.scrollView verticalScroller].frame))];
[self.scrollView setScrollerInsets:NSEdgeInsetsMake(0.0, 0.0, -NSHeight([self.scrollView horizontalScroller].frame), -NSWidth([self.scrollView verticalScroller].frame))];
[self.scrollView setScrollEnable:NO];
NSClipView *clipView = (NSClipView *)[self.scrollView documentView];
if ([clipView isKindOfClass:[NSClipView class]])
{
[clipView setAutomaticallyAdjustsContentInsets:NO];
[clipView setContentInsets:NSEdgeInsetsZero];
}
Then the NSCollectionView will fit to the Clip View as same width and height without the horizontal and vertical scrollers.
If someone still needs it, this one trick should work
collectionView.enclosingScrollView?.horizontalScroller?.alphaValue = 0.0

NSToolbar width Bug with title.hidden

Has anyone come across the NSToolbar width not being correct in relation to the window width when hiding the title visibility? It seems to be the toolbar isn't preserving the correct size after a quit and restart of the app.
I'm using this in my NSWindow Subclass:
self.window!.titleVisibility = NSWindowTitleVisibility.Hidden
When doing so after the restart of my app the far right hand toolbar items aren't hugging the edge of the window and i can see the toolbar isn't being redrawn to the full extent...
I had the same issue. I solved it by removing the toolbar and setting the same toolbar again using GCD (which will actually execute a little later).
Create a subclass of NSWindow and set this class in Interface Builder. Add this to your awakeFromNib:
-(void)awakeFromNib
{
self.titleVisibility = NSWindowTitleHidden;
NSToolbar* toolbar = self.toolbar;
self.toolbar = nil;
dispatch_async(dispatch_get_main_queue(), ^{
self.toolbar = toolbar;
});
}
I found setting the titleVisibility in windowDidLoad() fixed the problem.
override func windowDidLoad() {
super.windowDidLoad()
self.window!.titleVisibility = NSWindowTitleVisibility.Hidden
}

Is it possible to use AutoLayout with UITableView's tableHeaderView?

Since I discovered AutoLayout I use it everywhere, now I'm trying to use it with a tableHeaderView.
I made a subclass of UIView added everything (labels etc...) I wanted with their constraints, then I added this CustomView to the UITableView'tableHeaderView.
Everything works just fine except the UITableView always displays above the CustomView, by above I mean the CustomView is under the UITableView so it can't be seen !
It seems that no matter what I do, the height of the UITableView'tableHeaderView is always 0 (so is the width, x and y).
My question : is it possible at all to accomplish this without setting the frame manually ?
EDIT :
The CustomView'subview that I'm using has these constraints :
_title = [[UILabel alloc]init];
_title.text = #"Title";
[self addSubview:_title];
[_title keep:[KeepTopInset rules:#[[KeepEqual must:5]]]]; // title has to stay at least 5 away from the supperview Top
[_title keep:[KeepRightInset rules:#[[KeepMin must:5]]]];
[_title keep:[KeepLeftInset rules:#[[KeepMin must:5]]]];
[_title keep:[KeepBottomInset rules:#[[KeepMin must:5]]]];
I'm using a handy library 'KeepLayout' because writing constraints manually takes forever and way too many line for one single constraint but the methods are self-explaining.
And the UITableView has these constraints :
_tableView = [[UITableView alloc]init];
_tableView.translatesAutoresizingMaskIntoConstraints = NO;
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.backgroundColor = [UIColor clearColor];
[self.view addSubview:_tableView];
[_tableView keep:[KeepTopInset rules:#[[KeepEqual must:0]]]];// These 4 constraints make the UITableView stays 0 away from the superview top left right and bottom.
[_tableView keep:[KeepLeftInset rules:#[[KeepEqual must:0]]]];
[_tableView keep:[KeepRightInset rules:#[[KeepEqual must:0]]]];
[_tableView keep:[KeepBottomInset rules:#[[KeepEqual must:0]]]];
_detailsView = [[CustomView alloc]init];
_tableView.tableHeaderView = _detailsView;
I don't know if I have to set some constraints directly on the CustomView, I think the height of the CustomView is determined by the constraints on the UILabel "title" in it.
EDIT 2: After another investigation it seems the height and width of the CustomView are correctly calculated, but the top of the CustomView is still at the same level than the top of the UITableView and they move together when I scroll.
I asked and answered a similar question here. In summary, I add the header once and use it to find the required height. That height can then be applied to the header, and the header is set a second time to reflect the change.
- (void)viewDidLoad
{
[super viewDidLoad];
self.header = [[SCAMessageView alloc] init];
self.header.titleLabel.text = #"Warning";
self.header.subtitleLabel.text = #"This is a message with enough text to span multiple lines. This text is set at runtime and might be short or long.";
//set the tableHeaderView so that the required height can be determined
self.tableView.tableHeaderView = self.header;
[self.header setNeedsLayout];
[self.header layoutIfNeeded];
CGFloat height = [self.header systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
//update the header's frame and set it again
CGRect headerFrame = self.header.frame;
headerFrame.size.height = height;
self.header.frame = headerFrame;
self.tableView.tableHeaderView = self.header;
}
If you have multi-line labels, this also relies on the custom view setting the preferredMaxLayoutWidth of each label:
- (void)layoutSubviews
{
[super layoutSubviews];
self.titleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.titleLabel.frame);
self.subtitleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.subtitleLabel.frame);
}
or perhaps more generally:
override func layoutSubviews() {
super.layoutSubviews()
for view in subviews {
guard let label = view as? UILabel where label.numberOfLines == 0 else { continue }
label.preferredMaxLayoutWidth = CGRectGetWidth(label.frame)
}
}
Update January 2015
Unfortunately this still seems necessary. Here is a swift version of the layout process:
tableView.tableHeaderView = header
header.setNeedsLayout()
header.layoutIfNeeded()
header.frame.size = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
tableView.tableHeaderView = header
I've found it useful to move this into an extension on UITableView:
extension UITableView {
//set the tableHeaderView so that the required height can be determined, update the header's frame and set it again
func setAndLayoutTableHeaderView(header: UIView) {
self.tableHeaderView = header
self.tableHeaderView?.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
header.widthAnchor.constraint(equalTo: self.widthAnchor)
])
header.setNeedsLayout()
header.layoutIfNeeded()
header.frame.size = header.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
self.tableHeaderView = header
}
}
Usage:
let header = SCAMessageView()
header.titleLabel.text = "Warning"
header.subtitleLabel.text = "Warning message here."
tableView.setAndLayoutTableHeaderView(header)
I've been unable to add a header view using constraints (in code). If I give my view a width and/or a height constraint, I get a crash with the message saying:
"terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Auto Layout still required after executing -layoutSubviews. UITableView's implementation of -layoutSubviews needs to call super."
When I add a view in the storyboard to my table view, it shows no constraints, and it works fine as a header view, so I think that the placement of the header view isn't done using constraints. It doesn't seem to behave like a normal view in that regard.
The width is automatically the width of the table view, the only thing you need to set is the height -- the origin values are ignored, so it doesn't matter what you put in for those. For instance, this worked fine (as does 0,0,0,80 for the rect):
UIView *headerview = [[UIView alloc] initWithFrame:CGRectMake(1000,1000, 0, 80)];
headerview.backgroundColor = [UIColor yellowColor];
self.tableView.tableHeaderView = headerview;
I saw a lot of methods here doing so much unnecessary stuff, but you don't need that much to use auto layout in the header view. You just have to create you xib file, put your constraints and instantiate it like this:
func loadHeaderView () {
guard let headerView = Bundle.main.loadNibNamed("CourseSearchHeader", owner: self, options: nil)?[0] as? UIView else {
return
}
headerView.autoresizingMask = .flexibleWidth
headerView.translatesAutoresizingMaskIntoConstraints = true
tableView.tableHeaderView = headerView
}
Another solution is to dispatch the header view creation to the next main thread call:
- (void)viewDidLoad {
[super viewDidLoad];
// ....
dispatch_async(dispatch_get_main_queue(), ^{
_profileView = [[MyView alloc] initWithNib:#"MyView.xib"];
self.tableView.tableHeaderView = self.profileView;
});
}
Note: It fix the bug when the loaded view has a fixed height. I haven't tried when the header height only depends on its content.
EDIT :
You can find a cleaner solution to this problem by implementing this function, and calling it in viewDidLayoutSubviews
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[self sizeHeaderToFit];
}
You can get autolayout to provide you with a size by using the systemLayoutSizeFittingSize method.
You can then use this to create the frame for your application. This technique works whenever you need to know the size of a view that uses autolayout internally.
The code in swift looks like
//Create the view
let tableHeaderView = CustomTableHeaderView()
//Set the content
tableHeaderView.textLabel.text = #"Hello world"
//Ask auto layout for the smallest size that fits my constraints
let size = tableHeaderView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
//Create a frame
tableHeaderView.frame = CGRect(origin: CGPoint.zeroPoint, size: size)
//Set the view as the header
self.tableView.tableHeaderView = self.tableHeaderView
Or in Objective-C
//Create the view
CustomTableHeaderView *header = [[CustomTableHeaderView alloc] initWithFrame:CGRectZero];
//Set the content
header.textLabel.text = #"Hello world";
//Ask auto layout for the smallest size that fits my constraints
CGSize size = [header systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
//Create a frame
header.frame = CGRectMake(0,0,size.width,size.height);
//Set the view as the header
self.tableView.tableHeaderView = header
It should also be noted that in this particular instance, overriding requiresConstraintBasedLayout in your subclass, does result in a layout pass being performed, however the results of this layout pass are ignored and the system frame set to the width of the tableView and 0 height.
Code:
extension UITableView {
func sizeHeaderToFit(preferredWidth: CGFloat) {
guard let headerView = self.tableHeaderView else {
return
}
headerView.translatesAutoresizingMaskIntoConstraints = false
let layout = NSLayoutConstraint(
item: headerView,
attribute: .Width,
relatedBy: .Equal,
toItem: nil,
attribute:
.NotAnAttribute,
multiplier: 1,
constant: preferredWidth)
headerView.addConstraint(layout)
let height = headerView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
headerView.frame = CGRectMake(0, 0, preferredWidth, height)
headerView.removeConstraint(layout)
headerView.translatesAutoresizingMaskIntoConstraints = true
self.tableHeaderView = headerView
}
}
Extended this solution http://collindonnell.com/2015/09/29/dynamically-sized-table-view-header-or-footer-using-auto-layout/ for table footer view:
#interface AutolayoutTableView : UITableView
#end
#implementation AutolayoutTableView
- (void)layoutSubviews {
[super layoutSubviews];
// Dynamic sizing for the header view
if (self.tableHeaderView) {
CGFloat height = [self.tableHeaderView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
CGRect headerFrame = self.tableHeaderView.frame;
// If we don't have this check, viewDidLayoutSubviews() will get
// repeatedly, causing the app to hang.
if (height != headerFrame.size.height) {
headerFrame.size.height = height;
self.tableHeaderView.frame = headerFrame;
self.tableHeaderView = self.tableHeaderView;
}
[self.tableHeaderView layoutIfNeeded];
}
// Dynamic sizing for the footer view
if (self.tableFooterView) {
CGFloat height = [self.tableFooterView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
CGRect footerFrame = self.tableFooterView.frame;
// If we don't have this check, viewDidLayoutSubviews() will get
// repeatedly, causing the app to hang.
if (height != footerFrame.size.height) {
footerFrame.size.height = height;
self.tableFooterView.frame = footerFrame;
self.tableFooterView = self.tableFooterView;
}
self.tableFooterView.transform = CGAffineTransformMakeTranslation(0, self.contentSize.height - footerFrame.size.height);
[self.tableFooterView layoutIfNeeded];
}
}
#end
Updated for Swift 4.2
extension UITableView {
var autolayoutTableViewHeader: UIView? {
set {
self.tableHeaderView = newValue
guard let header = newValue else { return }
header.setNeedsLayout()
header.layoutIfNeeded()
header.frame.size =
header.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
self.tableHeaderView = header
}
get {
return self.tableHeaderView
}
}
}
The following worked for me.
Use a plain old UIView as the header view.
Add subviews to that UIView
Use autolayout on the subviews
The main benefit I see is limiting frame calculations. Apple should really update UITableView's API to make this easier.
Example using SnapKit:
let layoutView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 60))
layoutView.backgroundColor = tableView.backgroundColor
tableView.tableHeaderView = layoutView
let label = UILabel()
layoutView.addSubview(label)
label.text = "I'm the view you really care about"
label.snp_makeConstraints { make in
make.edges.equalTo(EdgeInsets(top: 10, left: 15, bottom: -5, right: -15))
}
Strange things happens. systemLayoutSizeFittingSize works great for iOS9, but doesn't for iOS 8 in my case. So this problems solves quite easy. Just get link to the bottom view in header and in viewDidLayoutSubviews after super call update header view bounds by inserting height as CGRectGetMaxY(yourview.frame) + padding
UPD: The easiest solution ever:
So, in header view place subview and pin it to left, right, top. In that subview place your subviews with auto-height constraints. After that give all the job to the autolayout (no calculation required)
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
CGFloat height = CGRectGetMaxY(self.tableView.tableHeaderView.subviews.firstObject.frame);
self.tableView.tableHeaderView.bounds = CGRectMake(0, 0, CGRectGetWidth(self.tableView.bounds), height);
self.tableView.tableHeaderView = self.tableView.tableHeaderView;
}
As a result subview is expanding/shrinking like it should, at the end it calls viewDidLayoutSubviews. At the time we know the actual size of the view, so set headerView height and update it by re-assigning.
Works like a charm!
Also works for footer view.
you can add top + horizontal location constraint between header and tableview, to place it, correctly (if the header itself contains all the necessary internal layout constraints to have a correct frame)
in the tableViewController viewDidLoad method
headerView.translatesAutoresizingMaskIntoConstraints = false
tableView.tableHeaderView = headerView
headerView.widthAnchor.constraint(equalTo: tableView.widthAnchor).isActive = true
headerView.topAnchor.constraint(equalTo: tableView.topAnchor).isActive = true
headerView.centerXAnchor.constraint(equalTo: tableView.centerXAnchor).isActive = true
For most cases the best solution is simply not to fight the framework and embrace autoresizing masks:
// embrace autoresizing masks and let the framework add the constraints for you
headerView.translatesAutoresizingMaskIntoConstraints = true
headerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
// figure out what's the best size based on the table view width
let width = self.tableView.frame.width
let targetSize = headerView.systemLayoutSizeFitting(CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
headerView.frame.size = targetSize
self.tableView.tableHeaderView = headerView
By using autoresizing masks you're telling the framework how your view should change its size when the superview changes its size. But this change is based on the initial frame you've set.
My table header view is a UIView subclass - I created a single contentView UIView within the initializer, with its bounds the same as the table header view's frame and added all my objects as a subview of that.
Then add the constraints for your objects within the table header view's layoutSubviews method rather than within the initializer. That solved the crash.
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:CGRectMake(0, 0, 0, 44.0)];
if (self) {
UIView *contentView = [[UIView alloc] initWithFrame:self.bounds];
contentView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
// add other objects as subviews of content view
}
return self;
}
- (void)layoutSubviews
{
[super layoutSubviews];
// remake constraints here
}
My AutoLayout is working very good:
CGSize headerSize = [headerView systemLayoutSizeFittingSize:CGSizeMake(CGRectGetWidth([UIScreen mainScreen].bounds), 0) withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
headerView.frame = CGRectMake(0, 0, headerSize.width, headerSize.height);
self.tableView.tableHeaderView = headerView;
My solution is making a new class like this.
class BaseTableHeaderView: UIView {
func sizeToFitBasedOnConstraints(width: CGFloat = Screen.width) {
let size = systemLayoutSizeFitting(CGSize(width: width, height: 10000),
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel)
frame = CGRect(origin: .zero, size: size)
}
override func willMove(toSuperview newSuperview: UIView?) {
sizeToFitBasedOnConstraints()
super.willMove(toSuperview: newSuperview)
}
}
To use it, simply add all your subviews onto an instance of BaseTableHeaderView and attach it to your table view.
let tableHeaderView = BaseTableHeaderView()
tableHeaderView.addSubview(...)
tableView.tableHeaderView = tableHeaderView
It will automatically resize based on its constraints.
I know this is an old post but After going through all the SO posts regarding this and passing a whole afternoon playing with this, I finally came up with a clean and yet very simple solution
First of all, My view hierarchy looks like this:
Table View
View tableHeaderView
View with an outlet called headerView
Now inside the View (No.3), I set up all the constraints as I would normally including the bottom space to container. This will make the container (i.e. 3.View i.e. headerView) to size itself based on it's subviews and their constraints.
After that, I set the constraints between 3. View and 2. View to these:
Top Space to container: 0
Leading Space to container: 0
Trailing Space to container: 0
Notice that I omit intentionally the bottom space intentionally.
Once all of this is done in the storyboard, everything that's left to do is paste those three lines of codes:
if (self.headerView.frame.size.height != self.tableView.tableHeaderView.frame.size.height) {
UIView *header = self.tableView.tableHeaderView;
CGRect frame = self.tableView.tableHeaderView.frame;
frame.size.height = self.headerView.frame.size.height + frame.origin.y;
header.frame = frame;
self.tableView.tableHeaderView = header;
}
Tips: If you use method setAndLayoutTableHeaderView, you should update subviews's frame,so in this situation UILabel's preferredMaxLayoutWidth should call before systemLayoutSizeFittingSize called, do not call in layoutSubview.
code show
Share my approach.
UITableView+XXXAdditions.m
- (void)xxx_setTableHeaderView:(UIView *)tableHeaderView layoutBlock:(void(^)(__kindof UIView *tableHeaderView, CGFloat *containerViewHeight))layoutBlock {
CGFloat containerViewHeight = 0;
UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectZero];
[backgroundView addSubview:tableHeaderView];
layoutBlock(tableHeaderView, &containerViewHeight);
backgroundView.frame = CGRectMake(0, 0, 0, containerViewHeight);
self.tableHeaderView = backgroundView;
}
Usage.
[self.tableView xxx_setTableHeaderView:myView layoutBlock:^(__kindof UIView * _Nonnull tableHeaderView, CGFloat *containerViewHeight) {
*containerViewHeight = 170;
[tableHeaderView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(#20);
make.centerX.equalTo(#0);
make.size.mas_equalTo(CGSizeMake(130, 130));
}];
}];
An old post. But a good post. Here's my 2 cents.
Firstly, ensure that your header view has its constraints arranged so that it can support it's own intrinsic content size. Then do the following.
//ViewDidLoad
headerView.translatesAutoresizingMaskIntoConstraints = false
headerView.configure(title: "Some Text A")
//Somewhere else
headerView.update(title: "Some Text B)
private var widthConstrained = false
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if widthConstrained == false {
widthConstrained = true
tableView.addConstraint(NSLayoutConstraint(item: headerView, attribute: .width, relatedBy: .equal, toItem: tableView, attribute: .width, multiplier: 1, constant: 0))
headerView.layoutIfNeeded()
tableView.layoutIfNeeded()
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { (context) in
self.headerView.layoutIfNeeded()
self.tableView.layoutIfNeeded()
}, completion: nil)
}
I was able to achieve it by the following approach (this works for footer the same way).
First, you will need small UITableView extension:
Swift 3
extension UITableView {
fileprivate func adjustHeaderHeight() {
if let header = self.tableHeaderView {
adjustFrame(header)
}
}
private func adjustFrame(_ view: UIView) {
view.frame.size.height = calculatedViewHeight(view)
}
fileprivate func calculatedHeightForHeader() -> CGFloat {
if let header = self.tableHeaderView {
return calculatedViewHeight(header)
}
return 0.0
}
private func calculatedViewHeight(_ view: UIView) -> CGFloat {
view.setNeedsLayout()
let height = view.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
return height
}
}
In your view controller class implementation:
// this is a UIView subclass with autolayout
private var headerView = MyHeaderView()
override func loadView() {
super.loadView()
// ...
self.tableView.tableHeaderView = headerView
self.tableView.sectionHeaderHeight = UITableViewAutomaticDimension
// ...
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
// this is to prevent recursive layout calls
let requiredHeaderHeight = self.tableView.calculatedHeightForHeader()
if self.headerView.frame.height != requiredHeaderHeight {
self.tableView.adjustHeaderHeight()
}
}
Notes about a header UIView's subview implementation:
You have to 100% sure that your header view has the correct autolayout setup. I would recommend to start with simple header view with just one heigh constraint and try out the setup above.
Override requiresConstraintBasedLayout and return true:
.
class MyHeaderView: UIView {
// ...
override static var requiresConstraintBasedLayout : Bool {
return true
}
// ...
}
For Xamarin users:
public override void ViewDidLayoutSubviews()
{
base.ViewDidLayoutSubviews();
TableviewHeader.SetNeedsLayout();
TableviewHeader.LayoutIfNeeded();
var height = TableviewHeader.SystemLayoutSizeFittingSize(UIView.UILayoutFittingCompressedSize).Height;
var frame = TableviewHeader.Frame;
frame.Height = height;
TableviewHeader.Frame = frame;
}
Assuming you named the header view of your tableview as TableviewHeader
Here is how you can do in your UIViewController
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if headerView.frame.size.height == 0 {
headerView.label.preferredMaxLayoutWidth = view.bounds.size.width - 20
let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
headerView.frame.size = CGSize(width: tableView.bounds.size.width, height: height)
}
}
Any constraint-based UIView can be a good tableHeaderView.
One needs to set a tableFooterView before and then impose additional trailing constraint on tableFooterView and tableHeaderView.
- (void)viewDidLoad {
........................
// let self.headerView is some constraint-based UIView
self.tableView.tableFooterView = [UIView new];
[self.headerView layoutIfNeeded];
self.tableView.tableHeaderView = self.headerView;
[self.tableView.leadingAnchor constraintEqualToAnchor:self.headerView.leadingAnchor].active = YES;
[self.tableView.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor].active = YES;
[self.tableView.topAnchor constraintEqualToAnchor:self.headerView.topAnchor].active = YES;
[self.tableFooterView.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor].active = YES;
}
One can find all details and code snippets here
I have figured out a workaround. wrap your autolayout wrriten xib header view in an empty uiview wrapper, and assign the header view to tableView's tableViewHeader property.
UIView *headerWrapper = [[UIView alloc] init];
AXLHomeDriverHeaderView *headerView = [AXLHomeDriverHeaderView loadViewFromNib];
[headerWrapper addSubview:headerView];
[headerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(headerWrapper);
}];
self.tableView.tableHeaderView = headerView;
Here's what works for UITableViewController in ios 12,
Drap a UIView into the TableView above all the prototype cells for header and below all the prototype cells for footer. Setup your header and footer as needed. Set all the required constraints.
Now use the following extension methods
public static class UITableVIewExtensions
{
public static void MakeHeaderAutoDimension(this UITableView tableView)
{
if (tableView.TableHeaderView is UIView headerView) {
var size = headerView.SystemLayoutSizeFittingSize(UIView.UILayoutFittingCompressedSize);
if (headerView.Frame.Size.Height != size.Height) {
var frame = headerView.Frame;
frame.Height = size.Height;
headerView.Frame = frame;
tableView.TableHeaderView = headerView;
tableView.LayoutIfNeeded();
}
}
}
public static void MakeFooterAutoDimension(this UITableView tableView)
{
if (tableView.TableFooterView is UIView footerView) {
var size = footerView.SystemLayoutSizeFittingSize(UIView.UILayoutFittingCompressedSize);
if (footerView.Frame.Size.Height != size.Height) {
var frame = footerView.Frame;
frame.Height = size.Height;
footerView.Frame = frame;
tableView.TableFooterView = footerView;
tableView.LayoutIfNeeded();
}
}
}
}
and call it in ViewDidLayoutSubviews of the subclass of UITableViewController
public override void ViewDidLayoutSubviews()
{
base.ViewDidLayoutSubviews();
TableView.MakeHeaderAutoDimension();
TableView.MakeFooterAutoDimension();
}
I encountered the problem of getting width 375pt, the only way that worked for me is to relayout the tableView to get the correct width. I also preferred AutoLayout over setting Frame size.
Here's the version that works for me:
Xamarin.iOS
public static void AutoLayoutTableHeaderView(this UITableView tableView, UIView header)
{
tableView.TableHeaderView = header;
tableView.SetNeedsLayout();
tableView.LayoutIfNeeded();
header.WidthAnchor.ConstraintEqualTo(tableView.Bounds.Width).Active = true;
tableView.TableHeaderView = header;
}
Swift Version (modified from #Ben Packard answer)
extension UITableView {
//set the tableHeaderView so that the required height can be determined, update the header's frame and set it again
func setAndLayoutTableHeaderView(header: UIView) {
self.tableHeaderView = header
self.setNeedsLayout()
self.layoutIfNeeded()
header.widthAnchor.widthAnchor.constraint(equalTo: self.bounds.width).isActive = true
self.tableHeaderView = header
}
}
I created a subclass of UITableView and used UIStackView for both header and footer, it also enables setting more than one view.
https://github.com/omaralbeik/StackableTableView
After checking other solutions that used to work but no longer work I created a following solution for header view with multiline labels that works fine for me on iOS 12, iOS 13, iOS 14, iOS 15 in all cases also with device rotations. Instead of setting your view directly as a table header view use a wrapper view that automatically updates it heights if the real view changes the height. Add following class to your project:
class TableViewHeaderHelperView: UIView {
private weak var headerView: UIView?
private weak var tableView: UITableView?
private var lastUpdatedHeight: CGFloat = 0
init(headerView: UIView) {
self.headerView = headerView
super.init(frame: .zero)
translatesAutoresizingMaskIntoConstraints = true
addSubview(headerView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func addToTableView(_ tableView: UITableView) {
self.tableView = tableView
tableView.tableHeaderView = self
headerView?.widthAnchor.constraint(equalTo: tableView.widthAnchor).isActive = true
}
func removeFromTableView() {
self.tableView?.tableHeaderView = nil
self.tableView = nil
}
override func layoutSubviews() {
super.layoutSubviews()
refreshHeaderHeightIfNeeded()
}
func refreshHeaderHeightIfNeeded() {
DispatchQueue.main.async {
let headerViewSize = self.headerView?.bounds.size ?? .zero
if headerViewSize.height != self.lastUpdatedHeight {
self.lastUpdatedHeight = headerViewSize.height
self.frame.size = headerViewSize
self.tableView?.tableHeaderView = self
}
}
}
}
Then you can use this helper class as below:
let headerWrapperView = TableViewHeaderHelperView(headerView: yourRealHeaderView)
headerWrapperView.addToTableView(tableView)
Alternatively also I used a similar approach in my library LSCategories that provides various extensions including an extension for UITableView class which allows to set an autolayout view in a table header view or footer view with a single line of code so you can also try that instead.
Accepted answer is a only useful for tables with a single section. For multi-section UITableView just make sure that your header inherits from UITableViewHeaderFooterView and you will be fine.
As an alternative, just embed your current header in the contentView of a UITableViewHeaderFooterView. Exactly like UITableViewCell works.

Limit the scroll for UITableView

I have a TableViewController:
As you see I have my own custom bar at the top.
The UITable View is just a static one, and I add a view at the top of UITableView.
The thing is when I scroll the TableView to top-side it become like bellow image, and I don't want it. is there any easy code that I can limit the scroll for the tableView?
since UITableView is a subclass of UIScrollView you can use this UIScrollViewDelegate method to forbid scrolling above the top border
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == self.tableView) {
if (scrollView.contentOffset.y < 0) {
scrollView.contentOffset = CGPointZero;
}
}
}
Yo will need to set the bounce property of the uitableview to NO
UITableView *tableView;
tableView.bounces = NO;
Edit: Note also you can uncheck the bounces from interface builder too
Please check this answer for further details Disable UITableView vertical bounces when scrolling
I had the same problem and asked our UX-Designer, how it would be better to do. He said, that both strict solutions (prevent bouncing or allow it as it is) are bad. It's better to allow bouncing but only for some space
My solution was:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == self.tableView {
if scrollView.contentOffset.y < -64 {
scrollView.scrollRectToVisible(CGRect(origin: CGPoint(x: 0, y: -64), size: scrollView.frame.size), animated: false)
scrollView.scrollRectToVisible(CGRect(origin: CGPoint.zero, size: scrollView.frame.size), animated: true)
}
}
}
Where 64 was that "some space" for me. Code stops tableView at -64 from the top and brings it up with an animation.
Good luck!
If i understand correctly you have set-up your custom bar as part of your tableview. Put your custom bar in a separate view not in the tableview and put your tableview below custom bar when you are setting up your views. You need to create your custom view controller that will have your custom bar and your static table view.
You need to create your view controller object as type UIViewController and not UITableViewController. Then add the custom bar as a subview to self.view. Create a separate UITableView and add it below the custom bar. That should make custom bar static and table view scrollable.
Update:
In order to make the tableview static you need to set it as
tableView.scrollEnabled = NO:
Let me know if this works for you.
Swift version of Mattias's answer:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (scrollView == self.ordersTable) {
if (scrollView.contentOffset.y < 0) {
scrollView.contentOffset = CGPoint.zero;
}
}
}

check if UIView is in UIScrollView visible state

What is the easiest and most elegant way to check if a UIView is visible on the current UIScrollView's contentView? There are two ways to do this, one is involving the contentOffset.y position of the UIScrollView and the other way is to convert the rect area?
If you're trying to work out if a view has been scrolled on screen, try this:
CGRect thePosition = myView.frame;
CGRect container = CGRectMake(scrollView.contentOffset.x, scrollView.contentOffset.y, scrollView.frame.size.width, scrollView.frame.size.height);
if(CGRectIntersectsRect(thePosition, container))
{
// This view has been scrolled on screen
}
Swift 5: in case that you want to trigger an event that checks that the entire UIView is visible in the scroll view:
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.bounds.contains(targetView.frame) {
// entire UIView is visible in scroll view
}
}
}
Implement scrollViewDidScroll: in your scroll view delegate and calculate manually which views are visible (e.g. by checking if CGRectIntersectsRect(scrollView.bounds, subview.frame) returns true.
updated for swift 3
var rect1: CGRect!
// initialize rect1 to the relevant subview
if rect1.frame.intersects(CGRect(origin: scrollView.contentOffset, size: scrollView.frame.size)) {
// the view is visible
}
I think your ideas are correct. if it was me i would do it as following:
//scrollView is the main scroll view
//mainview is scrollview.superview
//view is the view inside the scroll view
CGRect viewRect = view.frame;
CGRect mainRect = mainView.frame;
if(CGRectIntersectsRect(mainRect, viewRect))
{
//view is visible
}
José's solution didn't quite work for me, it was detecting my view before it came on screen. The following intersects code works perfect in my tableview if José's simpler solution doesn't work for you.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let viewFrame = scrollView.convert(targetView.bounds, from: targetView)
if viewFrame.intersects(scrollView.bounds) {
// targetView is visible
}
else {
// targetView is not visible
}
}
Solution that takes into account insets
public extension UIScrollView {
/// Returns `adjustedContentInset` on iOS >= 11 and `contentInset` on iOS < 11.
var fullContentInsets: UIEdgeInsets {
if #available(iOS 11.0, *) {
return adjustedContentInset
} else {
return contentInset
}
}
/// Visible content frame. Equal to bounds without insets.
var visibleContentFrame: CGRect {
bounds.inset(by: fullContentInsets)
}
}
if scrollView.visibleContentFrame.contains(view) {
// View is fully visible even if there are overlaying views
}