How do I hide a tableView searchBar - ios

I want to have a searchBar in the tableView that is hidden by default, but if the user pulls down it appears.
So I have used the following code to implement this but when the view is first displayed the searchBar is momentarily visible and I would like avoid this brief flash. Adding self.tableView.contentOffset = CGPointMake(0, 0) to the viewDidLoad() method does not have any affect. Note that the tableView is behind a navigation controller which is why setting the Y offset to 0 effectively hides the searchBar behind the navigation bar. Any idea how to make sure the tableView's headerView is hidden when the view is first be displayed.
I have considered simply removing the headerView but then the user can't drag down to access it.
override func viewDidLoad() {
super.viewDidLoad()
searchBar.delegate = self
searchBar.frame = CGRectMake(0, 0, self.tableView.frame.size.width, 44)
if let tv = self.tableView {
if let headerView = tv.tableHeaderView {
headerView.addSubview(searchBar)
} else {
FLOG("No table header view is available so create one!")
let headerView = UIView()
let width: CGFloat = tv.frame.size.width
headerView.frame = CGRectMake(0, 0, width, 44);
tv.tableHeaderView = headerView
tv.tableHeaderView!.addSubview(searchBar)
}
}
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
dispatch_after(0, dispatch_get_main_queue()) {
self.tableView.contentOffset = CGPointMake(0, 0)
}
}

I think there's an ordering issue with viewWillAppear and viewDidLoad. As I understand, viewWillAppear gets called every time a view will be drawn and added to the view hierarchy, whereas viewDidLoad gets called when a VC loads its subview for the first time.
So, I suppose on your first run the view loads via the mainQueue with viewDidLoad which places your search bar at (0,44), then viewWillAppear gets called but actually causes a brief visual blip due to the call to move the table's offset on the mainQueue. But on subsequent calls, since viewDidLoad has already happened, only viewWillAppear gets called. And since the search bar doesn't first start at (0,44) and then needs to move to (0,0), there's no blip.
Rather than directly defining the frame for the search bar, try calling sizeToFit on it after adding it to the tableHeader. Secondly, I don't believe you have to instantiate the header view, just set it equal to the searchbar. Lastly, take the contentOffset call out of the dispatch_queue, it should be handled correctly by viewWillAppear as is.
I went deep into UISearchController for iOS8 recently, and documented the journey (in part at least). For the code I used, here's a link to the implementation file and the corresponding write-up I did. It's in ObjC, so you'll need to do a little translation. But it looks like:
self.searchController = [[UISearchController alloc] initWithSearchResultsController:self.filteredResultsTableViewController];
self.searchBar = self.searchController.searchBar;
// other code ...
[self.searchBar sizeToFit];
// other code ...
self.tableView.tableHeaderView = self.searchBar;

Related

SearchBar in TableViewController

In my app I have a UITableViewController, and I want to add a searchbar to it. The tableview has 3 sections which expand when clicked on one of them. I added this code to viewDidLoad:
searchBar.showsScopeBar = true
searchBar.delegate = self
searchBar.frame = CGRectMake(0, 0, tableView.frame.size.width, 100)
self.view.addSubview(searchBar)
The problem is: the searchbar appears in the first section when you open it, and it should appear above the whole tableview, any suggestions?
Any help is appreciated!
You can add searchBar as a tableHeaderView of the TableView of your UITableViewController.
You can find pretty helpful example in Programming iOS 9 by Matt Neuburg, inside chapter II: Table Views and Controller Views.
let src = SearchResultsController(data: self.sectionData)
let searcher = UISearchController(searchResultsController: src)
self.searcher = searcher
searcher.searchResultsUpdater = src
let b = searcher.searchBar
b.sizeToFit() // crucial, trust me on this one
b.autocapitalizationType = .None
self.tableView.tableHeaderView = b
self.tableView.reloadData()
self.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 0), atScrollPosition:.Top, animated:false)
He also warns, that adding search bar as the table view's header view may have an odd effect: it causes the table view's background color to be covered by an gray color, visible above the search bar when the user scrolls down. The official workaround is to assign the table view a backgroundView with the desired color.

UITableViewController changes UITableView's frame when app enters back into foreground and becomes active

I have a UITableViewController, that is embedded in a UITabBarController and also managed by a UINavigationController.
The only place that I have been able to customize the UITableViewController's table view frame is in viewDidAppear:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:YES];
[self customizeTableViewAppearance];
}
Here is the customizeTableViewAppearance method:
- (void)customizeTableViewAppearance
{
self.tableView.contentInset = UIEdgeInsetsMake(20, 0, 0, 0);
if([UIScreen mainScreen].bounds.size.height == 568) {
[self.tableView setFrame:CGRectMake(0, 60, 320, 460)];
} else {
[self.tableView setFrame:CGRectMake(0, 60, 320, 370)];
}
}
This works perfectly when first using the app, but if you go to the device's home screen, and then resume using the app again, none of the usual view methods are called and the table view has been moved. So for whatever reason, even though view methods are not called, UITableViewController is changing the custom frame that I have set for it's UITableView.
Sure enough, if I move to a different tab, and then revisit the tab again, the view methods are called and the UITableView's frame is correct again.
How can I make it so that if the user leaves the app, and then resumes the app again later, that my frame will stay set and not be reset by the UITableViewController?
It's not really clear why you are manipulating the frame of your UITableViewController's tableView, but in most cases, you shouldn't.
From what you pasted, it seems like are trying to prevent the tableView or its content from appearing underneath your navbar, and your tabbar.
Instead of changing the tableView's frame, you should try one of the following things:
Try setting self.edgesForExtendedLayout = UIRectEdgeNone when initialising your UITableViewController
or:
Make sure self.automaticallyAdjustsScrollViewInsets = YES when initialising your UITableViewController
or, if for some reason you need to manage your tableView's contentInset manually:
Make sure self.automaticallyAdjustsScrollViewInsets = NO when initialising your UITableViewController. Now implement viewDidLayoutSubviews as follows
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.tableView.contentInset = UIEdgeInsetsMake(self.topLayoutGuide.length, 0, self.bottomLayoutGuide.length, 0);
}
Edit:
I just saw you're using a Storyboard. You can either set the edgesForExtendedLayout or automaticallyAdjustsScrollViewInsets from within your storyboard, or set them by implementing the -(void)awakeFromNib; method.
Without knowing why you are doing what you're doing, an easy solution to handle that case would be the following - Add this in your tableView's init method:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(customizeTableViewAppearance) name:UIApplicationDidBecomeActiveNotification object:nil];
That way, any time your application becomes active again your tableView will call that method.
The way to fix this was to override the viewDidLayoutSubviews method and place my customizeTableViewAppearance method inside of it:
- (void)viewDidLayoutSubviews
{
[self customizeTableViewAppearance];
}
The UITableView always has the correct frame now.

UITableView goes under translucent Navigation Bar

I am trying to have a transparent navigation bar in IOS 7 app. There is a full screen image in my application. I am also having a UITableView over that image. When I use the code below, image fits the screen as I want but UITableView goes under navigation bar.
in viewDidLoad
i use
self.navigationController.navigationBar.shadowImage = [UIImage new];
self.navigationController.navigationBar.translucent = YES;
self.navigationController.view.backgroundColor = [UIColor clearColor];
it is being ok when I change to self.navigationController.navigationBar.translucent = NO; but then I lose transparency at navigation bar.
You could set the contentInsets of your tableView so it is initially below the navigation bar, but would scroll behind it (content would be overlapping)
self.tableView.contentInset = UIEdgeInsetsMake(44,0,0,0);
Or you could offset the frame of the tableview. Then the scrolling content would be cut off below the navigation bar (which wouldn't look good, too)
I my case helped this one (modified version of Bill Chan's code):
Objective C version:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
CGRect rect = self.navigationController.navigationBar.frame;
float y = rect.size.height + rect.origin.y;
self.tableView.contentInset = UIEdgeInsetsMake(y, 0, 0, 0);
}
The point is that table have to be pushed down for the height of navigationBar (rect.size.height) plus status bar height (rect.origin.y);
Swift version (also compatible with Swift 2):
override func viewDidLayoutSubviews() {
if let rect = self.navigationController?.navigationBar.frame {
let y = rect.size.height + rect.origin.y
self.tableView.contentInset = UIEdgeInsetsMake( y, 0, 0, 0)
}
}
I had the similar problem for iOS 9. When I first open viewController, tableView is under top bar. Then after scrolling tableView everything works fine.
Select your view controller
Click the 'Attributes Inspector' tab
Uncheck 'Under Top Bars'
Set the y-position of tableview to height of the navigation bar plus height of the status bar (let it be height)
i.e,
height = 64; // height of navigation bar = 44(In portait), height of status bar = 20
tableView.frame = CGRectMake(tableView.frame.origin.x, height , tableView.frame.size.width, tableView.frame.size.height);
If you are using autolayout just change the update the tableView top constraint instead of changing frame.
and also change viewController automaticallyAdjustsScrollViewInsets to NO
self.automaticallyAdjustsScrollViewInsets = NO;
If you are supporting different orientation update frame and contentInset to (52) because navigation bar height in landscape mode is 32.
check this Sample
This is working in both landscape mode and portrait mode in iOS8:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
CGRect rect = self.navigationController.navigationBar.frame;
float y = -rect.origin.y;
self.tableView.contentInset = UIEdgeInsetsMake(y ,0,0,0);
}
Better not to hardcode the Inset values as it might based on the orientation of the device.
Code:
func setTableViewContentInset() {
let contentInsetHeight = topLayoutGuide.length
let contentInset = UIEdgeInsetsMake(contentInsetHeight, 0, 0, 0)
tableView.contentInset = contentInset
tableView.scrollIndicatorInsets = contentInset
}
func scrollToTop() {
if tableView.indexPathsForVisibleRows?.count > 0 {
let topIndexPath = NSIndexPath(forRow: 0, inSection: 0)
tableView.scrollToRowAtIndexPath(topIndexPath, atScrollPosition: .Top, animated: false)
}
}
func scrollToTopOfVisibleCells() {
if let visibleIndexPaths = tableView.indexPathsForVisibleRows where tableView.indexPathsForVisibleRows?.count > 0 {
let topMostVisibleIndexPath = visibleIndexPaths[0]
tableView.scrollToRowAtIndexPath(topMostVisibleIndexPath, atScrollPosition: .Top, animated: false)
}
}
//MARK: Load Views
override func viewDidLoad() {
super.viewDidLoad()
setTableViewContentInset()
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
scrollToTop()
}
//MARK: Trait collection change
override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
setTableViewContentInset()
scrollToTopOfVisibleCells()
}
Solutions that introduce a magic constant don't scale most of the time. For example, if the next iPhone introduces a different navigation bar height we'll have to update our code.
Fortunately, Apple provided us cleaner ways of overcoming this issue, for example topLayoutGuide:
The topLayoutGuide property comes into play when a view controller is
frontmost onscreen. It indicates the highest vertical extent for
content that you don't want to appear behind a translucent or
transparent UIKit bar (such as a status or navigation bar)
Programmatically you can achieve with the following code snippet (the same can be achieved via IB too):
override func viewDidLoad() {
super.viewDidLoad()
automaticallyAdjustsScrollViewInsets = false
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.topAnchor.constraint(equalTo:
topLayoutGuide.bottomAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
Note: topLayoutGuide is deprecated in iOS 11, we should use the safeAreaLayoutGuide property of UIView instead.
Introduction
I am new to both iOS development and Stack Overflow, so forgive me if my post isn't perfect.I also had this issue, and when I used the content insets for my UITableView it worked perfectly upon loading first, or when visiting it from my other tabs; however, if I navigated back to the view, it would have the extra "padding". I figured out a work around, so that my UITableView will be correctly placed every time.
The Issue
When you first load the UITableView, or tab to it, it needs the insets to correctly start the table below the navigation bar, but when you navigate back it does not need the insets, because for some reason, it correctly calculates for the placement of the UITableView. This is why you can get the extra padding.
The Solution
The solution involves using a boolean to determine whether you have navigated away, so that it can correctly determine whether it needs the content insets or not.In -(void)viewDidLoad I set hasNavigatedFurther = NO. Then:
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (!hasNavigatedFurther) {
self.tableView.contentInset = UIEdgeInsetsMake(64, 0, 0, 0);
} else {
self.tableView.contentInset = UIEdgeInsetsZero;
//In order to allow visiting between tabs and retaining desired look
hasNavigatedFurther = NO;
}
}
In order to make this work, you need to set hasNavigatedFurther = YES just before your code that pushes another view onto the navigation stack.
-(void)btnTouched:(id)sender {
hasNavigatedFurther = YES;
NextViewController* nvc = [NextViewController new];
[self.navigationController pushViewController:nvc animated:YES];
}
I came up with the following solution, which, on navigating to the view controller for the first time, adjusts the table view's contentInset for the navigation bar's height, taking into account any padding that the top cell might have. When returning to this view controller after pushing another view controller onto the stack, I then re-adjust the contentInset to UIEdgeInsetsZero:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self adjustEdgeInsetsForTableView];
}
- (void)adjustEdgeInsetsForTableView {
if(self.isMovingToParentViewController) {
self.tableViewForm.contentInset = UIEdgeInsetsMake(self.navigationController.navigationBar.frame.size.height + padding, 0, 0, 0);
} else {
self.tableViewForm.contentInset = UIEdgeInsetsZero;
}
}
I combined #Adam Farrell and #Tash Pemhiwa 's solutions, and finally the code below works for me:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self adjustEdgeInsetsForTableView];
}
- (void)adjustEdgeInsetsForTableView
{
if(self.isMovingToParentViewController) {
self.tableView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0);
} else {
self.tableView.contentInset = UIEdgeInsetsMake(64, 0, 0, 0);
}
}
Hope this will help people who waste couple of hours on this weird UI behavior.
Constrain the table view to the bottom of the navigation bar. The table view will automatically be offset by 44, but then in code we can just do this:
tableView.contentInset = UIEdgeInsets(top: -44, left: 0, bottom: 0, right: 0)
The bar is transparent and has no color, but the table view does not overlap it at all. Notice the word "Hook" gets cut off despite the navigation bar being transparent. This will only work of you constrain the table view top edge to be 0 from the navigation bar. NOT 0 from the top view.
All you need is love this:
assert(tableView.contentInsetAdjustmentBehavior == .automatic)
there is zero need to do ugly magic constants beardance from iOS 11 onwards
I did not even need to set contentInsetAdjustmentBehavior to .none
to fix navbar underlapping.
.automatic
worked automagically
try to use layoutguide to fix
var constraints = [NSLayoutConstraint]()
let guide = view.safeAreaLayoutGuide
constraints.append(self.tableView.leadingAnchor.constraint(equalTo: guide.leadingAnchor))
constraints.append(self.tableView.trailingAnchor.constraint(equalTo: guide.trailingAnchor))
constraints.append(self.tableView.topAnchor.constraint(equalTo: guide.topAnchor))
constraints.append(self.tableView.bottomAnchor.constraint(equalTo: guide.bottomAnchor))

Why does UIViewController extend under UINavigationBar, while UITableViewController doesn't?

I have UITabbarController with UINavigationController in it. I have a subclass of UIView that I assign as the view of UIViewController in the navController. This is pretty standard stuff, right? This is how I do it
_productCategoryView = [[ProductCategoryView alloc] initWithFrame:self.view.frame];
self.view = _productCategoryView;
This view has a UITableView as subView
_productCategoryTableView = [[UITableView alloc] initWithFrame:self.frame style:UITableViewStylePlain];
_productCategoryTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
_productCategoryTableView.backgroundColor = [UIColor clearColor];
[self addSubview:_productCategoryTableView];
For the sake of debugging I am setting self.backgroundColor = [UIColor blueColor] on the view.
From the above initialization of tableView one might think that the view's and table's frame is same. However when I run in iOS 7, the view's origin is set behind the UINavigationBar. This is understandable because I am setting self.navigationBar.translucent = YES; in my subclass of UINavigationController. But what I don't understand is how come the table is sitting just below the navBar? Shouldn't it also start from (0, 0) which is behind the navBar? See screenshot Scenario 1 below. Notice the blue hue behind navBar
Now, I push another viewController on the navigation stack, simply by using [self.navigationController pushViewController.....]. Again I have a custom UIView with a tableView in it. However I also have a UILabel above this table, and again for debugging, I gave it a redColor. This time I am setting the label's origin to be almost same as the view's
CGRect boundsInset = UIEdgeInsetsInsetRect(self.bounds, UIEdgeInsetsMake(10, 10, 10, 10));
CGSize textSize = [_titleLabel.text sizeWithFont:_titleLabel.font
constrainedToSize:CGSizeMake(boundsInset.size.width, MAXFLOAT)
lineBreakMode:NSLineBreakByWordWrapping];
printSize(textSize);
_titleLabel.frame = CGRectMake(boundsInset.origin.x,
boundsInset.origin.y,
boundsInset.size.width,
textSize.height);
So, going by the logic above, the label should be visible, right? But this time it's not. This time the label is behind the navBar.
Notice, the red hue behind navBar.
I would really like to align the subView below the navBar consistently. My questions are
1. How is the tableView offset by 64pixels (height of nav + status bar in iOS 7) automatically, even though it's frame is same as the view's?
2. Why does that not happen in the second view?
By default, UITableViewController's views are automatically inset in iOS7 so that they don't start below the navigation bar/status bar. This is controller by the "Adjust scroll view insets" setting on the Attributes Inspector tab of the UITableViewController in Interface Builder, or by the setAutomaticallyAdjustsScrollViewInsets: method of UIViewController.
For a UIViewController's contents, if you don't want its view's contents to extend under the top/bottom bars, you can use the Extend Edges Under Top Bars/Under Bottom Bars settings in Interface Builder. This is accessible via the edgesForExtendedLayout property.
Objective-C:
- (void)viewDidLoad {
[super viewDidLoad];
self.edgesForExtendedLayout = UIRectEdgeNone;
}
Swift 2:
self.edgesForExtendedLayout = UIRectEdge.None
Swift 3+:
self.edgesForExtendedLayout = []
#Gank's answer is correct, but the best place to do this is on the UINavigationControllerDelegate (if you have one):
func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
viewController.edgesForExtendedLayout = UIRectEdge.None
}

UITableView Activity Indicator the Apple way

My problem is pretty common. I want to show some "loading" stuff while the data that will be shown in a table is being downloaded. But all the solutions I've seen this far is either to add some spinners to cells, or put some semi-transparent view over the app.
What I want to active is the thing that apple does in its apps (for example YouTube). Like, the spinning wheel with word "Loading" is shown on the white background, and then the data is shown.
I have also seen some solution with a grouped style, but my table has a plain style so it isn't helping me a lot I think.
EDIT
My final code looks like this:
- (void)loadImagesData
{
UIView *tableView = self.view;
self.view = [[LoadingView alloc] initWithFrame:CGRectMake(0, 0, 320, 367)];
dispatch_queue_t downloadQueue = dispatch_queue_create("flickr downloader", NULL);
dispatch_async(downloadQueue, ^{
NSArray *images = [FlickrFetcher photosInPlace:self.place maxResults:MAX_PHOTOS];
dispatch_async(dispatch_get_main_queue(), ^{
self.view=tableView;
self.imagesData = images;
});
});
dispatch_release(downloadQueue);
}
and imageData setter calls reloadData on table view.
Create a UIViewController (not a UITableViewController - even if you are about to present a table)
Create your "Loading" view and add it as an outlet
Create your tableview and add it as an outlet too
When the controller is presented set its view property to point to your "Loading" view. When all tasks have completed, switch the view with the tableview
Just don't forget to add protocols and methods involved in order to control your tableview
I hope that this will help you
Another answer to show an activity indicator inside a UITableViewController, without changing it to a UIViewController.
In the storyboard, add an activity indicator inside the table view (but not above the TableViewCell !). It will be inside the table view header (not section header !)
Connect the outlet of the activity indicator to your class file
#IBOutlet weak var activityIndicator: UIActivityIndicatorView!
Use these functions to show or hide the activity indicator
// enable tableview and hide the activity indicator
func activateScreen() {
self.tableView.tableHeaderView?.frame = CGRect(x: 0, y: 0, width: tableView.frame.width, height: 0)
self.tableView.tableHeaderView = self.tableView.tableHeaderView // necessary to really set the frame
activityIndicator.stopAnimating()
}
// disable tableview animate the activity indicator
func disactivateScreen() {
self.tableView.tableHeaderView?.frame = tableView.frame
self.tableView.tableHeaderView = self.tableView.tableHeaderView // necessary to really set the frame
activityIndicator.startAnimating()
}
Enjoy !

Resources