This will be really useful for me since I have been struggling with this issue for some time. Every time I am trying to create a custom UITableViewCell and use it in a UITableView, things get mixed up. I believe it is because of the function I use to fill the cell with, in this example it's name is fillPlayerCellWithPlayer:.
I have a NSArray of PlayerContacts as my DataSource of the UITableView. My idea is to fill the cell's contents like UILabels and UIImageViews and etc with each PlayerContact properties. The usual practice is to fill these properties right in cellForRowAtIndexPath like: tablecell.playerNameLabel.text = player.playerName and so on. But I implement it like as follows for convenience and reasons I will tell later down below:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellIdentifier = #"PlayerCellID";
AddPlayerObjectCell *tablecell = (AddPlayerObjectCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
NSArray * chosenPlayerArray = [self generatePlayersArrayForCategoryRow:selectedCategoryIndexRow];
NSMutableDictionary * dict = (NSMutableDictionary *)[chosenPlayerArray objectAtIndex:indexPath.row];
PlayerContact * player = (PlayerContact *)[dict objectForKey:#"sgfPlayer"];
[tablecell fillPlayerCellWithPlayer:player];
return tablecell;}
To explain why I don't use the common practice method, first let me share the fillPlayerCellWithPlayer function:
- (void) fillPlayerCellWithPlayer : (PlayerContact *) player {
[self createGestureRegocnizer];
myPlayer = player; //this sets the player as the private variable of AddPlayerObjectCell class
cellItemLabel.text = myPlayer.playerName;
cellItemSubLabel.text = myPlayer.playerTeam;
UIImage * img = [UIImage imageNamed:#"messi.JPG"];
cellItemImageView.image = img; }
- (void) createGestureRegocnizer{
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTapFrom)];
gesture.delegate = self;
[self addGestureRecognizer:gesture];}
- (void) handleTapFrom{
NSString * imageName = #"unselected_user_icon.png";
NSString * imageNameSelected = #"selected_user_icon.png";
if(playerSelected){
playerSelected = NO;
myPlayer.playerSelected = playerSelected;
[cellItemSelectionImageView setImage:[UIImage imageNamed:imageName]];}
else{
playerSelected = YES;
myPlayer.playerSelected = playerSelected;
[cellItemSelectionImageView setImage:[UIImage imageNamed:imageNameSelected]];}}
As You can see, when the user touches the cell, handleTapFrom fires and i can change the playerSelected property of myPlayer so i can later on use this info to filter my main array for selected players.
One of my problems is that whenever i slide my finger in the tableview of players, their selected checkmarks gets changed like crazy. I guess this is caused by my using reusable cells logic in a wrong way. If you guys can suggest me a way to this properly, i would be really happy.
And my second question is more important for me since, if you guys say its true, I want to use it as a general logic throughout my app design. Lets say i passed a NSArray named playersArray to the tableview as datasource. And the cell class changed the playersArray object that corresponds to its indexPath. In my controller class, and the classes that can access playersArray, this change is observed. This is good news for me because i don't need to make a copy of array and change its contents accordingly. Do you friends think this is a good practice for a UITableViewCell to change the contents of its UITableView's DataSource? or is UITableViewCell's only job to display a cell as needed? Should I implement this some other way? like delegation or something if I want to update the datasource?
Thank you very much in advance. I can be more clear in anyway if you guys need me to.
Aytunç İşseven
In general you should avoid that the Cell is holding the data model itself and manipulating them. You also should avoid creating & adding this gesture recogniser multiple times (as that will happen if you call fillPlayerCellWithPlayer: and the cell is reused). That will increase the count of the tap recognisers and each of the will trigger the a tap. Instead you could override
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
on the UITableViewCell and handle your selection there. This will be triggered each time you change the selection state of the UITableViewCell. You should only change the visual appearance of the cell here as the cell should not hold the player object.
To handle the selection of the player just implement the UITableViewDelegate method (on your controller)
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
and handle the manipulation of your data model there.
The reason why is:
The UITableViewCell is a view and following the Model-View-Controller Pattern (which Appkit is based on) the view should just represent the model without managing it. The controller (which implements the UITableViewDelegate - e.g. a UIViewController) is the connector between those two and should be the one managing the model information instead of the view as you suggested.
Related
I have an app, in which I have 7 different UITableViewControllers. All 7 are linked through a tabBarController. I am looking for a way to have a single custom class to be used throughout all 7 UITableViewControllers. I have 7 different arrays that all hold a specific number of objects. I need to know how to:
Change the number of rows in the tableView, depending on the array that I'm using as my data source.
Change the array that is being used as the data source based on which ViewController the user is currently looking at (Can this even be done?)
Change the contents of a cell, based on the array being used as the data source.
I'm familiar with using UITableView with a single data source, but I really don't know how to approach it with multiple data sources.
Thanks!
You can have one class be the dataSource for all of the UITableViewControllers
You might implement this by creating a custom subclass of UITabBarController which keeps an array of UITableViewControllers and a corresponding dictionary that maps a UITableVC to the array used by it's data source.
Set that as the data source for all the UITableViews and then handle each dataSource method like my example below.
Take a look at the UITableViewDataSource docs.
All of the methods pass in which tableView they're trying to get information about.
For example:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
//Here you could compare the tableView var to your array of tableViews to figure out which table view called this
//Based on that you could query your dictionary to find the array that houses the data for that tableView.
//Use the indexPath to find the data that you need to create and return the right cell
}
• Change the number of rows in the tableView, depending on the array
that I'm using as my data source.
You can accomplish this by conditions in tableView delegates
- (NSInteger)tableView:tableView numberOfRowsInSection:section
Inside this delegate you need to identify which dataSource for the particular tableView.
Check the table if its the one being refreshed like so:
- (NSInteger)tableView:tableView numberOfRowsInSection:section
{
if (tableView == self.firstTableView)
return self.firstTableDataSource.count;
if (tableView == self.secondTableView)
return self.secondTableDataSource.count;
//and so on..
}
• Change the array that is being used as the data source based on
which ViewController the user is currently looking at (Can this even
be done?)
Figuring which array you will be using for that particular table is up to you. You can use segement control, buttons, another table, it's up to you.
But the very important part is [tableView reloadData]; at your target table (table that is currently active) and again table delegates will be triggered and you will be doing all the filtering inside those delegates..
while you can check if the viewController is visible by:
if ([self/*viewController*/ isViewLoaded] && self/*viewController*/.view.window)
{
//visible
}
which was already discussed here
• Change the contents of a cell, based on the array being used as the
data source.
This one is not clear.
Is it just the content/values of the subviews of the cell like: cell.textLabel, cell.detailTextLabel and cell.imageView?
or the cell.contentView which is basically, you want to change the look of your cell?
If content/values again you just have to determine which is which, like this (using customCell):
assuming you have a dataSource that looks like:
{
data_source = (
{
text_label = test0;
detail_label = "this is just a text";
image_name = "your_image0.png";
},
{
text_label = test1;
detail_label = "this is just a another text";
image_name = "your_image1.png";
}
)
}
then in the delegate cellForRowAtIndexPath it'll be something like:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellID = #"tableID";
self.customCell = [tableView dequeueReusableCellWithIdentifier:cellID];
if (!self.customCell)
self.customCell = [[YourCustomCell alloc] initWithStyle:(UITableViewCellStyle) reuseIdentifier:cellID];
static NSString *dataSource = #"data_source";
static NSString *textLabel = #"text_label";
static NSString *detailLabel = #"detail_label";
static NSString *imageName = #"image_name";
if (tableView == self.firstTableView)
{
self.customCell.textLabel.text = [self.firstDataSource valueForKey:dataSource][indexPath.row][textLabel];
self.customCell.detailTextLabel.text = [self.firstDataSource valueForKey:dataSource][indexPath.row][detailLabel];
self.customCell.imageView.image = [UIImage imageNamed:[self.firstDataSource valueForKey:dataSource][indexPath.row][imageName]];
}
if (tableView == self.secondTableView)
{
self.customCell.textLabel.text = [self.secondDataSource valueForKey:dataSource][indexPath.row][textLabel];
self.customCell.detailTextLabel.text = [self.secondDataSource valueForKey:dataSource][indexPath.row][detailLabel];
self.customCell.imageView.image = [UIImage imageNamed:[self.secondDataSource valueForKey:dataSource][indexPath.row][imageName]];
}
// and so on...
}
To check all other methods, check apples documentation ,i hope this is useful for you and for others as well.. Happy coding.. :)
I'm really new to Objective-C here so what I'm asking may be trivial to most of you but any guidance will help.
Here's a picture of my storyboad.
My current objective is to allow for the user to enter in the number of sets (NSInteger *numReps) and then press the "Log Reps" button and have the table initialize with numReps cells that look like the prototype cell.
Now where I'm at a loss for the implementation. I've never done this kind of thing before so I'm not exactly sure what the best way to go about it is. I have thought of making a custom class for the UITableView table that would take info from the view after the Log Reps button is pushed. I'm not entirely sure how this would need to be implemented. Or can I simply add the table to the properties of the view controller and setup the table within the view controller? That was my initial idea and seems ideal so that I would have everything in one place.
Pleas advise. I am new to all of this and come from a C++ background so I'm still learning a lot of the notation.
Try this:
-(IBAction)btnLogClicked {
int iSet = 4 //Number of row in table
UITableView *tblView= [[UITab;eView alloc]initWithFrame:CGRectMake(0,50,320,100)];
tblView.delegate = self;
tblView.dataSource = self;
[self.view addSubView:tblView];
}
Table View Data Source and Delegate Methods
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return iSet;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
// Display what you want to display in each cell
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
It's not clear what you want to present in your prototype cells, but you need an array (or probably an array of dictionaries) to populate the cells. The number of rows is determined by the number of entires (the count) of that array. So, if you take the number entered by the user, and add that many object to your array, then call reloadData on the table, you will get the number of rows that you want. What those object are that you add to the array, depends on what you're trying to show there.
you could start reading: Table View Programming Guide for iOS
But I can answer you:
You can add the UITableView to the UIViewController, but you need set your UIViewController like the TableView's delegate and dataSource. Your ViewController need to implement the protocol: UITableViewDataSource
The method that you are looking for is: tableView:numberOfRowsInSection:
But I really recommend you that read the Apple Reference.
I have an app that lists some data in a tableview, in cells. I want the user to be able to select a table view cell, any one, and have the app cycle through the 5 lower cells. Here is what I have so far:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
// Navigation logic may go here. Create and push another view controller.
[self fetchCellsToProcess:indexPath];
}
Here is the fetchCellsToProcess: method:
-(void)fetchCellsToProcess:(NSIndexPath*)indexPath{
for (int cellsToProcess = 0; cellsToProcess < 5; cellsToProcess++) {
//process each cell
//...
}
}
I need use the indexPath to get its indexPath.row. Then add 5 to that indexPath.row and only process the tweets between indexPath.row passed in and indexPath.row+5. What programming logic should I use to cycle through cells x -> x+5?
You should not be using cellForRowAtIndexPath: here: that's part of the presentation logic, while you are working on the model-level logic here.
Look at your cellForRowAtIndexPath: method, and see from where does the text of the cell's labels come. Usually it is an NSArray or some other collection. Your didSelectRowAtIndexPath: method should go directly to that same collection, and grab the info from there.
There is no direct approach to achieve this as dasblinkenlight mentioned
but there is a work around which works for me.
Please follow below approach:
Create your custom button within your custom cell
Create your custom cell class and put button(change type to custom ) do necessary connection custom cell class
create and connect action method named "actionSelectedCell" as below from that button in class you are implementing this logic.
And cellForRowAtIndexPath cell.customButton.text = data from your array or dictionary
- (IBAction)actionSelectedCell:(UIButton *)sender {
// below code for getting selected cell row and its section
UIView *view = sender.superview;
while (view && ![view isKindOfClass:[UITableViewCell self]]) view = view.superview;
UITableViewCell *cell = (UITableViewCell *)view;
indexPathForSelectedCell = [self.tableViewListViewVC indexPathForCell:cell];
NSLog(#"cell is in section %d, row %d", indexPathForSelectedCell.section, indexPathForSelectedCell.row);// so now you are getting indexpath for selected cell
// now you can create simple loop logic to get required data and do whatever you like post it to something or store it for any other use
NSString *strFifthCellContent = [NSString stringWithFormat:#"%#",[dataArray ObjectAtIndex:indexPathForSelectedCell.row+5]]; // in this way you can get data from any cell
}
The code below is creating a search for many strings. Initially there are 5 rows, when you reach row five, it adds another row. Instead of just directly editing the row, i load a filter controller (another view controller that as you type it completes words for you). When the user finishes finding a word he clicks it and comes back to this view controller. Now i want to fill the cell that was originally tapped with the text from the filter.
I tried asking earlier and didn't get any concrete answers.
I am running into a problem where when i scroll (after adding a new row), it starts filling in those rows with info already in the table, (as opposed to staying blank)
Please help me where i am going wrong
//global indexpath to remember which cell tapped
NSIndexPath *globalPath;
#interface SearchViewController ()
#end
#implementation SearchViewController
//Load implementation once per launch
- (void)viewDidLoad
{
[super viewDidLoad];
[self linkInputTableToDelegate];
_temporaryResultsArray =[[NSMutableArray alloc]init];
_flurryArray=[[NSMutableArray alloc]init];
_numberOfSections=6;
}
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:NO];
[InputTable reloadData];
textFromUserDefaults=[[[HelperMethods alloc]init]getObjectUserDefault:#"textFiltered"];
[self addTextToFlurryArrayForFlurryAndSavedLists:_textFromUserDefaults];
}
-(void)viewDidDisappear:(BOOL)animated{
}
- (IBAction)searchButtonPressed:(UIButton *)sender {
self.tabBarController.selectedIndex = 1;
}
//Makes the input table respond to delegate table view methods
-(void)linkInputTableToDelegate{
_inputTable.dataSource=self;
_inputTable.delegate=self;
}
-(void)performSearch:(NSString*)text{
//do search
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
int numberOfRows=_numberOfSections;
//Rows for iPhone 4
if ([[UIScreen mainScreen]bounds].size.height==480) {
numberOfRows=numberOfRows;
//Rows for iPhone 5
}else if ([[UIScreen mainScreen]bounds].size.height==568){
numberOfRows=numberOfRows+1;
}
return numberOfRows;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
//In reality groups are created with 1 row inside, this is to allow spacing between the rows
return 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *kCellID = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellID];
if (!cell) {
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kCellID];
}
//Is the cell the same as the one clicked when going to ingredient filter
BOOL cellIndexPathSameAsSelected=[self isCellIndexSameAsPreviousClicked:indexPath];
cell.textLabel.textColor=[UIColor blackColor];
if (cellIndexPathSameAsSelected && _textFromUserDefaults!=nil) {
if (![cell.textLabel.text isEqualToString:_textFromUserDefaults]) {
cell.textLabel.text=_textFromUserDefaults;
[self performTextSearch:_textFromUserDefaults];
}
}
return cell;
}
//Compares the previous clicked cell with the cell now selected
-(BOOL)isCellIndexSameAsPreviousClicked: (NSIndexPath*)cellPath{
if (cellPath.row == globalPath.row && globalPath.section==cellPath.section) {
return YES;
}
else{
return NO;
}
}
- (void)updateTableViewWithExtraRow :(NSIndexPath*)rowSelected{
NSLog(#"number of sections =%i",_numberOfSections);
if (rowSelected.section == _numberOfSections) {
_numberOfSections ++;
}
}
#pragma mark - Table view delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *cellText = [tableView cellForRowAtIndexPath:indexPath].textLabel.text;
[[[HelperMethods alloc]init]saveObjectToUserDefaults:cellText :#"textFiltered"];
globalPath = indexPath;
[self updateTableViewWithExtraRow:indexPath];
}
-(void)addTextToFlurryArrayForFlurryAndSavedLists:(NSString*)text{
if ([_flurryArray count]==0 &&[text length]>0) {
[_flurryArray addObject:text];
}
for (int i=0;i<[_flurryArray count];i++) {
NSString *textInArray=[_flurryArray objectAtIndex:i];
if (![textInArray isEqualToString:text]) {
[_flurryArray addObject:text];
}
}
NSLog(#"Total number of saved items = %i",[_flurryArray count]);
}
// Dispose of any resources that can be recreated.
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
}
I have a couple of reactions looking at the code:
A couple of observations about the proper use of the UITableViewDataSource methods, specifically numberOfRowsInSection, numberOfSectionsInTableView, and cellForRowAtIndexPath:
These really should be driven by some model data structure (e.g. a NSMutableArray) and nothing else;
These methods should be stateless. They should not relying on the value of some NSString instance variable, like _textFromUserDefaults) but rather always look up the value in the NSMutableArray model structure on the basis of the value of the indexPath parameter. You simply cannot make any assumptions about when cellForRowAtIndexPath will be called. This may well account for your duplicate values.
None of these should be doing anything besides responding to the UITableView inquiry. For example, your cellForRowAtIndexPath is invoking performTextSearch. It really shouldn't do anything except return the cell.
Your cellForRowAtIndexPath currently has conditional logic and only updates the cell if certain conditions holds. Because cells are reused, you really want to make sure that you initialize the cells regardless. You can't be assured that the cell is blank when you get it, nor that the previous contents are the previous values for that indexPath. Because cells are reused, it could be for an entirely different row. This could also account for your duplicative entries.
Regarding the interaction of the master view controller and the details view controller, there are more elegant ways than passing data back and forth via NSUserDefaults. For example when you initiate the details view controller, you could just pass it the information it needs. And when it's done, it should call a method in the master view controller to update the data in the master view. To do that, the master view controller should conform to some protocol of your own creation. If you see the example I shared via chat, you can see what that might look like. Anyway, by having some delegate method in the master view controller that the detail view controller calls when it's done, that eliminates the rather fragile technique of using viewDidAppear to control the updating of the master table view.
You might want to contemplate employing "edit" (which allows you to delete, possibly also edit a particular row) and "add" buttons like the standard "master-detail" template that Xcode provides. There are a number of standard conventions here that might be better than having an array of blank cells that you can then tap on. Clearly, your user experience is entirely up to you, but you can always contemplate whether there are existing, familiar conventions that you might employ.
Rob's feedback is good. In broader terms, you can't rely on the cells in a UITableView to hold onto their data. For efficiency, it will be creating, using, and destroying cells at will, and using cellForRowAtIndexPath to figure out what they should look like. Instead of testing what's in a cell, you need to have your own set of data which describe the value of each cell, and just set the value based on the indexPath. I'd recommend storing all your cell information in an NSMutableArray which contains NSStrings or something more complicated if necessary. It will be easy to set default values when you add cells to the array. Then cellForRowAtIndexPath can just access the array rather than attempting its own logic based on current cells.
I have a view controller (EmbeddedMenuView) that uses a custom view (HorizontalMenuView). The Embedded menu view uses multible HorizontalMenuViews. The HorizontalMenuView contains a UITableView. Each cell in the table view uses quite a bit of memory (high quality images.).
Now, I need to execute a task every time a section of the table view cells in the HorizontalMenuView is touched. I did this by creating a protocol in the table view cell and assigning the HorizontalMenuView its delegate. Then I created a protocol in the HorizontalMenuView and assigned the EmbeddedMenuView its delegate. So I pass the touch event up to the EmbeddedMenuView.
The problem is, when I assign the cell's delegate, the HorizontalMenuView does not get deallocated. Since this view refreshes itself every time the view appears, the memory footprint gets out of control fast.
If I comment out the part where the cell is assigned a delegate, everything works fine.
My question is: How can I properly release a UITableViewCell's delegate?
This is the code snippet from the HorizontalMenuView:
-(UITableViewCell*) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
//Custom Logic
HorizontalMenuItemTableViewCell *cell = (HorizontalMenuItemTableViewCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil) {
cell = [[[NSClassFromString([[AMPUIManager sharedManager] classNameForName:cellIdentifier]) alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier] autorelease];
cell.shouldAlwaysTransform = shouldAlwaysTransform;
cell.selectionStyle = UITableViewCellSelectionStyleNone;
cell.colorsDict = colorsDict;
if ([cell isKindOfClass:[ATCustomTableViewCell class]]) {
((ATCustomTableViewCell *)cell).delegate = self; //Commenting this out solves my problem.
}
}
//More Custom Logic
return cell;
}
PS I am using manual reference counting. ARC is not an option for this project.
It sounds like you may have a circular reference. You almost always want to use 'assign' convention with delegates.
See: Why are Objective-C delegates usually given the property assign instead of retain?