I have a UITableView with searchBar and searchDisplayController. What I wanted to do was to show a button when no results were found. So user could search server's database. I have a NSMutableArrayto store searchResults. So my delegate method looks like this:
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope
{
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"titulo contains [cd] %#", searchText];
NSArray *filtroUsuario = [self.objects filteredArrayUsingPredicate:predicate];
self.searchResults = [[NSMutableArray alloc] initWithArray:filtroUsuario];
if (self.searchResults.count < 1) {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeRoundedRect];
btn.frame = CGRectMake(0, 0, 320, 50);
btn.showsTouchWhenHighlighted = YES;
[btn setTitle:#"Procurar no Banco de Dados" forState:UIControlStateNormal];
btn.tag = 1;
[self.searchResults addObject:btn];
self.adicionar = YES;
}
}
Basically, when there are no results, I create and add a buttonto my results array. Then, in cellForRowAtIndexPath, I have the following:
if (tableView == self.searchDisplayController.searchResultsTableView) {
if (self.adicionar == YES) {
cell.textLabel.text = #"";
[cell.contentView addSubview:self.searchResults[indexPath.row]];
self.adicionar = NO;
}
This shows the buttonexactly the way I want, and when cancelButtonis pressed, or the buttonin question, I just remove it from searchResultsin case user searches again. [self.searchResults removeAllObjects].
The problem was that, since I'm reusing cells, the subview was still there when user searched again. I had a few options to deal with this, I could create a property for the cell and remove subview when buttonwas pressed. But I opted to include the line [[cell.contentView viewWithTag:1] removeFromSuperview];at the beginning of cellForRowAtIndexPath, so when it's called again, it deletes the subViewsbefore continue.
Everything works perfectly now. My question is if this is the best approach or if there's something more simple. Since my app is a complex app, I'm very concerned about memory and performance, besides, I would really like to learn the coolest techniques available.
Any thoughts?
take a look at -prepareForReuse on the UITableViewCell. Probably the best fit for the exact situation you describe. (sketched example below)
#interface CustomCellName : UITableViewCell
#property (strong, nonatomic) UIButton *someButton;
#end
#implementation
- (void)prepareForReuse
{
_someButton.hidden = YES;
//etc...
}
#end
Tags are also perfectly fine, although many people find that approach to be unsatisfying. (*citation needed)
An alternate approach is to subclass UITableViewCell with a UIButton as a property which you can tweak from the tableView by -cellForRowAtIndexPath.
Best is a flexible term - best in what regard...
What you have works, but it isn't a good allocation of responsibility. It would be better to create a custom cell subclass and have it provide an API where a custom button can be added. The cell subclass would handle cleanup of the button in prepareForReuse.
From a memory and performance point of view there is little difference. But using a cell subclass is more correct.
For performance, it's better not to create and destroy button instances. So, it would be better for your cell subclass to create a button but keep it hidden until it's needed, then show it. Now, prepareForReuse would simply hide the button. Generally this would use a little more memory on average - it's a trade off...
First thing, mixing controls with your app data is not a good practice. You can make it work as you already have, but in long term trie to separate UI from application data. Table view should do all the work of presenting data and controls that manipulate your data so it is probably better idea to add string "No Data" to your result array and then handle it in cellForRowAtIndexPath. Create a button on your cell and just hide/show it when you need it. Don't worry about memory consumption, the cells are reusable so you will have very small number of buttons (hidden ones).
Second thing if you really wont to create only one button then tag it, for example, when you decide you need it create it and tag it with say 1000. Then later when you reload and don't want to show it, just ask your cell if it has a view with tag == 1000 and remove it from superview. But again it is an overkill and your memory integrity wont be endangered if you create one button for every visible cell.
Related
I have get tableview textfield value on click on button , I lots of try but i can't able to get value Please help me here is my code its always print null value, this for loop run on
Click On button but this code is not working in ios 7 simulater but its working with ios 8 Please help me
for (int k=0; k< aryModifier.count; k++) {
// NSLog(#"--%d",i);
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:k inSection:0];
UITableViewCell *cell = [[self tbl_itemModifier] cellForRowAtIndexPath:indexPath];
UITextField *getTextView = (UITextField*)[cell.contentView viewWithTag:200];
//for (UIView *subView in cell.subviews)
for (UIView *subView in cell.contentView.subviews)
{
if ([subView isKindOfClass:[UITextField class]])
{
UITextField *txtField = (UITextField *)subView;
NSLog(#"%#",txtField.text);
}
}
//UITextField *getTextView = (UITextField*)[cell.contentView viewWithTag:i];
NSLog(#"%#",getTextView.text);
}
and Table Cellforrow at index path
UITextField *txt=[[UITextField alloc]initWithFrame:CGRectMake(180, 40, 50, 15)];
txt.text = #"1";
txt.textAlignment = NSTextAlignmentCenter;
txt.font=[UIFont systemFontOfSize:12];
txt.textColor = [UIColor blackColor];
txt.tag = 200;//indexPath.row;
txt.delegate = self;
NSString *str = txt.text;
[cell addSubview:txt];
I try both way but i cant get success . Please help me .........
//for (UIView *subView in cell.subviews)
for (UIView *subView in cell.contentView.subviews)
Please give me solution ...
if you have any demo link please share with me
There are several issues with the approach you're using:
Creating a new control in tableView:cellForRowAtIndexPath: is bad practice. Proper MVC would dictate moving that creation to your UITableViewCell's init or initWithCoder: method. Doing it in the tableView:cellForRowAtIndexPath: method is extremely likely to result in multiple controls being added to a cell as that cell is recycled. That method is really intended only for cell configuration, not view hierarchy creation/management.
Iterating UITableViewCell objects is a bad idea. The reason is that iOS aggressively manages memory consumption when creating UITableViewCell instances for display. It recycles cells as often as it can. Therefore, if you have a table view with many cells, only a small portion of them will be available at any given time. You should be using an external data source to provide the data that the table view uses to populate its cells. You should also be using this to grab the data you need, not relying on controls in a UITableViewCell for that data.
A few questions for you:
- How are you currently populating your cells?
- Is there other data that is displayed in the cells, or are they just a scrolling set of UITextField instances?
- Is this basically just a scrollable form?
Assuming (based on the limited evidence provided) that you're trying to implement a form, here are my suggestions:
Don't use a UITableView. I know it's tempting because it handles the keyboard show/hide well and will (sometimes) provide auto-scrolling to the selected control, but it's really trying to fit a square peg in a round hole. It's not what a UITableView is made to do. Instead, learn how to correctly implement forms in a UIScrollView. In particular, this become much easier for me when I embraced a "full auto layout implementation" methodology. It's a little bit more complicated to set up initially, but it works much better and makes tasks like yours a lot more logical.
If you must use a UITableView, figure out a way to persist data on-the-fly to a separate data source that is used to populate your UITableView. That way, as cells are recycled, you can refresh them with correct data, and when you need to access the entered data, you have no need to iterate UITableViewCell instances (which is fairly "expensive" to do).
Change below line of code:
UITextField *getTextView = (UITextField*)[cell.contentView viewWithTag:200];
to
UITextField *getTextView = (UITextField*)[cell viewWithTag:200];
Hope this helps..
I have a simple application whereby a UITableView is populated by a user tapping on an item in the UINavigationBar and then getting taken to another view to fill in UITextFields and UITextView. When the user clicks save, the information is saved to Core Data and then represented in this UITableView with the use of NSFetchedResultsController.
One of the attributes is a "Notes" attribute on a "Transaction" Entity. Filling in the Notes in the UITextView is completely optional, but if the user adds in a note, I want to show a custom image that I've created on the cell for the entry that has the note.
When the app is run in this version alone (so deleted and installed with this developer release), it works very well and the cells show the notes only for the cells that have the notes. However, when updating from a previous version of the app, this is where the problem occurs.
The previous version of the app didn't have a Notes attribute and so I used CoreData lightweight migration to set up a new model with a Notes attribute. That part works.
The Problem
Because of the reusing of cells, I'm noticing that when I've updated from an old version to this new version, none of the cells have the custom image and that's good because the notes doesn't exist. However, if I go in and add a new entry with a note and then scroll through my UITableView, I notice the cells start showing the custom image randomly, based on scrolling. So it disappears from one cell and shows up on another. This is a big mis-representation for my users and I'm not quite sure what to do to fix this.
In my cellForRow I have the following code:
self.pin = [[UIImageView alloc] initWithFrame:CGRectMake(13, 30, 24, 25)];
self.pin.image = [UIImage imageNamed:#"pin"];
self.pin.tag = 777;
if (!transaction.notes) {
dispatch_async (dispatch_get_main_queue (), ^{
[[customCell viewWithTag:777] removeFromSuperview];
});
}
if ([transaction.notes isEqualToString:#""] || ([transaction.notes isEqualToString:#" "] || ([transaction.notes isEqualToString:#" "] || ([transaction.notes isEqualToString:#" "]))))
{
[[customCell viewWithTag:777] removeFromSuperview];
}
else
{
[customCell addSubview:self.pin];
}
So the first if statement is to check whether the notes exist and that returns true when updating from an old version of an app to this version. The second if statement just checks if the value of the notes is equal to a few spaces and if so, then to remove the note.
I just can't figure out what's going on here.
Possible Solution
In the same UITableView cell, I also have a green/red dot UIImageView which is displayed depending on whether the user selected a Given or Received Status when adding a new entry. With this in mind, one image is ALWAYS displayed, whether it's the green or red dot. So what I'm thinking about here is creating a transparent square and just changing the if statement to say "If note, show pin image and if not, show transparent image".
That feels a bit like a hack though and I'd prefer a proper way to fix this.
Any thoughts on this would really be appreciated.
First of all, bad practice to allocate views in cellForRow. If you really need to allocate views in cellForRow do it just when it's needed, in your case in the else statement.
Second, do not use dispatch_async to dispatch on main thread if you are already on main thread (cellForRow it's on main thread).
The above points are just some suggestions for performance improvement.
As a solution of your problem, I would create a custom UITableViewCell and in it's method prepareForReuse I would remove the imageView.
EDIT
YourCustomCell.m
- (void)prepareForReuse {
[super prepareForReuse];
[[self viewWithTag:YOUR_TAG] removeFromSuperview];
}
This is a straightforward implementation, but you have to take in consideration that is more expensive to alloc/dealloc the UIImageView than keep a reference to the image and hide it when you don't need it. Something like:
YourCustomCell.h
#interface YourCustomCell : UITableViewCell {
IBOutlet UIImageView *theImageView; // make sure you link the outlet properly ;)
}
YourCustomCell.m
- (void)prepareForReuse {
[super prepareForReuse];
theImageView.hidden = YES;
}
And in cellForRow you just have to check if you have notes and make the imageView visible (probably you will make theImageView a property)
Because table view cells are reused you must have a default value for your image. So for example set your image to transparent by default and change it under some condition. This will stop your image being shown in reused cells.
Why do you have a dispatch_async here?
if (!transaction.notes) {
dispatch_async (dispatch_get_main_queue (), ^{
[[customCell viewWithTag:777] removeFromSuperview];
});
}
Because you cannot be sure when the function inside it will execute. Suppose that transaction.notes is nil. All the isEqualToString functions will return false and the else condition of addSubView will be called. But sometime after this function is exited the code inside dispatch_async will be run and remove the pin view. I'm not whether this is the intended behavior.
I'm trying to find the best way to save how long users of my app have looked at each UITableViewCell, for optimization and metrics purposes. I haven't found a tool (Parse, AppSee, etc) that can monitor that, so I'm doing it manually. Problem is, it's incredibly not efficient.
- (void)scrollViewDidScroll:(UIScrollView *)aScrollView
{
UITableView *tableView = self.tableView; // Or however you get your table view
NSArray *paths = [tableView indexPathsForVisibleRows];
// For getting the cells themselves
NSMutableSet *visibleCells = [[NSMutableSet alloc] init];
for (NSIndexPath *path in paths) {
NSLog(#"visible: %i", path.row);
[visibleCells addObject:[tableView cellForRowAtIndexPath:path]];
}
}
I will then start a NSTimer as soon as is't visible and stop it when it's not visible anymore.
This has number of flaws:
When the tableview is presented, the user can look at the first cell without scrolling - thus this code isn't called.
This code is called tens of time a second - there must be a better way
This code is called when the user is looking at, for example, the second cell, but the first cell is still a few pixels visible. So there should be a condition where the call has to be at least 20% visible to make the timer active.
If the user taps another tab bar while looking at cells, this code isn't aware and keeps counting.
My solution is obviously not optimal. Have a better way? Your solution is welcome
Use the table view delegate methods and the view controller display methods. Specifically:
tableView:willDisplayCell:forRowAtIndexPath:
tableView:didEndDisplayingCell:forRowAtIndexPath:
viewDidDisappear:
I have a UITableViewController with prototype cells containing UITextFields. To configure these custome cells, I've created a UITableViewCell subclass. I've conected the textField to the cell subclass via an outlet (nonatomic, weak).
On this subclass I've created a protocol for which the UITableViewController is its delegate so that everytime something changes in these textFields, the TableViewController knows about it. Basically I wanted this to save the values on the NSUserDefaults
Besides, in order to dynamically obtain values from these textFields, I can do something like this:
((TextFieldCell*)[self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]]).textField.text
It works ok most of the times. However when the textField is outside of the view because it has scrolled, the vaulue I get from textField.text is (null). As soon as it gets in the view again, everything goes back to normal.
I tried to change the outlet from weak to strong but to no avail.
I guess I could define some private NSStrings on the class, and fill them out when the delegate protocol gets called. The thing is that I wanted to get my code as generic as possible, keeping the need for private variables as low as possible, mostly to simplify the cell generation code.
Is there any other way to get the values of the textFields when they are outside of the view?
Thanks in advance!
But you know that UITableView only keeps Cells for the visible rect?
When a cell leaves the screen, and a new cell is needed for another cell moving into the visible area, the old cell is reused for the new content.
So there is not one cell for each row of your table view.
And if your table contains a lot data, there are far more rows than cells.
As Thyraz said, the UITableView only keeps cells for the visible rect -- and a reasonable buffer to allow for scrolling. Thats why 'reuse identifiers' are so very important, they indicate which cells can be used for which tables (critical when you have more than one table to worry about). Unfortunately, that doesn't answer your question by itself.
The responsibility for storing the contents of those textViews isn't on the UITableView's shoulders. It's your job to provide that data through the data source delegate protocols, and therefore you should be querying the data source for that information.
Edit: Which means that yes, you should be storing this data somewhere else, usually in the form of properties on the view controller class that contains the table view. I'd recommend the use of NSArray for the purpose, but you can also do it through dicts or even, at the last resort (and this is more a in theory you can do this, but it's an incredibly bad idea kind of thing), a series of properties. Personally, I almost always use NSArrays because they're structured in a manner appropriate to the problem, but you could theoretically do it other ways. (I've used a dict based structure exactly once, and that was a situation where my data was nested inside itself in a recursive structure)
UITableViewController doesn't keep cells around once off the screen. You can use the following pattern to get a previously used one as a memory management optimization, but you MUST assume that cells need to have the values reset on them every time they come onto the screen (even if dequeued) because there is no guarantee what the values will be.
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier1 = #"Cell1";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier2];
if( cell == nil ) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier1] autorelease];
cell2.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell2.editingAccessoryType = UITableViewCellAccessoryNone;
}
switch( indexPath.section ) {
case first_Section:
if( row == 0 ) {
cell1.textLabel.text = #"Some Text";
cell1.accessoryView = [self myCustomViewControl];
cell = cell1;
}
... etc
}
}
In my app I have a table view with 12 types of custom cells.
I designed each within the storyboard.
In general everything works fine - thanks to your tutorials :)
My problem is, that I have individual styles an additional data in each cell - therefore I don't want them to be reused.
How can I generate a individual custom cell from storyboard to provide it in the -
tableView:cellForRowAtIndexPath:indexPath?
Any suggestions?
Thanks
Dominic
Added:
newCell = [tableView dequeueReusableCellWithIdentifier:cellType];
cell = newCell;
[cell styleWithElement:element andResubItem:resubItem];
my problem is, I need another way to create a custom-styled cell from the storyboard than the quouted above - I have no XIB and the above way is the only way I know to create my cell.
Could it be a solution to create a celltype once the above way and then copy the cell? Is there a way to copy a cell-object?
You can generate a random cell ID each time so it won't be reused.
However, that would not be very good for scroll performance and memory consumption, so consider actually reusing the cells (i.e. come up with a way to replace data in them).
Edit:
you can always just copy the views from one to another, something along these lines:
UITableViewCell* myCell = [UITableViewCell alloc] initWithStyle:YOUR_CELL_STYLE reuseIdentifier:YOUR_RANDOM_ID];
for (UIView *view in newCell.subviews) {
[view removeFromSuperview];
[myCell addSubview: view];
}
You may also need to adjust the frame of myCell to be the same as that of newCell, and if newCell is an action target for any events (e.g. clicking a control element within the cell triggers some action defined in the cell class) you'll need to reassign those to the myCell, too.
Edit2:
To move actions you could do something like this:
NSSet* targets = [control allTargets];
for(id target in targets) {
NSArray* actions = [control actionsForTarget:target forControlEvent:UIControlEventTouchUpInside];
for(NSString* selectorName in actions) {
[newControl addTarget:target action:NSSelectorFromString(selName) forControlEvents:UIControlEventTouchUpInside];
}
}
Substitute your new cell for the target - and make sure it implements all the necessary selectors (or you can put a new selector). The code above will replace targets for UIControlEventTouchUpInside - you can use the ones you need instead (or use allControlEvents to enumerate the ones used by a control).