I'm figuring out the right mechanism to pass data from UITableViewCells to a UIableViewController (or UIViewController).
Searching within stackoverflow I found different ways to do this and finally I found a mechanism that works well but I don't know if it could be a valid approach.
This is the scenario. First, I created a custom cell (associated with a xib interface), named DataTableViewCell, that extends UITableViewCell. This cell has some outlet to display (and modify) data and an addictional property called index like the following:
#property (nonatomic, copy) NSIndexPath* index;
This property is refreshed inside the method cellForRowAtIndexPath method within the UITableViewController:
- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
DataTableViewCell *cell = (DataTableViewCell*)[tv dequeueReusableCellWithIdentifier:kCellTableIdentifier];
if (cell == nil)
{
[[NSBundle mainBundle] loadNibNamed:#"DataTableViewCell" owner:self options:nil];
cell = (DataTableViewCell*)self.dataTableViewCellOutlet;
}
// configure the cell with data
// do stuff here...
// configure the cell with the current indexPath
cell.index = indexPath;
return cell;
}
Since it is possible to change values within a cell, I had the need to pass data to the UITableViewController for updating the model. To do that I decided to use a delegate mechanism. So, I created a protocol with a method like the following:
- (void)updateData:(DataItem*)dataItem atIndexPath:(NSIndexPath*)index;
The UITableViewController implements that protocol. In this way, within the cell (against to some events), I can call that method and update the model in the correct way.
Having said this, I have some questions to ask:
Is this a right mechanism to pass data from a cell to a controller?
Is it correct to using an index property like the one use in the cell?
Is it the same using retain policy instead of copy policy. What could be the difference?
Since the solution I found could be very scheming, is it possible to use block insteads?
About blocks, I thought this way:
Within the cell create a property block like the following:
#property (nonatomic, copy) void (^updateModelOnEvent)(DataItem* dataItem);
Then inside the method cellForRowAtIndexPath method within the UITableViewController assign that property to a block code like this (this code is at the same level of cell.index = indexPath;):
// configure the cell with the current indexPath
cell.updateModelOnEvent = ^(DataItem* dataItem) {
[self.model insertObject:dataItem atIndex:indexPath.row];
};
Could be a valid alternative? In this case, do I have to use copy or retain policy?
Thank you in advance.
Why not just use [UITableView indexPathForCell:] with a delegate?
MyViewController.h
#interface MyViewController : UITableViewController <MyTableViewCellDelegate>
#end
MyViewController.m
#implementation MyViewController
// methods...
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *reuseIdentifier = #"MyCell";
MyTableViewCell *cell = (id)[tableView dequeueReusableCellWithIdentifier:reuseIdentifier];
if (cell == nil)
cell = [[[MyTableViewCell alloc] initWithMyArgument:someArgument reuseIdentifier:reuseIdentifier] autorelease];
[cell setDelegate:self];
// update the cell with your data
return cell;
}
- (void)myDelegateMethodWithCell:(MyTableViewCell *)cell {
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
// update model
}
#end
MyTableViewCell.h
#protocol MyTableViewCellDelegate;
#interface MyTableViewCell : UITableViewCell
#property (assign, nonatomic) id <MyTableViewCellDelegate> delegate;
- (id)initWithMyArgument:(id)someArgument reuseIdentifier:(NSString *)reuseIdentifier;
#end
#protocol MyTableViewCellDelegate <NSObject>
#optional
- (void)myDelegateMethodWithCell:(MyTableViewCell *)cell;
#end
MyTableViewCell.m
#implementation MyTableViewCell
#synthesize delegate = _delegate;
- (id)initWithMyArgument:(id)someArgument reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier];
if (self) {
// custom setup
}
return self;
}
- (void)prepareForReuse {
[super prepareForReuse];
self.delegate = nil;
}
- (void)someActionHappened {
if ([self.delegate respondsToSelector:#selector(myDelegateMethodWithCell:)])
[self.delegate myDelegateMethodWithCell:self];
}
#end
To modify cells you should modify data model and reload table data. Nothing else.
Not necessary to have a indexPath for cell
In your case it is the same using retain or copy policy. Copy makes new objects with same state.
Related
We use UITableViewCell like this.
- (void)viewDidLoad {
[super viewDidLoad];
[self.tableView registerNib: [UINib nibWithNibName: Cell bundle: nil] forCellReuseIdentifier: kIdentifier];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
Cell *cell = [tableView dequeueReusableCellWithIdentifier: kIdentifier forIndexPath: indexPath];
return cell;
}
When cells are born with some properties(tag), how to get cell's - init method, customize it, and tag the cell?
As I did not see any chance while calling the relative methods.
So how to hook a UITableViewCell/UICollectionViewCell's init method?
Here is a situation:
There are two pages. The cell has a page tag.
Sure, I can add property. Just go a litter farther.
I would recommend creating a simple subclass of UITableViewCell. This way you can create customized table cells with whatever you would like the cell to contain "during" the initialization of the cell. Then you could set your nib file class to, for this example, CustomTableViewCell.
Then, like you already have shown, you can just create your customized cells from your reuseIdentifier:
Additionally, you can intercept the other built in methods awakeFromNib and even prepareForReuse for further customization.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: kIdentifier forIndexPath: indexPath];
// Do anything else here you would like.
// [cell someCustomMethod];
return cell;
}
.h
#import <UIKit/UIKit.h>
#interface CustomTableViewCell : UITableViewCell
- (void)someCustomMethod;
...
#property (nonatomic, nullable) <Some class you want> *somePropertyName;
...
#end
.m
#import "CustomTableViewCell.h"
#implementation CustomTableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
// Do whatever you would like to do here :)
}
return self;
}
- (void)awakeFromNib {
[super awakeFromNib];
// Initialization code. Do whatever you like here as well :)
}
- (void)prepareForReuse {
[super prepareForReuse];
// And here.. :)
}
#end
init isn't really helpful, since cells are created only rarely and then reused.
That said, when cells are initially created, you can intercept that by overloading awakeFromNib. When they're reused later, prepareForReuse is called.
Don't forget to call the super implementations in both methods.
I have two UITableViews using Storyboards in Xcode 7. I've set the delegate and dataSource using the Connections Inspector for both table views.
Let the first table view be the main table view and let the table views within each cell of the main table view be the detail table views with cell identifiers named appropriately and respectively.
When [tableView dequeueReusableCellWithIdentifier:#"MainCell" forIndexPath:indexPath] executes, it immediately calls its dataSource method -cellForRowAtIndexPath: for DetailCell preventing me from setting a custom instance variable in time to add the appropriate data to each cell.
The following is a simplified example marked using comments.
MainTableViewController:
#implementation MainTableViewController
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// Keep in mind the following two (2) lines are set using the Connections Inspector
//cell.detailTableView.dataSource = cell;
//cell.detailTableView.delegate = cell;
// Stepping over the following line will jump to the
// other `-cellForRowAtIndexPath:` (below) used to set
// the detail info.
cell = (MainTableViewCell *)[tableView dequeueReusableCellWithIdentifier:#"MainCell" forIndexPath:indexPath];
CustomObj *obj = self.mainData[indexPath.row];
cell.nameLabel.text = obj.name;
cell.additionalInfo = obj.additionalInfo; // This line is not set before instantiation begins for the detail table view...
return cell;
}
...
#end
DetailTableViewCell (contains a UITableView and implements appropriate protocols):
#interface DetailTableViewCell : UITableViewCell <UITableViewDataSource, UITableViewDelegate>
#property (nonatomic, weak) IBOutlet UILabel *nameLabel;
#property (nonatomic, weak) IBOutlet UITableView *detailTableView;
#property (nonatomic, strong) CustomObj *additionalInfo;
#end
#implementation DetailTableViewCell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
cell = (DetailTableViewCell *)[tableView dequeueReusableCellWithIdentifier:#"DetailCell" forIndexPath:indexPath];
// Instantiate detail ...
cell.detailLabel.text = self.additionalInfo.text;
// Problem!
// self.additionalInfo == nil thus we cannot set a value to the label.
return cell;
}
...
#end
The problem is when the detail -cellForRowAtIndexPath: method is called, I haven't had a chance to set a value for its dataSource, in this case, additionalInfo.
There are many possible ways to fix your problem, but first I would say that, your design seems not a good one, A UItableViewCell has another UITableView, and another UItableViewCell inside this UITableView? Why you do this? Just use one UITableView and put all of your views into one UItableViewCell as subViews should be enough.
Now get to your problem:
I would suggest not to use IBOutlet for setting up your delegate and dataSource, use code. This can give you a chance to delay setting the dataSource and delgate when you are ready. Once you think it's the proper time, just call [cell.detailTableView reloadData] will trigger your DetailTableViewCell to invoke cellForRowAtIndexPath
#implementation MainTableViewController
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// Keep in mind the following two (2) lines are set using the Connections Inspector
//cell.detailTableView.dataSource = cell;
//cell.detailTableView.delegate = cell;
// Stepping over the following line will jump to the
// other `-cellForRowAtIndexPath:` (below) used to set
// the detail info.
cell = (MainTableViewCell *)[tableView dequeueReusableCellWithIdentifier:#"MainCell" forIndexPath:indexPath];
CustomObj *obj = self.mainData[indexPath.row];
cell.nameLabel.text = obj.name;
cell.additionalInfo = obj.additionalInfo; // This line is not set before instantiation begins for the detail table view...
// setup dataSource and delegate now
cell.detailTableView.dataSource = cell;
cell.detailTableView.delegate = cell;
// call reloadData whenever you think is proper
[cell.detailTableView reloadData];
return cell;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell* cell = nil;
//Check this call is for which table view.
if(tableView == detailTableView) {
cell = (MainTableViewCell *)[tableView dequeueReusableCellWithIdentifier:#"MainCell" forIndexPath:indexPath];
// Do any additional setup you want with MainCell
} else {
cell = (DetailTableViewCell *)[tableView dequeueReusableCellWithIdentifier:#"DetailCell" forIndexPath:indexPath];
// Do any additional setup you want with DetailCell
}
return cell;
}
I am trying to implement the concept shown in this example project. My goal is to separate my view controller class and the datasource protocol. Instead of implementing the table view datasource methods in my table view controller class, I try to put it in its own class, and in my view controller, I only call this method to set up my table view:
- (void)setupTableView
{
void (^configureCell)(JVRTodoItemCell *, JVRTodoItem *) = ^(JVRTodoItemCell *cell, JVRTodoItem *todoItem)
{
[cell configureForTodoItem:todoItem];
};
NSArray *todoItems = currentUser.todoItems;
self.todoArrayDataSource = [[JVRArrayDataSource alloc] initWithItems:todoItems withCellIdentifier:TodoCellIdentifier withConfigureCellBlock:configureCell];
self.tableView.dataSource = self.todoArrayDataSource;
[self.tableView registerClass:[JVRTodoItemCell class] forCellReuseIdentifier:TodoCellIdentifier];
}
The data source is separated into its own class:
#interface JVRArrayDataSource ()
#property (copy,nonatomic) NSArray *items;
#property (copy,nonatomic) NSString *cellIdentifier;
#property (copy,nonatomic) void (^configureCellBlock)(id item, id cell);
#end
#implementation JVRArrayDataSource
...
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.items.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:self.cellIdentifier forIndexPath:indexPath];
id item = [self itemAtIndexPath:indexPath];
self.configureCellBlock(cell,item);
return cell;
}
The interesting part is that creating the cell based on the identifier (using dequeueReusableCellWithIdentifier:forIndexPath:) seems to be successful, because the correct cell gets allocated, but its labels remain nil. I try setting up my cell using the following method, but the values remain nil (aTodoItem has valid properties):
- (void)configureForTodoItem:(JVRTodoItem *)aTodoItem
{
self.todoItemTitle.text = aTodoItem.title;
self.todoItemPriority.text = [NSString stringWithFormat:#"%d", aTodoItem.priority];
}
I am trying to figure out what could possibly be missing here, but so far, I haven't managed to fix the issue yet, and I'm starting to lose hope. Any help would be appreciated.
UPDATE:
To make it clear, the issue is shown on this picture.
It seems that the cells get created, but its labels don't.
If all you want to do is separate your tableview datasource delegate from the view controller you can create a separate class called TableViewDataSource. Within that class you can manage the datasources and their table view cells; configuring the them in your view controller, but letting the TableViewDataSource manage them.
TDSTableViewDataSource.h
#import <Foundation/Foundation.h>
#protocol TDSTableViewDataSourceDelegate <NSObject>
- (NSString *)fetchCellIdentifierForObject:(id)object;
- (UITableViewCell *)configureCell:(UITableViewCell *)cell usingObject:(id)item;
#end
#interface TDSTableViewDataSource : NSObject <UITableViewDataSource>
#property (strong, nonatomic) NSArray *items;
#property (strong, nonatomic) id<TDSTableViewDataSourceDelegate> delegate;
#end
TableViewDataSource.m
#import "TDSTableViewDataSource.h"
#implementation TDSTableViewDataSource
- (NSArray *)items {
if (!_items) _items = [[NSArray alloc] init];
return _items;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
if ([self.items count]) {
return [self.items count];
} else {
NSLog(#"numberOfSectionsInTableView could not be determined. self.items is nil or empty.");
return 0;
}
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if ([self.items count]) {
return [self.items count];
} else {
NSLog(#"numberOfRowsInSection could not be determined. self.items contains fewer section requested does not contain any items.");
return 0;
}
}
/*
Single dimension Array of items belonging to a UITableView section
The method checks if the cell implements the HZConfigureTableViewCellDelegate, which is required.
The delegate should be the View Controller.
*/
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
id obj = [self.items objectAtIndex:indexPath.row];
UITableViewCell *cell = nil;
if ([self.delegate conformsToProtocol:#protocol(TDSTableViewDataSourceDelegate)]) {
NSString *cellIdentifier = [self.delegate fetchCellIdentifierForObject:obj];
cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (obj)
cell = [self.delegate configureCell:cell usingObject:obj];
}
return cell;
}
#end
This class and the protocol will essentially allow you to fetch and configure UITableViewCell's and not have to implement the protocols into your View Controller.
Inside your view controller, you create a datasource property using the protocol above.
#import "TDSViewController.h"
#import "TDSTableViewDataSource.h"
#interface TDSViewController () <UITableViewDelegate, TDSTableViewDataSourceDelegate>
#property (strong, nonatomic) TDSTableViewDataSource *dataSource; // UITableView data source.
#property (weak, nonatomic) IBOutlet UITableView *tableView;
#end
#implementation TDSViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableView.dataSource = self.dataSource;
self.dataSource.delegate = self;
}
#pragma mark - UITableView methods
-(NSString *)fetchCellIdentifierForObject:(id)object {
// Check if this is an event or a Reminder class.
if ([object isKindOfClass:[UITableViewCell class]]) {
// Return the cell identifier for this particular cell.
return #"com.myapp.defaultcell";
}
return #"blankcell";
}
- (UITableViewCell *)configureCell:(UITableViewCell *)cell usingObject:(id)item {
UITableViewCell *configuredCell = cell;
// Check if this is an event or a reminder.
if ([item isKindOfClass:[UITableViewCell class]]) {
// Configure the cell to present what data we want here...
}
return configuredCell;
}
#end
This is a complete example project. You can use this to configure any kind of cell you want, without having to add the datasource methods to your view controllers.
The view controller is used by the ConfigureTableViewCellDelegate protocol to configure the UITableViewCell's and use them in the Table View. Since the code is segregated now, the TableViewDataSource class now handles presenting the data to the table view. The View Controller is simply used to configure the cell's. This allows you to use custom UITableViewCells' on each ViewController if you want, and not have to deal with implementing the data sources each time.
UPDATED
Provided a better example, a complete project template.
In the ViewDidLoad register the nib, it fix the problem :)
-(void)viewDidLoad
{
[self.leftTableView registerNib:[UINib nibWithNibName:NIB_FILE bundle:nil] forCellReuseIdentifier:CELL_IDENTIFIER];
}
After hours of digging, I've managed to solve the issue (for now), by changing my custom cell's outlets to strong properties, and initializing them in the cell's init method:
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.todoItemTitle = [[UILabel alloc] init];
self.todoItemPriority = [[UILabel alloc] init];
}
return self;
}
This is very strange, since I thought that creating my views in storyboard, this should be automatically taken care of, and I've never had to do this manually before.
I've got a button in a custom Cell for a TableView which is supposed to open the camera for taking pictures.
I thought of two ways but can't get them working.
First one is to open an instance of UIImagePickerController from within the cell. Well, it seems like I can't call
[self presentViewController...];
from within the cell. Is this right?
Because of this "result" I thought of putting the method which opens up the UIImagePickerController inside the TableViewController and then call this method from within the cell (where the button is located) by something like
[super openCamera];
Or making the TableViewController the delegate of the cell to enable it to call the method.
Are these ideas going in the right direction? What would you recommend? Thank you very much!
Ok, I figured something out but I'm still wondering if it can be done easier.
Here is the solution I found:
In the custom cell I added
#property (nonatomic, assign) id adminController;
Then in the tableViewController I customized the following method to use the custom cell I created and set the tableViewController als "admin"
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"cell";
CreateCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Configure the cell...
cell.adminController = self;
return cell;
}
So I could finally call
[self.adminController performSelector:#selector(openCamera)];
This is an old question but I'd like to have my old questions answered too so... Yes, there is an easier way using blocks:
First, declare a public method in your UITableViewCell interface:
#interface YourCell : UITableViewCell
#property (weak, nonatomic) IBOutlet UIButton *button;
- (void)setDidTapButtonBlock:(void (^)(id sender))didTapButtonBlock;
#end
In the UITableViewCell subclass implementation file declare a private property with a copy attribute.
#import "YourCell.h"
#interface YourCell ()
#property (copy, nonatomic) void (^buttonTappedBlock)(id sender);
#end
Add the target and action of the UIControl in the UITableViewCell constructor and implement the selector method
- (void)awakeFromNib {
[super awakeFromNib];
[self.button addTarget:self
action:#selector(didTapButton:)
forControlEvents:UIControlEventTouchUpInside];
}
- (void)didTapButton:(id)sender {
if (buttonTappedBlock) {
buttonTappedBlock(sender);
}
}
Finally implement the block code in the tableView:cellForRowAtIndexPath: method in the controller
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
YourCell *cell = (YourCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier
forIndexPath:indexPath];
[cell buttonTappedBlock:^(id sender) {
NSLog(#"%#", item[#"title"]);
}];
return cell;
}
For further information in blocks, you can read Working With Blocks
I have a custom UITableViewCell, which have a button on it, IB linked to a function called:
- (IBAction)clickUse:(id)sender;
In this function, I planned to pass an object from UITableView's data source (an object in a NSMutableArray) to next UIViewController, when the user clicks the button on the UITableViewCell.
I set a property in the custom UITableViewCell, like this:
#property (nonatomic, retain) SomeObject *some_object;
In UITableView's cellForRowAtIndexPath function, I pass the object to the cell:
MyCustomCell *cell = (MyCustomCell *)[tableView dequeueReusableCellWithIdentifier:identifier];
cell.some_object = [self.cellData objectAtIndex:indexPath.row];
At this moment I track the object, it's still here. But in the MyCustomCell cell, the object is gone and assigned to nil. Therefore, the object cannot be passed to next UIViewController.
What did I miss?
Perhaps it's better to use a different approach. You can give each cell button a tag. The tag value could be the row index path.
Your -tableView:cellForRowAtIndexPath: method could include the following:
MyCustomCell *cell = (MyCustomCell *)[tableView dequeueReusableCellWithIdentifier:identifier];
cell.button.tag = indexPath.row
And your -clickUse: method could look like this:
- (IBAction)clickUse:(id)sender
{
UIButton *button = (UIButton *)sender;
SomeObject *object = [self.cellData objectAtIndex:button.tag];
// do stuff with your object on click
}
I recommend creating a delegate protocol to handle this. Define a delegate on the cell. In your cellForRowAtIndexPath: method, set the cell.delegate to the viewController that implements that method. Make sure to nil your delegate in your cell's dealloc and prepareForReuse methods. In my opinion, this is the solution that sets up the cleanest relationships between the objects involved. See example below.
Assigning a button target to an object that is some object other than the superview of the button always seems counterintuitive to me. Or, whenever I work on a codebase where there's a setup like that I find that it eventually gets in the way / confuses things.
Inside CommentCell.h:
#class Comment;
#class SMKCommentCell;
#protocol SMKCommentCellDelegate <NSObject>
#required
- (void)commentCellDidTapShowReplies:(SMKCommentCell *)cell;
- (void)commentCellDidTapUsername:(SMKCommentCell *)cell;
#end
#interface SMKCommentCell : UITableViewCell
#property (nonatomic, strong) Comment *comment;
#property (nonatomic, weak) id<SMKCommentCellDelegate> delegate;
#end
Inside CommentCell.m:
#pragma mark - Actions
- (IBAction)didTapShowReplies:(id)sender
{
if ([self.delegate respondsToSelector:#selector(commentCellDidTapShowReplies:)])
{
[self.delegate commentCellDidTapShowReplies:self];
}
}
- (IBAction)didTapUsername:(id)sender
{
if ([self.delegate respondsToSelector:#selector(commentCellDidTapUsername:)])
{
[self.delegate commentCellDidTapUsername:self];
}
}
Inside your viewController.m:
#pragma mark - SMKCommentCell Delegate
- (void)commentCellDidTapShowReplies:(SMKCommentCell *)cell
{
// Do something
}
- (void)commentCellDidTapUsername:(SMKCommentCell *)cell
{
// Do something
}
Inside cellForRowAtIndexPath:
commentCell.comment = comment;
commentCell.delegate = self;