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
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.
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;
}
}
}
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
}