Correct subclassing and reusing of UITableViewHeaderFooterView - ios

I have a UITableView where I have section headers that can be tapped to expand or collapse the section. In my particular example each section only has one row, which is either visible (section expanded) or hidden (section collapsed).
As section header i'm using custom UITableViewHeaderFooterView - HeaderAccountView. I created *.xib file at Interface Builder , and set it custom class to my HeaderAccountView (still at IB field).
There are no any changes to init method or smth like this in my HeaderAccountView.h and HeaderAccountView.m files - only some functions to highlight self (selected section) etc.
in my main ViewController .m file
- (void)viewDidLoad
{
[super viewDidLoad];
.........
.........
UITableView *tableView = (id)[self.view viewWithTag:1];
UINib *nib= [UINib nibWithNibName:#"HeaderAccountView" bundle:nil];
[tableView registerNib:nib forHeaderFooterViewReuseIdentifier:#"HeaderCell"];
}
and then
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
HeaderAccountView *headerView = [tableView dequeueReusableHeaderFooterViewWithIdentifier:#"HeaderCell"];
if (headerView==nil)
{ headerView = [[HeaderAccountView alloc]
initWithReuseIdentifier:#"HeaderCell"];
}
return headerView;
}
when i'm running project everything going OK - sections load with needed data in it, when section receive tap - it highlights (like standard cell).
But when i'm scrolling away tableview to bottom for example from selected highlighted section, and this highlighted section already is not visible at view - that section that just appeared from bottom - already highlighted!
I understand that its because it creates new instance of my HeaderAccountView with property BOOL selected set to YES.
But I'm new to objective-c (and coding) and don't understand how to correct resolve this.
I tried to use prepareForReuse method of my custom UITableViewHeaderFooterView like this
HeaderAccountView.m:
-(void) prepareForReuse
{
self.selectedBackground.alpha = 0;
}
It works better - but now i have another issue - when i returning to my first (truly) selected and highlighted section - it obviously don't highlight.
Thanks for any help and sorry if it elementary question.

You have to manually keep a list of your selected headers indexes.
Next, implement the method tableView:willDisplayHeaderView: in your view controller to refresh your header when it will be displayed.
- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
{
view.selectedBackground.alpha = ([_highlightedHeadersList containsObject:#(section)] ? 0.0f : 1.0f);
}
And you have to add / remove indexes in _highlightedHeadersList.

I've done this using the following. In the table view controller I created a property (NSInteger), and called it sectionForSelectedHeader. Set it to -1 initially so no section will be initially selected.
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
RDHeader *header = [tableView dequeueReusableHeaderFooterViewWithIdentifier:#"Header"];
header.tag = section;
if (header.gestureRecognizers.count == 0) {
UITapGestureRecognizer *tapper = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(headerSelected:)];
[header addGestureRecognizer:tapper];
}
header.selected = (self.sectionForSelectedHeader == section)? 1 : 0;
return header;
}
-(void)headerSelected:(UITapGestureRecognizer *) tapper {
if ([(RDHeader *)tapper.view selected] != 1) {
self.sectionForSelectedHeader = tapper.view.tag;
}else{
self.sectionForSelectedHeader = -1;
}
[self.tableView reloadData];
}
Then in my custom header subclass, I have a method setSelected: (selected is an NSInteger property in the RDHeader class) like this:
-(void)setSelected:(NSInteger)selected {
_selected = selected;
if (selected) {
self.contentView.backgroundColor = [UIColor orangeColor];
}else{
self.contentView.backgroundColor = [UIColor yellowColor];
}
}

Related

Unable to update the Subview in a Custom UITableViewCell

I have a custom UITableViewCell in which I have two UIViews . I want to change the BackgroundColor radius and some more properties of UIViews.
But I am unable to do so.
Here is my setUp.
Step 1:
Create A Custom Cell with XIB.
Step 2: Entered the Cell Identifer name in XIB for CustomCell.
Step 3: Instantiated NIB in viewDidLoad
UINib *nib = [UINib nibWithNibName:#"CustomCell" bundle:nil];
[[self mTableView] registerNib:nib forCellReuseIdentifier:#"CustomCell"];
Step 4: Cell For Row At Index Method:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Create an instance of ItemCell
CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:#"CustomCell"];
[cell updateCellView];
return cell;
}
Step 5: Checked Twice the Outlet Connections all are Fine.
Step 6: Custom Cell Class:
#import "TransportCell.h"
#implementation TransportCell
- (void)awakeFromNib {
// Initialization code
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
- (void)updateCellView
{
self.backgroundView.backgroundColor = [UIColor redColor];
}
#end
This code has no effect on my Cell View.
I debug the code. When I logged the backgroundView I get nil when updateCellView method get called:
Here is my CustomCell's Xib:
I have to change the Properties of Inner UIView.(Blue in color)
Instead of calling the method in the cellForRowAtIndexPath try this:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Create an instance of ItemCell
CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:#"CustomCell"];
return cell;
}
and then in your custom method
- (void)updateCellView
{
self.backgroundView.backgroundColor = [UIColor redColor];
// reload the table view
[self.tableView reloadData];
}
you need to add [ tableview reloadData ]
in method update cell view
Ideally all the info needed to render the cell should come from either the data source or other events. For example, if you are changing the border-color it should mean that either some entity (say user) has performed an event or something has changed in the data-source like a value passed its threshold.
The change in data-source or those events should either trigger refreshing the whole table:
-(void) reloadData
or some cells:
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
Only then the cells will get updated.
you are changing the self.backgroundview.backgroundColor
Default it is nil for cells in UITableViewStylePlain, and non-nil for UITableViewStyleGrouped.
The 'backgroundView' will be added as a subview behind all other views.
#property (nonatomic, strong, nullable) UIView *backgroundView;
try this method in TransportCell, it's swift code make same for objective c
override func layoutSubviews()
{
self.backgroundView.backgroundColor = [UIColor redColor];
}
Use this:
self.contentView.backgroundColor = [UIColor redColor];
Hope this helps!

Dynamically add tableview cell one by one by button click in a single tableview

i. I have tried a lot but could not execute successfully .
ii. In tableview cell, there are 3 3 fields need to display. One image view, button1 -->taking photo button, button2---> browse button.
iii. First time tableview should display a custom cell with one row.
iv. When a user clicks "add new button" , which lays outside of tableview, a new row will create with all above 3 fields (image view, button1, button2)
v. Number of clicks of "add new button" , will create new rows with all above 3 fields.
vi. I could do all above things created dynamically successfully with a simple image view which contains above 3 fields but could not succeed to work on custom cell.
vii. Again I need to set the tag of each cell, broswe button, take photo button so that when clicks, will take the tag value.
The table view works by adding a delegate and a data source. Assuming your table view has an owner as a view controller and both delegate and data source are the view controller itself. All you need to do is implement those data source methods to return an appropriate data then you should call reloadData on the table view or if you want a bit of extra work to look nicer check how to add rows animated around the web.
This is a very simple and not optimized example but is very short and easy to read. I hope it helps you get on the right track:
#interface MyViewController()<UITableViewDataSource, UITableViewDelegate>
#property UITableView *tableView;
#property NSArray *myCells;
#end
#implementation MyViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.delegate = self; // could be done in storyboard
self.tableView.dataSource = self; // could be done in storyboard
[self addACell];
}
- (void)addCellButtonPressed:(id)sender {
[self addACell];
}
- (void)addACell {
MyCell *cell = [[MyCell alloc] init];
[cell.button1 addTarget:self action:#selector(cellButton1Pressed:) forControlEvents:UIControlEventTouchUpInside];
[cell.button2 addTarget:self action:#selector(cellButton2Pressed:) forControlEvents:UIControlEventTouchUpInside];
self.myCells = [self.myCells arrayByAddingObject:cell];
[self.tableView reloadData]; // will call the delegate again and refresh cells
}
- (void)cellButton1Pressed:(id)sender {
MyCell *cellPressed = nil;
for(MyCell *cell in self.myCells) {
if(cell.button1 == sender) {
cellPressed = cell;
break;
}
}
// do whatever
}
- (void)cellButton2Pressed:(id)sender {
MyCell *cellPressed = nil;
for(MyCell *cell in self.myCells) {
if(cell.button2 == sender) {
cellPressed = cell;
break;
}
}
// do whatever
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.myCells.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
return self.myCells[indexPath.row];
}
#end

iOS Table View - Set tag on cell or label?

I'm a newbie to iOS development. Currently learning by following tutorials from iOS Apprentice by Matthijs Hollemans. I'm using Xcode 6 and Objective-C to make the tutorial apps.
In the book's second tutorial, he teaches his readers how to build a to-do-list app with table views, navigation controllers, etc.
In the beginning:
A custom Label with the text "Label" is placed in the prototype cell of the table. On running the app at this point, "Label" shows up in the app, as expected.
To display custom text in the table view, he asks the readers to set the 'Tag' identifier for the Label and then display custom text using the 'Tag' as shown in the code below. This leads to abnormal behaviour.
What this does is that it displays "Label" on launching the app. However, on scrolling the text off the screen and then back on the screen, the custom text shows up.
To resolve this, instead of setting the 'Tag' identifier on the Label, I set it on the Table View Cell, in spite of the Author of the book specifically warning against doing this. I also had to remove the default text "Label" from the Label in the table view so that the custom text and "Label" don't overlap.
This fixed the problem, but now I'm confused as to what is the correct protocol to follow for setting the 'Tag' identifier when it comes to using table views. Should the 'Tag' be set on Table View Cell or the Label? If it should be set on the Label then what could be the reason for me to run into this problem?
Here's the code for the main ViewController.m
#import "ViewController.h"
#import "ChecklistItem.h"
#interface ViewController ()
#end
#implementation ViewController
{
NSMutableArray * _items;
}
- (void)viewDidLoad {
[super viewDidLoad];
_items = [[NSMutableArray alloc] initWithCapacity:20];
ChecklistItem * item;
item = [[ChecklistItem alloc]init];
item.text = #"Walk the dog";
item.checked = NO;
[_items addObject:item];
item = [[ChecklistItem alloc]init];
item.text = #"Brush teeth";
item.checked = NO;
[_items addObject:item];
item = [[ChecklistItem alloc]init];
item.text = #"Prepare breakfast";
item.checked = NO;
[_items addObject:item];
item = [[ChecklistItem alloc]init];
item.text = #"Soccer practice";
item.checked = NO;
[_items addObject:item];
item = [[ChecklistItem alloc]init];
item.text = #"Eat ice cream";
item.checked = NO;
[_items addObject:item];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
}
//data source method no.1 to get the number of rows in section for the table view
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [_items count];
}
- (void)configureCheckmarkForCell:(UITableViewCell *)cell withChecklistItem:(ChecklistItem *) item {
//if the item is checked, display checkmark
if (item.checked) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
} else {
cell.accessoryType = UITableViewCellAccessoryNone;
}
}
- (void)configureTextForCell:(UITableViewCell *)cell withChecklistItem:(ChecklistItem *) item {
UILabel * label = (UILabel *)[cell viewWithTag:1000];
label.text = item.text;
}
//data source mathod no.2 to get the cell to display the row in the given index path
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:#"ChecklistItem"];
ChecklistItem * item = _items[indexPath.row];
[self configureTextForCell:cell withChecklistItem:item];
[self configureCheckmarkForCell:cell withChecklistItem:item];
return cell;
}
//delegate method to handle taps on rows
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell * cell = [tableView cellForRowAtIndexPath:indexPath];
ChecklistItem * item = _items[indexPath.row];
[item toggleChecked];
[self configureCheckmarkForCell:cell withChecklistItem:item];
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
#end
I'm not familiar with the tutorial, but using tags to identify labels within cells this way is not a good idea.
The cell should know about it's own label. It's far better to just have a method on the cell that you can pass the text to, and then let the cell take care of displaying the text in the cell.
By using the tag in this way, you are expecting to know too much about the internal implementation of the cell, and this is brittle and is likely to break.
So my answer is to set the tag on neither of them, and to use a proper configuration method of the cell itself.
Edited to add
You can download a simple version of a project for configuring a cell, without using tags here https://bitbucket.org/abizern/so-27713743/get/543739690dc4.zip
An alternative way to solve this is to not deal with a view tag at all, but create a subclass of UITableViewCell and design your layout with this subclass. When you dequeue the cell in tableView: cellForRowAtIndexPath: or get the cell in tableView:didSelectRowAtIndexPath, use your subclass for the cell.
For example, create a subclass of UITableViewCell and give it a label as a property. If you're using xib's or storyboard, attach this label as an outlet. Then you can access that label directly as you would any other property.

UISearchBar jumps if pushed controller hides the tab bar

My UI structure is as follow:
UITabBarController (TBC) -> UINavigationController (NC) -> UITableViewController (TVC)
(for the simplicity of the example lets say the TBC has only one controller on its viewControllers array - the NC)
My TVC has UISearchBar as its table header, and when the TVC appear I hide the search bar beneath the NC navigation bar by settings the table view content offset.
When user tap a cell in the TVC another view controller is pushed (VC) and hides the tab bar with VC.hidesBottomBarWhenPushed = YES;
Now there is a very annoying behavior that I dont know how to solve:
When the user tap the back button from VC back to TVC, the search bar jumps to be visible even if it was hidden (beneath the navigation bar) before the VC was pushed.
This effect happens only if the TVC doesn't have enough rows to fill the screen, its like the search bar force itself to be visible if there is a place on screen. but its really looks bad and buggy.
I uploaded a simple project that demonstrates the problem, it has the same structure as I described in my question.
I added two bar buttons for your convenience, the "hide bar" button hides the search bar for you, and the "toggle count" button toggle the table view rows count to demonstrate that the issue happens only if there are few items.
Okay.. It looks to me like you've stumbled upon a bug. It should be reported through apples bugreporter (here).
I've made a fairy simple working work-around, but keep in mind that it is a work-around. This will work, but you might have to review it if you have/add other controls to the tableView. It should be safe to use(not acting randomly), and it's not the ugliest of work-arounds, so I think it's fine to use in a release. I've uploaded the same project with the fix here, and you can just go ahead and download it, and you'll probably understand what I've done. I'll explain (in extreme detail) what I've actually thought and done here, in case the download links dies in the future:
Train of thought:
As simalone also said, the problem is that when hidesBottomBarWhenPushed is set to YES, then it will call an additional viewDidLayoutSubviews which somehow resets your current state. We need to override viewDidLayoutSubviews, and check if we are laying out subviews because we are coming from ViewController, or if it's just a regular call. When we establish that the call is indeed because we are returning from ViewController, we need to hide the search bar (only if it was hidden before).
When we return from ViewController, three calls are made to viewDidLayoutSubviews in TableViewController. I'm guessing the first is for tableView, and it seems that the second call is 'for'(or rather from) the tabBar. This second one is the one moving the searchBar down. I have no idea what the third call is, but we can ignore it.
So now there are three things we need to check inside viewDidLayoutSubviews: We need to check if we are returning from ViewController, we need to check if the searchBar was hidden before we pushed(if it should hidden be now), and we need to check that it's the second call to this method.
First things first.
In TableViewController, I added a property #property BOOL backPush; to the header(.h)-file. Now I need to change this variable from ViewController.
In ViewController, I put this:
#import "TableViewController"
...
-(void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
if(self.isMovingFromParentViewController)
{
if([self.navigationController.topViewController isKindOfClass:[TableViewController class]])
[((TableViewController*)self.navigationController.topViewController) setBackPush:YES];
}
}
In the code above, when the view is disappearing (I.E pushing forward, back, closing, whatever), I'm checking if we are disappearing because it was removed from the parent. If it is(which it is when the back-button is called), I check if the now-current top view controller is of class TableViewController, which it also is if we go back. Then I set the property backPush to YES. That's the only thing we need in ViewController.
Now, to the TableViewController. I added a counter next to your row-count:
#interface TableViewController () {
NSInteger _rows;
int count;
}
This is to keep track of how many calls have been made to viewDidLayoutSubviews later. I set count = 0; in viewDidLoad.
Now to the magic:
-(void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if((self.backPush && count == 0 && self.tableView.contentOffset.y ==
self.tableView.tableHeaderView.frame.size.height) ||
(self.backPush && count == 1 &&
self.tableView.contentOffset.y == 0))
{
if(count == 0)
count++;
else
{
count = 0;
self.backPush = NO;
[self hideSearchBar];
}
}
else if((count == 0 || count == 1) || self.tableView.tableHeaderView.isFirstResponder)
{
count = 0;
self.backPush = NO;
}
}
The first if-statement wants either of these situations:
backPush is YES, count is 0, and searchBar is already hidden.
backPush is YES, count is 1, and searchBar is visible.
If 1. is true, then we increment count by 1.
If 2. is true, then 1. has already happened, and we now know that we are in the second round of viewDidLayout.. when we are coming back from VC AND that the searchBar WAS hidden (because 1. happened) but now isn't hidden. It probably happens in the super-method or something.
Now we can finally push the searchBar out again. I also reset count and set backPush back to NO.
The else if is also pretty important. It checks if count is 0 or 1, or if the searchBar has the keyboard showing. If count is 0 or 1 when it reaches here, it means that the first if-statement failed, e.g that the searchBar wasn't hidden, or that it was scrolled far up.
(When I think of it, the else-if should check if backPush is YES as well. Now it sets those variables repeatedly)
Let me know if you find a better way!
I think this one is simple solution. Thanks to
Sti
for giving some ideas to solve this bug.
Initialize variable var hideSearchBar = false
and inside viewDidLayoutSubviews add this code for maintain same content offset.
if hideSearchBar == true {
self.tableView.contentOffset = CGPointMake(0, self.tableView.tableHeaderView!.bounds.height - self.tableView.contentInset.top)
}
Finally implement below methods.
override func scrollViewDidScroll(scrollView: UIScrollView) {
if self.tableView.tableHeaderView!.bounds.height - self.tableView.contentInset.top == self.tableView.contentOffset.y && self.tableView.dragging == false {
hideSearchBar = true
}
else if self.tableView.dragging == true {//Reset hiding process after user dragging
hideSearchBar = false
}
}
func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if self.tableView.contentOffset.y + self.tableView.contentInset.top <= self.tableView.tableHeaderView!.bounds.height
{
self.tableView.contentOffset = CGPointMake(0, self.tableView.tableHeaderView!.bounds.height - self.tableView.contentInset.top)
}
}
Try to set for TVC
self.automaticallyAdjustsScrollViewInsets = NO
This is a problem caused by hidesBottomBarWhenPushed = YES , if you uncheck Hide Bottom Bar On Push, the searchBar will not appear when VC back to TVC.
Try this in TableViewController.m:
- (void)viewDidLayoutSubviews{
[super viewDidLayoutSubviews];
[self hideSearchBar];
}
I can't explain why but I know that if hidesBottomBarWhenPushed = YES for UITabBarController to push vc, viewDidLayoutSubviews will be called more than once when the view appears again. First time subviews keep the same position, whereas the second time be called, subviews will be adjusted for some reason to relayout with the most original position, which is very weird. Do your custom layout in viewDidLayoutSubviews will prevent this to happen even after viewDidAppear.
My solution is a little stupid.
Add this method to the sample code.
- (void)viewWillLayoutSubviews
{
[self hideSearchBar];
}
It seems the tableView will redraw the scrollView inside it.
Since the tableView reset the contentOffset, I made a custom tableView has property to save the hidden status of search bar.Below is the code.Hope it helps.
//
// TableViewController.m
// SearchBarJump
//
// Created by Eyal Cohen on 3/9/14.
// Copyright (c) 2014 Eyal. All rights reserved.
//
#import "TableViewController.h"
#interface CustomTableView : UITableView
#property (nonatomic, assign, getter = isSearchBarHidden)BOOL searchBarHidden;
#end
#implementation CustomTableView
#synthesize searchBarHidden = _searchBarHidden;
- (void)layoutSubviews
{
[super layoutSubviews];
if (self.isSearchBarHidden) {
[self hideSearchBar:NO];
}
}
- (void)setSearchBarHidden:(BOOL)searchBarHidden
{
_searchBarHidden = searchBarHidden;
if (_searchBarHidden && self.contentOffset.y != self.tableHeaderView.frame.size.height) {
[self hideSearchBar:YES];
}
}
- (void)hideSearchBar:(BOOL)animated {
// hide search bar
[self setContentOffset:CGPointMake(0.0, self.tableHeaderView.frame.size.height) animated:animated];
}
#end
#interface TableViewController () {
NSInteger _rows;
}
#property (nonatomic, weak) IBOutlet CustomTableView *mainTable;
#end
#implementation TableViewController
#synthesize mainTable = _mainTable;
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.view = _mainTable;
[_mainTable setDelegate:self];
[_mainTable setDataSource:self];
_rows = 3;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.mainTable setSearchBarHidden:YES];
}
- (void)hideSearchBar {
// hide search bar
[_mainTable setContentOffset:CGPointMake(0.0, self.tableView.tableHeaderView.frame.size.height) animated:NO];
}
- (IBAction)toggleCount:(UIBarButtonItem *)sender {
if (_rows == 20) {
_rows = 3;
} else {
_rows = 20;
}
[_mainTable reloadData];
}
- (IBAction)hideBar:(UIBarButtonItem *)sender {
[self hideSearchBar];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return _rows;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Configure the cell...
cell.textLabel.text = #"cell";
return cell;
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
[_mainTable setSearchBarHidden:NO];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
if (_mainTable.contentOffset.y == _mainTable.tableHeaderView.bounds.size.height) {
[_mainTable setSearchBarHidden:YES];
}
}
#end
UITableViewController always modifies its UITableviews content offset in its viewDidAppear to make sure that its all rows are visible. So your hacky methods don't work here.
There are several solution to this problem. The one I selected is shown below
First delete that searchBar from your storyboard.
#interface TableViewController () {
NSInteger _rows;
}
#end
#implementation TableViewController
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
_rows = 4; // +1 for searchBar
}
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
}
- (void)hideSearchBar {
// hide search bar
[[self tableView] scrollToRowAtIndexPath:[NSIndexPath indexPathWithIndex:1] atScrollPosition:UITableViewScrollPositionTop animated:NO];
}
- (IBAction)toggleCount:(UIBarButtonItem *)sender {
if (_rows == 20) {
_rows = 4;
} else {
_rows = 20;
}
[self.tableView reloadData];
}
- (IBAction)hideBar:(UIBarButtonItem *)sender {
[self hideSearchBar];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return _rows;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.row == 0){
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, tableView.frame.size.width,44)];
[cell addSubview:searchBar];
return cell;
}
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Configure the cell...
cell.textLabel.text = #"cell";
return cell;
}
#end
The above solution just ensures that automatic scrolling magic is disabled.
If you want your default searchBar to be hidden override UITableView and call hideSearchBar when tableview is initially loaded for the first time.
I fix the bug just like this:
#interface NTTableView : UITableView
#end
#implementation NTTableView
-(void)setContentOffset:(CGPoint)contentOffset{
if (self.contentOffset.y==-20&&
contentOffset.y==-64) {
NSLog(#"iOS7 bug here, FML");
}else{
[super setContentOffset:contentOffset];
}
}
#end
Fix for my somewhat similar situation with a UISearchBar as the tableHeaderView. Not sure if this falls into the same exact scenario, but it hides the search bar when the view appears. (Being unconcerned with the amount of rows in the table view)
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO];
}
Setting edgesForExtendedLayout to [.top, .bottom] instead of just .top on TVC fixed problem for me.
Of course, automaticallyAdjustsScrollViewInsets is set to false
EDIT: seems that it only works if tvc.tabBar is translucent
As a weird hack I can only suggest to add an empty cell to the end of cells with height about 400
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return _rows + 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
if(indexPath.row == _rows)
{
//cellEmpty - cell identifier in storyboard
cell = [tableView dequeueReusableCellWithIdentifier:#"cellEmpty" forIndexPath:indexPath];
}
else
{
cell.textLabel.text = #"cell";
}
// Configure the cell...
return cell;
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.row == _rows)
{
return 400;
}
else
{
return 44;
}
}
your output file
https://github.com/iDevAndroid/SearchBarJump
simply use this code don't make do complex for that
-(void)viewDidDisappear:(BOOL)animated{
[self.tableView setContentInset:UIEdgeInsetsMake(-0.3, 0, 0, 0)];
[super viewDidAppear:animated];
}
here is one problem if you are set UIEdgeInsetsMake(0, 0, 0, 0) the searchBar jumping as in original mode

UISwitch and viewWithTag is nil in numberOfRowsInSection

I am trying to figure out the best approach for adding and managing a UISwitch to my UITableView's headers, and I've read several links so far on how best to use the UISwitch in a table: here, here and here. These links demonstrate using the UISwitch in the cell whereas I'm using them in a custom UIView for the header. That said, I would prefer to use tags to manage these objects but I'm not able to figure out why the viewWithTag approach is not working.
Side note: I was able to put the UISwitches into an NSMutableArray at run time and manage them that way, but I'd rather not be so verbose, manage bounds violations/indexes on the array or check for nil lists, etc... It is also not clear to me how I would do this using IBOutlets. This is why I'm trying the tag approach.
My goal is to use the switches to collapse / expand the rows in each of the sections which is why I've thought to tag and add UISwitches as sub views to the UIView that I return in viewForHeaderInSection. Then re-reference them later when I need to perform some logic to collapse the cells. Additionally, at run time I could have 1 or more sections so hard-coding the tag numbers is impractical. Here's the code for that method:
Assuming:
#define kSwitchTagRange 2000
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.tableView.bounds.size.width, 44)];
// Add UISwitches and ensure that UISwitch doesn't already exist for this section...
UISwitch *switchView = (UISwitch*)[self.tableView viewWithTag:(kLayerSwitchTagRange + section)];
if (switchView == nil) {
// Create switch
switchView = [[UISwitch alloc] init];
double xOffset = self.tableView.bounds.size.width - switchView.frame.size.width;
switchView.frame = CGRectMake(xOffset,
7,
switchView.frame.size.width,
switchView.frame.size.height);
[switchView addTarget:self action:#selector(switchChanged:) forControlEvents:UIControlEventValueChanged];
// Should we tag the switch view?
switchView.tag = kSwitchTagRange + section;
}
// Add switch as subview
[view addSubview:switchView];
return view;
}
In switchChanged: I just reload the table's data:
- (void)switchChanged:(id)sender {
[self.tableView reloadData];
}
Finally as the table's data is recreated I attempt to retrieve the UISwitch and determine it's on/off state, then I return 0 for the number of rows in the section if OFF and some other number if ON:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSInteger rowCount = 0;
UISwitch *switchView = (UISwitch*)[self.view viewWithTag:(kSwitchTagRange + section)];
if (switchView != nil && switchView.isOn == NO)
{
rowCount = 0;
}
else
{
// Row count is something other than 0
rowCount = 3;
}
return rowCount;
}
Unfortunately, this switchView is always nil.
I have a few guesses, but can't determine why this is happening.
Guess 1: The UIView to which the switch I want was added is deallocated when the table's data was reloaded. It doesn't exist in memory and therefore can't be found searching by tag.
Guess 2: I'm adding the UISwitch to the view object incorrectly (in many examples I've seen objects added to a UITableViewCell's contentView, however since I'm sending back a UIView in the viewForHeaderInSection: method, I'm not sure those examples apply here. addSubview should add any object to the tree.
Can anyone tell me why the viewWithTag method above would return nil? I'm leaning toward Guess #1 but haven't found any documentation that tells me the custom header UIView is deallocated at any time. The cells are re-used, but what about the section headers?
Lastly, I've read this post and while the recommendation on tag-use makes sense it seems to dislike the tag approach at all. Why not use tags if you don't find using them to be messy and they're being used intelligently? Why is the tag feature even available?
Really, I just want to know why the viewWithTag method returns nil in my case.
Cheers!
I'm not sure why your viewWithTag always returns nil, I do think it might have to do with the fact that the views are deallocated at some point.
However, the tags can still work to do what you want, but you need to have a property or key in your model object that keeps track of the value of the switch. You can use the tag to tell which section the switch is in in its action method, and update the model appropriately. Here's what I did. My model is an array of dictionaries where the value of the key "rowData" is an array with all the data for that section, and the value of the key "switchValue" is an NSNumber,0 or 1, representing the state of the switch. So, my data looks like this:
2012-11-28 22:50:03.104 TableWithSwitchesInHeaderView[3592:c07] (
{
rowData = (
One,
Two,
Three,
Four
);
switchValue = 1;
},
{
rowData = (
Red,
Orange,
Yellow,
Green,
Blue,
Violet
);
switchValue = 1;
},
)
This is what I have in the relevant table view delegate and data source methods:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.theData.count;
}
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.tableView.bounds.size.width, 44)];
UISwitch *switchView = [[UISwitch alloc] init];
double xOffset = self.tableView.bounds.size.width - switchView.frame.size.width;
switchView.frame = CGRectMake(xOffset,7,switchView.frame.size.width,switchView.frame.size.height);
[switchView addTarget:self action:#selector(switchChanged:) forControlEvents:UIControlEventValueChanged];
switchView.tag = kSwitchTagRange + section;
switchView.on = [[[self.theData objectAtIndex:switchView.tag - 101] valueForKey:#"switchValue"] boolValue];
[view addSubview:switchView];
return view;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return 50;
}
- (void)switchChanged:(UISwitch *)sender {
NSMutableDictionary *dict = [self.theData objectAtIndex:sender.tag - 101];
[dict setValue:[NSNumber numberWithBool:sender.isOn] forKey:#"switchValue"];
[self.tableView reloadData];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
BOOL switchValue = [[self.theData[section] valueForKey:#"switchValue"] boolValue];
if (switchValue) {
return [[self.theData[section] valueForKey:#"rowData"] count];
}else{
return 0;
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
cell.textLabel.text = [[self.theData[indexPath.section]valueForKey:#"rowData"]objectAtIndex:indexPath.row] ;
return cell;
}
My kSwitchTagRange was #defined as 101. This code worked to collapse the rows (actually get rid of all of them) in any section where the switch was off.
It seems from the post here that the UIView returned by viewForHeaderInSection is deallocated and must be recreated (as well as all it's subviews, including my UISwitch), rather than re-used. I trust that Apple assumes that you'll have far fewer section headers than you are cells and therefore didn't provide a reusability mechanism for retrieving section headers. The data-bound approach proposed by #rdelmar seems to make sense for this scenario.

Resources