I have a UINavigationController in my app. The UINavigationBar is set to opaque and all the scroll views do not overlap underneath the bar.
In one view I have a UITableView. The frame of the UITableView is (0 0; 320 504) on my iPhone 5. i.e. the height is 568 - 64 (the height of the nav bar and status bar).
The contentInset of the UITableView is (0, 0, 0, 0). When the table view first loads the contentOffset is (0, 0).
This is fine. Works brilliantly.
I added a UIRefreshControl to the table view. This works a couple of times but then after a few times of doing pull to refresh then the content at the top gets "stuck" under the nav bar.
When this happens I inspect the contentInset and it is now (-60, 0, 0, 0).
Is there any way to stop the UIRefreshControl from changing the contentInset?
This is probably the reason why UIRefreshControl is currently only supported on UITableViewController, rather than by addition to any scrollview (which you can get away with, in many cases).
The refresh control does its magic by tinkering with the content insets of the scrollview - particularly when it ends refreshing. Unfortunately the view controller is also tinkering with the content insets of the scroll view to fit it under the translucent nav and status bars. Fun ensues. Is this also an issue on iOS 6 (or, "good old iOS6" as I called it when dealing with the same issue).
The quickest solution is probably to add your table view as a child UITableViewController instead of a simple subview. I think that UITableViewController manages the insets for you at the end of the refresh. If that doesn't work, I've got workarounds for this but it will have to wait until I get back in the office.
I will add this answer here in case any one has problems with UIRefreshControl by changing the control properties (attributed title, tint, etc...):
Don't mess with the UIRefreshControl on -viewDidLoad:, use -viewDidAppear: instead.
Reset your table view contentInset.
-(void)pullToRefresh
{
[self.tableView reloadData];
[self.refreshControl endRefreshing];
[self.tableView setContentInset:UIEdgeInsetsMake(0, 0, 0, 0)];
}
You need override setContentInset: in you UICollectionView
- (void)setContentInset:(UIEdgeInsets)contentInset {
if (self.tracking) {
CGFloat difference = contentInset.top - self.contentInset.top;
CGPoint translation = [self.panGestureRecognizer translationInView:self];
translation.y -= difference * 3.0 / 2.0;
[self.panGestureRecognizer setTranslation:translation inView:self];
}
[super setContentInset:contentInset];
}
Related
When using prefersLargeTitles for a UINavigationController's UINavigationBar in iOS 11, the nav bar increases height. The increase is from 44 to 96 on the iPhones I have checked, but I think those numbers can change per device (or at least we need to code as if they can).
I want to programmatically find the 'extra' height - the height of the large titles area that is added beneath the traditional UINavigationBar when a large title is displayed. I can easily find the entire height of the bar with the large title displayed, but is there a way to programmatically find the height of the large title portion of the bar alone (without any hardcoding)?
The reason I need this is that there are times that I want to programmatically scroll to the top of a UITableView, pulling down the large title (which has scrolled up under the "regular height" nav bar) so that it is showing, and the content offset I need is the extra height of the nav bar. I could use the total height of the navigation bar, but this would pull the UITableView down too far. To do this now, I need to hardcode as below:
[self.tableView setContentOffset:CGPointMake(0, -52) animated:NO];
I think you won't find a way to clearly resolve your problem. What you are asking for is a part of a navigation bar internals which is not exposed to the user. However, I think I can offer you a workaround.
To understand this let's take a look at the navigation bar (with large titles enabled) in the view debugger. As you can see on the below image there is an extra view containing the large title for a particular view controller. This view is not present when you don't support large titles.
Basically, what you want to know is the height of that view.
This is my proposed solution/workaround
extension UINavigationBar
{
var largeTitleHeight: CGFloat {
let maxSize = self.subviews
.filter { $0.frame.origin.y > 0 }
.max { $0.frame.origin.y < $1.frame.origin.y }
.map { $0.frame.size }
return maxSize?.height ?? 0
}
}
What it does
Filters out subviews which start on the top of the navigation bar. They are most probably a background or the navigation area, we don't need them.
Finds the lowest subview in the in the navigation bar.
Maps the resulted subview frame to the frame size.
Returns the height of the found subview or 0 if none was found.
The obvious drawback of this approach is that it's tightly coupled to the structure of the navigation bar which may change in the future. However, it's unlikely that you will get something more than one or another dirty trick.
The result of the execution should look like follows.
print("Extra height: \(navigationController!.navigationBar.lagreTitleHeight)")
Extra height: 52.0
Seems like calling navigationBar.sizeToFit() forces the navigationBar to adjust its size as it shows large title. Therefore you can calculate top safe area easily. The solution that we came up with is next:
self.navigationController?.navigationBar.sizeToFit()
let navigationBarOffset = navigationController?.navigationBar.frame.origin.y ?? 0
let navigationBarHeight = navigationController?.navigationBar.frame.height ?? 0
let offset = -navigationBarOffset - navigationBarHeight
if collectionView.contentOffset.y > offset {
let contentOffset = CGPoint(x: 0, y: offset)
collectionView.setContentOffset(contentOffset, animated: true)
}
As I understand it, you have your tableView origin under the navigationBar, at y = 0 in your UIViewController's view.
Your tableView should have its top bound fix to the top layout guide, or use the new safe area. That way you won't have to programmatically calculate what's the size of the navigationBar.
If you never used it, take a look at auto-layout.
You can simply scroll to the top of the table view without knowing the size of the navigation bar. UITableView comes with an API for that.
Option 1
// Get the first index path. You might want to check here whether data source item exists.
NSIndexPath *firstIndexPath = [NSIndexPath indexPathForRow:0
inSection:0];
// Scroll to it. You can play with scroll position to get what you want
[tableView scrollToRowAtIndexPath:firstIndexPath
atScrollPosition:UITableViewScrollPositionTop
animated:YES];
Option 2
CGPoint offset = CGPointMake(0.0, -tableView.contentInset.top);
[tableView setContentOffset:offset animated:YES];
I just experienced this same issue.
-(void)layoutSubviews{
[super layoutSubviews];
CGRect rectStatusBar = [[UIApplication sharedApplication] statusBarFrame];
if (rectStatusBar.size.height==44.f) {
//statusBar shows
}else{
if (#available(iOS 11.0, *)) {
for ( UIView*view in self.subviews) {
if ([NSStringFromClass(view.classForCoder) isEqualToString:#"_UINavigationBarContentView"]) {
view.frame = CGRectMake( 0,20,view.frame.size.width,44);
}
else if ([NSStringFromClass(view.classForCoder) isEqualToString:#"_UIBarBackground"]) {
view.frame = CGRectMake(0,0,view.frame.size.width, 64);
}
}
}
}
}
I have a UIScrollView which contains a UIView and a UITableView. My goal is to adjust the height of the UIScrollView to allow me to scroll the contents of the UIScrollView to a specific point.
Here is my view: It has a UIView up top and a UITableView down below.
When I scroll, I want the UIView to stop at a specific point like so:
The tableView would be able to continue scrolling, but the UIView would be locked in place until the user scrolled up and brought the UIView back to its original state.
A prime example of what I am trying to do is the AppStore.app on iOS 6. When you view the details of the app, the filter bar for Details, Reviews and Related moves to the top of the screen and stops. I hope this all made sense.
Thanks
I ended up going with a simpler approach. can't believe I didn't see this before. I created two views, one for the UITableView's tableHeaderView and one for the viewForHeaderInSection. The view I wanted to remain visible at all times is placed in the viewForHeaderInSection method and the other view is placed in the tableHeaderView property. This is a much simpler approach, I think than using a scrollview. The only issue I have run into with this approach is all my UIView animations in these two views no longer animate.
Here is my code.
[self.tableView setTableHeaderView:self.headerView];
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
return self.tableViewHeader;
}
add yourself as a UIScrollViewDelegate to the UITableView and implement the - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView so that if your views are in their starter positions they do this:
- your UITableView animates its size to the second state:
[UIView animateWithDuration:.1f animations:^{
CGRect theFrame = myView.frame;
theFrame.size.height += floatOfIncreasedHeight;
myView.frame = theFrame;
}];
- your UIView animates its vertical movement
[UIView animateWithDuration:3 delay:0 options:UIViewAnimationOptionCurveLinear animations:^(void){
view.center = CGPointMake(view.center.x , view.center.y + floatOfVerticalMovement);
}completion:^(BOOL Finished){
view.center = CGPointMake(view.center.x , view.center.y - floatOfVerticalMovement);]
Finally always in the delegate implement – scrollViewDidScrollToTop: so that you know can animate back to the initial state (using the same techniques reversed).
UPDATE:
since your views are inside a scroll view, there is a simpler way if you are ok with the table view being partly out of bounds in your starter position (i.e. instead of changing size it just scrolls into view):
make the scroll view frame size as big as your final tableview + your initial (entire) view and place it at 0,0 (so its final part will be hidden outside of the screen)
scrollview.frame = CGRectMake(0,0,tableview.frame.size.width,tableview.frame.size.height + view.frame.size.height);
you make the container scrollview contents as big as the entire table view + the entire view + the amount of the view that you want out of the way when scrolling the table view.
scrollview.contentSize = CGSizeMake(scrollview.frame.size.width, tableview.frame.size.height + view.frame.size.height + floatOfViewHeightIWantOutOfTheWay);
you place the view one after the other in the scrollview leaving all the additional empty space after the table view
view.frame = CGRectMake(0,0,view.frame.size.width, view.frame.size.height);
tableview.frame = CGRectMake(0,view.frame.size.height, tableview.frame.size.width, tableview.frame.size.height);
now it should just work because since iOS 3 nested scrolling is supported
You can easily achieve this by setting the content size of the scrollView correctly and keep the height of the UITableView smaller than your viewcontroller's height, so that it fits the bottom part of the top UIView and the UITableView...
Another scenario is to split the top View in 2 parts.
The part that will scroll away and the part that will be visible.
Then set the part that will scroll away as the entire UITableView header and the part that will remain visible as the header view for the first table section.
So then you can achieve this with a single UITableView, without having to use a UIScrollView
What you're looking for is something like what Game Center happens to do with it's header which can actually be modelled with a table header, a custom section header view, and some very clever calculations that never actually involve messing with the frame and bounds of the table.
First, the easy part: faking a sticky view. That "view that's always present when scrolling the table" implemented as a section header. By making the number of sections in the table 1, and implementing -headerViewForSection:, it's possible to seamlessly make the view scroll with the tableview all for free (API-wise that is):
- (UITableViewHeaderFooterView *)headerViewForSection:(NSInteger)section {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0,0,320,50)];
label.text = #"Info that was always present when scrolling the UITableView";
label.textAlignment = UITextAlignmentCenter;
label.backgroundColor = [UIColor colorWithRed:0.243 green:0.250 blue:0.253 alpha:1.000];
label.textColor = UIColor.whiteColor;
return label;
}
Finally, the hard part: KVO. When the table scrolls, we have to keep the header up there sticky with regards to the top of the view's frame, which means that you can KVO contentOffset, and use the resultant change in value to approximate the frame that the view should stick to with a little MIN() magic. Assuming your header is 44 pixels tall, the code below calculates the appropriate frame value:
CGPoint offset = [contentOffsetChange CGPointValue];
[self.tableView layoutSubviews];
self.tableView.tableHeaderView.frame = CGRectMake(0,MIN(0,offset.y),CGRectGetWidth(self.scrollView.frame),44);
If the above is infeasible, SMHeadedList actually has a fairly great, and little known, example of how complicated it can be to implement a "double tableview". That implementation has the added benefit of allowing the "header" tableview to scroll with the "main" tableview.
For future visitors, I've implemented a much simpler version, albeit one that accomplishes the goal with Reactive Cocoa, and a little bit of a different outcome. Even so, I believe it may be relevant.
What if you break the UIView into the top and bottom. The bottom will be the info.
Set UITableView.tableHeaderView = topView in viewDidLoad
and the return bottomView as Section Header in delegate method to make it float:
(UITableViewHeaderFooterView *)headerViewForSection:(NSInteger)section
{
return bottomView;
}
Just using the UITableView can solve with your problem. it is not need to use another scroll view.
set your view as the header view of UITableView. Then add your present view to the header view.
complete - (void)scrollViewDidScroll:(UIScrollView *)scrollView; . Tn the function to check the contentoffset of scroll view, and set the present view's frame.
I currently have a view controller that is comprised of a Navigation bar, followed by a UIView that has two UIButtons added as subViews. There is then a UITableView underneath that begins at the bottom of the container UIView.
At the moment, when the user scrolls the UITableView it goes behind the UIView and UIButtons. What I actually want to happen is for the UIView and UIButtons to move up with the table view but only by the value of their height which in this case is 58 pixels. The flow would be like this...
1) Table scrolls and the UIView moves with it for the first 58 pixels.
2) The user continues to scroll the table but the UIView "pins" itself just out of view under the navigation bar.
3) When the user scrolls the table back down the UIView is then picked up and dragged back into view. I believe the new Facebook app does something similar in the timeline.
I don't want to set the UIView as the TableHeaderView of the table as I also have a pull-to-refresh which then sits above the buttons and looks terrible. I've tried playing around with the contentOffset properties of the underlying scrollview of the table but have hit a brick wall.
Any advice on where to start would be appreciated.
Thanks
EDIT: I am gotten a little further and using this code to move the frame of the UIView.
-(void) scrollViewDidScroll:(UIScrollView *)scrollView
{
NSLog (#"Content Offset: %f", self.tableView.contentOffset.y);
NSLog (#"Button Frame: %f", self.btnBackground.frame.origin.y);
if (self.tableView.contentOffset.y > 0)
{
CGRect newFrame = self.btnBackground.frame;
newFrame.origin.x = 0;
newFrame.origin.y = -self.tableView.contentOffset.y;
[self.btnBackground setFrame: newFrame];
}
}
The problem now is that the scrollViewDidScroll delegate method doesn't get fired quickly enough if the table view is scrolled fast. The result is that the UIView doesn't quite make all way back to its original position when scroll quickly.
The scroll content offset is a good idea. Also if you tableview has only one section one approach is to do a custom header view representing the top level widgets. If there is more than one sections create an additional empty section which would return your custom header.
You can refer to this stack overflow post.
Customize UITableview Header Section
Well Asked Question (y)
well , for me i would first : use a main UIScrollView that contains both your topView and the tableView under it and that has the same width as your top UIView and UITableView and set its height to be height(tableView) + height(topView).
Second : since UITableView is a subClass of UISCrollView you can use scrollViewDidScroll delegate to know if the tableview is scrolled up or down.
in this cas you will have Two cases :
1) tableview is scrolled up = > you set the content offset of the main scrollView to be
[scrollView setContentOffset:CGPointMake(0, 58) animated:YES];
2) when the table view is scrolled down you can reset the content offset again
[scrollView setContentOffset:CGPointMake(0, 0) animated:YES];
I am building an app with a stream of social content and am trying to get the behavior of how instagram does it's stream in app. So basically a top header that scrolls off the screen but bounces between that and the content. I can make the top header scroll off the screen and I can make the view not bounce but I want to use Pull to refresh and that ends up going above the "faux" nav bar UIView. I know that a normal Navbar will produce this but this one that scrolls off is a different story.
Currently I have a UITableview that has a UIView above the UITableViewCell and everything works great except the bounce happens above the UIView. I figure I need to get the UIView above the UITableView however in the UITableViewController the storyboard won't allow me to place the UIView above the UITableView.
Any ideas???
Thanks
Well I finally got this all to work so I thought I would post the Answer for everyone.
Basically I set a standard Navigation bar and then on scrollViewDidScroll I get the offset of the scrolling and change the frame based on that. This seems to work perfectly, see below for my scrollViewDidScroll method.
- (void)scrollViewDidScroll:(UIScrollView *)sender {
//Initializing the views and the new frame sizes.
UINavigationBar *navbar = self.navigationController.navigationBar;
UIView *tableView = self.view;
CGRect navBarFrame = self.navigationController.navigationBar.frame;
CGRect tableFrame = self.view.frame;
//changing the origin.y based on the current scroll view.
//Adding +20 for the Status Bar since the offset is tied into that.
navBarFrame.origin.y = MIN(0, (sender.contentOffset.y * -1)) +20;
navbar.frame = navBarFrame;
tableFrame.origin.y = MIN(0,MAX(-44,(sender.contentOffset.y * -1)));
tableView.frame = tableFrame;
}
Also you will want to make your TableView 44px taller to compensate for the scrolling otherwise your frame will not be big enough. I just did this in viewWillAppear and made the frame bigger.
I have a view containing a UITableView. Above the table I'd like to display an iAd. I figured the best way to do this was to set the contentInset for the table to the height of the iAd banner - all good so far.
The app supports rotation, so I have this code:
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation duration:(NSTimeInterval)duration {
NSString *bannerSize = (UIInterfaceOrientationIsPortrait(interfaceOrientation)) ? ADBannerContentSizeIdentifierPortrait : ADBannerContentSizeIdentifierLandscape;
int bannerHeight = [ADBannerView sizeFromBannerContentSizeIdentifier:bannerSize].height+1;
iAdBanner.currentContentSizeIdentifier = bannerSize;
[mainTable setContentInset:UIEdgeInsetsMake(bannerHeight, 0, 0, 0)];
}
This works on the first rotate, but when I rotate back, the contentInset doesn't appear to have been changed, in fact every time I rotate the device it appears to be set to the 'inverse' value
But it is working - in that if I attempt the scroll the table, everything jumps into place.
After much reading, I added the following line to the end of the code above:
[mainTable scrollRectToVisible:CGRectMake(0, 0, 10, 10) animated:NO];
This worked! - BUT - I don't want the table to scroll back to the top every time the device is rotated, so I tried this:
[mainTable scrollRectToVisible:CGRectMake(mainTable.contentOffset.y+bannerHeight, 0, 10, 10) animated:NO];
But if I'm not at the top of the table the original problem reappears, in that the header of the current section (it's a plain table) is about 20 pixels too high or too low, fixed immediately by manually scrolling.
So now I'm lost - any ideas?
you probably would want to use a UIViewController instead of a UITableViewcontroller and addd the UITableView programmatically. Then, you can easily set the frames of the ad and the tableview. And when rotated, use the rotating methods to change the frames again!