When it comes to write a table to present and organise the application settings area I feel always a bit "confused". I think that the better way would be to go with Static table, but I find extremely hard to customise it (especially because I can't customise grouped view cell).
So I often end up with a common table with a datasource. My doubts is related with cells that should directly presents the options information.
Let's say that I need 2 kind of cells.
A cell that shows a switch
A cell that shows the current value for the option
My doubts essentially are:
Which is the best way to setup the cells? Should I create a single subclass and just show or hide the label or the switch during the cellForItemAtIndex process?
Which is the best way to interact with the cells? When user changes the value of a switch... how can I set a reference to that switch and the right cell containing it?
What about the datasource? at the moment I implement a switch and depending on the required index I set up the cell
Can you kindly share some thoughts/examples of code about how you implement you settings?
This answer is based on the things i've done in my own app(s).
I wouldn't subclass UITableViewCell unless I wanted to do something "really" custom. I would just use the standard UITableViewCell, create a UISwitch control, or a label, depending on what you want to show, and add it as the cell's accessory. For instance:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString * cellIdentifier = #"Cell";
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell)
cell = [[TSOptionCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
if (indexPath.row == 0) /* or whatever index you want to set the switch or label at */
{
UISwitch * switch = [[UISwitch alloc] initWithFrame:frame];
[switch addTarget:self action:#selector(toggleSwitch:) forControlEvents:(UIControlEventValueChanged | UIControlEventTouchDragInside)];
/* create your switch and add it's target and action (method that's called when it is toggled) */
// Set the value of the switch
switch = [[NSUserDefaults standardUserDefaults] boolForKey:#"key"];
[cell setAccessoryView:switch];
}
}
The method that you added as the "action" for the switch above gets called when the user interacts with the switch. For instance, lower down in your code somewhere, you'd have this:
-(void)toggleSwitch:(id)sender
{
/* handle user interaction somehow */
}
This is what i currently do with my app in the app store. I use a switch statement to check the indexPath.row against the index of where I want the switch to be displayed, so I think this is appropriate.
Another way is to create two prototype cells in the storyboard, with different identifiers, and the in
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
you should use each cell at the index you need.
About the second issue in cellForRowAtIndexPath you should save the current state of the switch in an NSMutableArray.
you set a tag for each cell and in
- (void)toggleSwitch:(id)sender {
NSInteger currentIndex;
currentIndex = [sender superview].tag;
[yourMutableArray replaceObjectAtIndex:currentIndex withObject:![NSNumber numberWithBool:[yourMutableArray objectAtIndex:currentIndex]BoolValue];
}
of course, in your cellForRow you set the value of the array like:
[yourMutableArray insertObject:[NSNumber numberWithBool:[mySwitch isOn] atIndex:indexPath.row];
Hope this is what you were searching
Related
I'm a little confused to why an identifer (*MyIdentifier) is always required. The code below demonstrates this. I have noticed all tableviews require at least one.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = #"MyIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MyIdentifier] autorelease];
}
What is the purpose behind having an identifier? Ive have seen a few tutorials where there is more than one. Also, reading the Apple documentation, I was a little confused about why the following is called:
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MyIdentifier] autorelease];
}
I would like to know why it takes the input of 'MyIdentifier'? Assuming we had more than one identifier, which one should we choose? To be exact, what if we had a Parent, Child and sub-child etc.
Suppose you have several customized UITableViewCells and each of them have different backgound colors. In your storyboard or xib file, you may name the cell with red background color "redCell" and the one with blue "blueCell". Then you can select what kind of cell to add to a particular row using their identifiers. Let's say, you wanna apply red cells to odd rows and blue ones to even rows then you can use the following code to do this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *redCell = [tableView dequeueReusableCellWithIdentifier:#"redCell"];
UITableViewCell *blueCell = [tableView dequeueReusableCellWithIdentifier:#"blueCell"];
if (indexPath.row % 2 == 0) return redCell;
else return blueCell;
}
Without specifying an identifier, the system wouldn't know which kind of cell to pick.
From the docs:
The identifier is a string identifying the cell object to be reused. This parameter must not be nil.
For performance reasons, a table view’s data source should generally reuse UITableViewCell objects when it assigns cells to rows in its tableView:cellForRowAtIndexPath: method. A table view maintains a queue or list of UITableViewCell objects that the data source has marked for reuse. Call this method from your data source object when asked to provide a new cell for the table view. This method dequeues an existing cell if one is available or creates a new one using the class or nib file you previously registered. If no cell is available for reuse and you did not register a class or nib file, this method returns nil.
Every time you return a cell you have 2 options - you can create new cell and return it, dequeue cell that already exists and reconfigure it.
So the way this works is when you create cell you give it reuse identifier, so when cell goes off screen it can be used instead of creating new cell. After the cell is dequeued though, you might want to change its properties(like text or image)
You can have different reuse identifiers for different cell types(different content views)
I am trying to implement rating stars by using kDRATING VIEW .i have used following code in cellForRowAtIndexpath method but it causes my app to become slower.
If it try to allocate and initialise this in viewdidload method then it when i scroll up and down ,the stars fluctuates . please help in this regard
self.rating = [[KDRatingView alloc] initWithFrame:CGRectMake(0, 0, 60,20)];
[self.rating rateKDRatingView:2.80 outOf:3.0];
[cell.rating addSubview:self.rating ];
return cell;
It sounds like you need to look into UITableViewCell reuse because when you scroll a UITableViewCell out of the screen it will call cellForRowAtIndexPath again to remake this cell when it is back in view and that can cause flickering and memory consumption.
You are adding the KDRatingView to the rating view on the cell so I guess you have a custom cell, so why not instead have the KDRatingView inside the custom cell and just update its value when you need to.
Try this solution with some cell reuse:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"RatingCell";
RatingCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(cell == nil){
// initialisation code
cell = [RatingCell new];
}
// setting code
[cell setRatingViewValue:2.8 outOf:3.0];
}
That way it only creates the cell once, so it won't slow down your app. Then each time the cell would have been recreated it just updates the cells rating (and anything else you need to set) instead.
Then you just need to implement the setRatingViewValue:outOf: method in your custom cell to update the KDRatingView that you need to have added and positioned in your custom cell upon initialisation.
I have been searching and reading all over but couldn't find any conclusive method to achieve what I want to and hope to find help here...
I have a UITableView which allows the user to add multiple Flavours and Percentages to a Recipe. I have implemented the method to add or delete rows of Flavours with a custom Cell / Nib and it works perfectly well.
The issue I'm facing now, is how to retrieve the values the user has provided per added row.
(Edit for Clarity: My problem is not the populating of data, but only the dynamic reading of all data so I can save it)
I do manage to get the values for the visible rows (I do understand how the Reuseidentifier and the Tableview works, per se that for memory management's sake, iOS only keeps track of the visible rows), but not the hidden ones.
I assume in theory that I have to create an Array of Cells outside of 'cellForRowAtIndexPath' which maintains all cells. But then I'm facing another conceptual problem that my custom Nib / cell doesn't show.... basically:
How can I then use / register a nib without using the dequeingidentifier
Or in General, how can I solve the overall problem to be able an read all user entered values per row
Here the code I'm using within my cellForRowAtIndexPath. As mentioned adding and remove cell works like a charm, that isn't the issue...
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
RecipeFlavourTableViewCell *cell;
int section = (int)indexPath.section;
if(section==0)
return [super tableView:tableView cellForRowAtIndexPath:indexPath];
if(!cell){
[tableView registerNib:[UINib nibWithNibName:#"RecipeFlavourCell" bundle:nil] forCellReuseIdentifier:#"Cell"];
cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
}
cell.selectionStyle = UITableViewCellSelectionStyleNone;
return cell;
}
I have seen some Libraries doing it (e.g. XLForm) but do not understand (also when checking their sources) how they iterate through the values and overcome this dequeuing problem...
Any help is highly appreciated
EDIT 2: here the code I'm using to iterate through the cells in order to save the data, but as said I can only iterate through the visible cells:
- (IBAction)saveRecipe:(id)sender {
NSInteger dynamicRows = [self.tableView numberOfRowsInSection:1];
for (int i=0; i<dynamicRows; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:1];
RecipeFlavourTableViewCell *cell = (RecipeFlavourTableViewCell *)[self.tableView cellForRowAtIndexPath:indexPath];
NSLog(cell.flavour.text);
}
}
After 2 days of searching I finally came up with a solid solution. In case someone bumps into the same problem of dynamic forms with a tableview, here the solution:
As we understand, what ever cell is created in cellForRowAtIndexPath, it only persists as long as it is displayed. As soon as you scroll and the cell disappears, it gets automatically thrown out of memory. This behaviour makes it impossible to iterate through all cells at a later stages.
The steps to follow in order to make it work are as follows:
Preparation
Create an NSObject with all properties you want to persist in one form cell (-> cellObject)
In the ViewDidLoad of your controller create a NSMutableArray which will contain the cellObjects (-cellsArray)
Add as many cellObjects to the cellsArray as you initially want to appear in the Tableview
In numberOfRowsInSection return the count of you cellsArray
In the cellForRowAtIndexPath build your cells as usual BUT add a Textfield Delegate (self) to every Textfield in a cell
TextField Delegate
Implement:
- (void)textFieldDidEndEditing:(UITextField *)textField
and update your cellsArray Objects every time a Textfield ends editing. Per se, get the cellObject for the row and edit the properties with the value of the TextField
Add Row
When ever you add a row, just add an empty cellObject to your cellsArray and use the beginUpdates / insertRowsAtIndexPaths / endUpdates on your tableView (NOT reloadData as the already typed in data would get lost). Also add the following at the very beginning of your addRow method, as you want to make sure that if the user adds a row while editing a textfield, the latter gets persisted as well:
[self.view.window endEditing: YES];
Remove Row
Same as Add Row just reverse, remove the cellObject from your cellsArray and use deleteRowsAtIndexPaths on your tableView
Save Data
Now comes the trick: since you ought to always persist your data when a field ends editing mode, there is one case you need to cover: What if the user pushes "Save" when the focus is set on one TextField? Well at the very beginning of your Save Action insert the following:
[self.view.window endEditing: YES];
This make sure the the textFieldEndEditing will be triggered one last time for the current textField and that its data will also be persisted.
Finally iterate through your cellsArray and do whatever you want with it (validate, save etc)...
That's it, hope this can help anyone else as I couldn't find any valuable explanation anywhere else...
Lets assume that you have an NSArray and that it contains data you want to show. Your code should look something like this:
// Add this property to the class and fill it in with data you want to show
#property NSArray flavourElements;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
RecipeFlavourTableViewCell *cell;
int section = (int)indexPath.section;
if(section==0)
return [super tableView:tableView cellForRowAtIndexPath:indexPath];
if(!cell){
[tableView registerNib:[UINib nibWithNibName:#"RecipeFlavourCell" bundle:nil] forCellReuseIdentifier:#"Cell"];
cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
// At this point your cell is ready for showing
// And you can change values in it by getting element from array that contains data
cell.flavorTextField = flavourElements[indexPath.row].flavour
cell.precentageTextField = flavourElements[indexPath.row].precentage
}
cell.selectionStyle = UITableViewCellSelectionStyleNone;
return cell;
}
I wrote a code for accessing elements without knowing what you actually have, so you will need to adjust it a little bit to fit your app.
Apple's iOS TableView and cell reuse is killing me. I searched and searched and studied, but can't find good docs or good answers. The problem is that when the TableView reuses cells things like Checkmarks (cell accessory) set on a selected Cell are repeated in the cells further down in the table view. I understand that cell reuse is by design, due to memory constraints, but if you have a list with say 50 items, and it starts setting extra checkmarks where they're not wanted, this makes whole endeavor useless.
All I want to do is set a checkmark on a cell I've selected. I've tried this using my own custom cell class, and standard cells generated by a boiler plate TableView class, but it always ends up the same.
Apple even have an example project called TouchCell you can download from the dev center, that is supposed to show a different way of setting a checkmark using a custom cell with an image control on the left. The project uses a dictionary object for a data source instead of a muteable array, so for each item there is a string value and bool checked value. This bool checked value is supposed to set the checkmark so it can track selected items. This sample project also displays this goofy behavior as soon as you populate the TableView with 15+ cells. The reuse of cells starts setting unwanted check marks.
I've even tried experimenting with using a truely unique Cell Identifier for each cell. So instead of each cell having something like #"Acell" I used a static int, cast to a string so the cells got #"cell1", #"cell2" etc. During testing though, I could see that hundreds of new cells where generated during scrolling, even if the table only had 30 items.
It did fix the checkmark repeat problem, but I suspect the memory usage was going way too high.
It's as though the cells that are not currently in the viewable area of the table are created all over again when they are scrolled back into view.
Has anyone come up with an elegant solution to this irritating behavior?
cell reusing can be tricky but you have to keep 2 things in mind:
Use one identifier for one type of cell - Using multiple identifiers is really only needed when you use different UITableViewCell-subclasses in one table view and you have to rely on their different behaviour for different cells
The cell you reuse can be in any state, which means you have to configure every aspect of the cell again - especially checkmars / images / text / accessoryViews / accessoryTypes and more
What you need to do is to create a storage for your checkmark states - a simple array containing bools (or NSArray containing boolean NSNumber objects respectively) should do it. Then when you have to create/reuse a cell use following logic:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *reuseIdentifier = #"MyCellType";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier];
if(cell == nil) {
/* create cell here */
}
// Configure cell now
cell.textLabel.text = #"Cell text"; // load from datasource
if([[stateArray objectAtIndex:indexPath.row] boolValue]) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
} else {
cell.accessoryType = UITableViewCellAccessoryNone;
}
return cell;
}
then you will have to react on taps:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[stateArray replaceObjectAtIndex:indexPath.row withObject:[NSNumber numberWithBool:![[stateArray objectAtIndex:indexPath.row] boolValue]]];
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
Just remember to use NSMutableArray for your data store ;)
I have a UITableViewController with UITextfield inside the tableview cells. If I scroll the table view, the user entered data in the textfields disappears. I tried to add the textfield data to a NSMutableArray but it still didn't work. Any help please.
When cellForRowAtIndexPath: is called, the cell you return has to be completely filled in with whatever data you want to show. So, if the cell includes a UITextfield, you'll need to set it's text property to the right value for that row in your data.
When a table cell disappears off the top or bottom of the screen, the UITableViewCell itself becomes available for re-use. (As you scroll, cells disappear, and new cells appear, but the UITableView class is re-using the UITableViewCell objects.) In cellForRowAtIndexPath: when you get a cached cell to use, you have to be sure to setup everything you want it to show for the row in question, otherwise you might see some odd behavior in your table.
Does this help?
EDIT:
Here's an example of the typical pattern used in cellForRowAtIndexPath:. Notice the use of dequeueReusableCellWithIdentifier:. That method returns a previously allocated but not in use UITableViewCell, if there is one. Notice further that if no cached cell is returned, the code creates a new one, and sets it up (with stuff that is independent of anything that might be row specific). Following that, you'd setup the cell as you need it for the row in question.
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *SearchResultsCellIdentifier = #"SearchResultsCellIdentifer";
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:SearchResultsCellIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:SearchResultsCellIdentifier] autorelease];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
// Your row-specific setup of the cell here
// ...
return cell;
}
Check the docs for specifics about these methods. There are LOTS of examples from Apple and elsewhere about how to implement tableViews.