I don't know if this is considered to be an accepted Objective-C practice or not, so I'm open to other ideas. Here is the idea. I have a table that gets is cells from a custom UITableViewCell. Each of these cells presents an event that the user can attend. As such, I'd like for the user to be able to add them to their calendar directly from the table view.
To accomplish this, I put a button on each table cell - an "add to calendar button". What I am stuck on is how to wire an action from this button back to the UIViewController where the UITableView is a subview. The button is part of a UITableView class and doesn't have visibility to the UIViewController.
I've been trying to implement the delegate pattern suggested by Aaron below. I'm almost there, but something is still disconnected. Here is what I have:
New protocol EventDelegate.h
#protocol EventDelegate <NSObject>
- (void) addToCalendar : (NSString *) strDate;
#end
In MyTableCell.h, I have added this property:
#property (nonatomic, strong) id<EventDelegate> eventDelegate;
In MyTableCell.m, I have added this method:
- (IBAction)addToCalendar:(UIButton *)sender
{
NSLog(#"calling addToCalendar delegate %#", _dayAndTime.text);
[self.eventDelegate addToCalendar:_dayAndTime.text];
}
All is well to this point. When I click on the button that I added to the table cell, I get the output calling addToCalendar delegate Monday, January 13
Over in MyViewController.h, I changed it look like this:
#interface TrainingScheduleViewController : UIViewController <UITableViewDelegate, UITableViewDataSource, EventDelegate>
- (void) addToCalendar:(NSString *)strDate;
#end
And finally, in MyViewController.m I added the method body:
- (void) addToCalendar:(NSString *)strDate
{
NSLog(#"inside delegate");
NSLog(#"%#", strDate);
}
The part that I think might be the problem is where Aaron suggests adding this line of code:
[tableViewController setEventDelegate:self];
First, I'm not sure where to add this line. I put it in viewDidLoad. The compiler wouldn't let me type it verbatim, so the closest thing that I could find was this line:
[self.tableView setDelegate:self];
Maybe I need an additional outlet?
I have to be almost there, but I just don't see what I am still missing. Can anyone help me? Thanks!
Here is some example code that I pulled from my "cellForRowAtIndexPath" method. This is from a table that was built in IB and uses a prototype cell, but the idea is the same no matter how you implement. In this case, I use a tag to identify the button, then reference it when the cell is created:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
UIButtonRed *actionButton = (UIButtonRed *)[cell.contentView viewWithTag:4];
[actionButton addTarget:self action:#selector(initializeReorder:) forControlEvents:UIControlEventTouchDown];
return cell;
}
The initializeReorder: method automatically receives (id)sender as a parameter. You can cast that to a table cell and inspect it to get the rest of your info:
- (void)initializeReorder:(id)sender
{
UIButtonRed *actionButton = (UIButtonRed*)sender;
UITableViewCell *cell = (UITableViewCell*)actionButton.superview.superview;
NSIndexPath* cellPath = [self.tableView indexPathForCell:cell];
...
}
Create a protocol for this.
I would define a protocol like this:
#protocol MyEventDelegate <NSObject>
- (void)addToCalendar:(Event*)event;
#end
And add a delegate property to both your UITableViewController class and your MyTableCell class:
#property (nonatomic, strong) id<MyEventDelegate> eventDelegate;
MyViewController should conform to this protocol and implement addToCalendar:
#interface MyViewController : UIViewController <MyEventDelegate>
When your MyViewController object sets up the UITableViewController, pass in a reference to itself:
[tableViewController setEventDelegate:self];
and when your UITableViewController creates each cell, pass it on:
[cell setEventDelegate:self.eventDelegate];
Now, when the IBAction is called in your cell, the cell can call the delegate method on MyViewController like so:
[self.eventDelegate addToCalendar:event];
Related
I have toggle buttons in my tableview cells and I click them on for some cells but when I scroll down, those same buttons are selected for the bottom cells as well even though I didn't select them yet. I know this is happening because of the tableview reusing cells...is there any way I can fix this?
The cells are dynamic, not static.
what the tableview looks like
** EDIT: Also, lemme know if my logic seems alright: I tried creating a mutable array in my viewcontroller class and then setting all it's values to #"0". Then, in my tableviewcell's class, I set the value in the array to #"1" at the index of the current cell if I select the button, so then back in my viewcontroller class, I can tell if I have already selected a button at that cell or not. The only flaw is that I can't access the array in my tableviewcell's class, it is coming out at null...i guess that it because of the mvc pattern in objective c. Any advice?
EDIT
I am still unable to resolve my issue. Can someone please help me? I have been stuck on it for a while now!
I am trying to create a tableview where the cells have a check and cross button and when I click the check button, it should turn green, but the same button in other cells should remain gray, however, when I scroll down, some cells that I didn't select buttons in still turn green...because of cell recycling.
I am using delegates and protocols right now but it isn't working; perhaps I am using it wrong?
I am setting yesChecked value in IBaction functions in my cell class, and in my viewcontroller class, I am using that yesChecked value to see what color to give to the button based on whether it says "yes" or "no".
Kindly help! Thanks!
#protocol DetailsTableViewCellDelegate <NSObject>
- (void) customCell:(DetailsTableViewCell *)cell yesBtnPressed:(bool)yes;
#property (nonatomic, retain) NSString * yesChecked;
You'd have to select or deselect them in cellForRowAt. For example if your cell had a leftButton property and you had a model like this, you could do something like the following:
#interface Model : NSObject
#property (nonatomic, assign) BOOL selected;
#end
#protocol CustomCellDelegate <NSObject>
- (void)cellActionTapped:(UITableViewCell *)cell;
#end
#interface CustomCell : UITableViewCell
#property (nonatomic, assign) BOOL leftButtonSelected;
#property (weak, nonatomic, nullable) id<CustomCellDelegate> delegate;
#end
// ModelViewController.h
#interface ModelViewController : UIViewController<CustomCellDelegate>
#end
// ModelViewController.m
#interface ViewController () {
NSArray<Model*>* models;
}
#end
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"reuseIdentifier"];
((CustomCell *)cell).delegate = self;
((CustomCell *)cell).leftButtonSelected = models[indexPath.row].selected;
return cell;
}
- (void)cellActionTapped:(UITableViewCell *)cell {
NSIndexPath *indexPath = [tableView indexPathForCell:cell];
// Update data source using (maybe) indexPath.row
}
I created the custom cell (XIB) as the subclass of UICollectionViewCell and the cell has a button in it. When I click a button, I want to go to another view with some data on that, and could go back to the original view by clicking a button as well. I've search for that and found something like "segue" or "modal" but I can't do it initially from my custom cell.
Is there any way to do this? Any help would be very thankful.
So what you want to do, since it seems like UICollectionView works the same as UITableView, is make a subclass of UICollectionViewCell that contains a protocol to send actions, like pressing a button, to a view controller from a different view. In this case, a different view being the UICollectionViewCell.
Adding a Protocol to a UICollectionViewCell
Add a new Cocoa Touch Class called UICustomCollectionViewCell with subclass of UICollectionViewCell. And include the interface builder file
header file UICustomCollectionViewCell.h
#protocol UICustomCollectionViewCellDelegate;
#interface UICustomCollectionViewCell : UICollectionViewCell
#property ( nonatomic, retain) IBOutlet UIButton *button;
- (IBAction)pressButton:(id)sender;
#property ( assign) id< UICustomCollectionViewCellDelegate> delegate;
#end
#protocol UICustomCollectionViewCellDelegate <NSObject>
#optional
- (void)customCollectionViewCell:(UICustomCollectionViewCell *)cell pressedButton:(UIButton *)button;
#end
implementation file UICustomCollectionViewCell.m
#implementation UICustomCollectionViewCell
#synthesize delegate;
- (IBAction)pressButton:(id)sender {
if ([delegate respondsToSelector: #selector( customCollectionViewCell:pressedButton:)])
[delegate customCollectionViewCell: self pressedButton: sender];
}
#end
xib file UICustomCollectionViewCell.xib
make sure the connections from the UICustomCollectionViewCell are connected to the button from the Connections Inspector:
button
-pressButton:
Finally, using this class in your project
Import the class as well as the delegate:
#import "UICustomCollectionViewCell.h"
#interface ViewController () < UICustomCollectionViewCellDelegate>
#end
In this following code, you will use the UICustomCollectionViewCell class instead of UICollectionViewCell:
UICustomCollectionViewCell *cell;
...
[cell setDelegate: self];
...
return cell;
And now the action, or method, that is called when the button is pressed:
- (void)customCollectionViewCell:(UICustomCollectionViewCell *)cell pressedButton:(UIButton *)button {
//action will be here when the button is pressed
}
If you want to find out what indexPath this cell was from:
[collectionView indexPathForCell: cell];
You can't/shouldn't perform navigation jobs in the cell, navigation is not in the cells domain.
What you can try is
1) Use a delegate, setup a delegate and wire it up to the button action, the controller hosting the tableview/collection view can set itself up as the delegate and listen to any events. This controller should be in charge of pushing a new view to the stack using any method you desire.
2) If you hate delegates but love blocks, you can setup a callback block on the cell, its actions could be setup in the cellForRowAtIndex: method in the controller.
Noticed a pattern here? both the above methods are delegating the task from the cell to the controller.
If all fails, just implement didSelectItemAtIndexPath: and stick with it.
Did you try with didSelect method?
- (void)collectionView:(UICollectionView *)collectionView
didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Main" bundle: nil];
YourNewViewControllerClass *someViewController = [storyboard instantiateViewControllerWithIdentifier:#"YourNewVCID"];
[self presentViewController:someViewController
animated:YES
completion:nil];
}
Easiest way would be to implement cellForRow.. method, set a tag for your cell/button and react basing on that tag (eg. indexPath.row).
1.custom your button
NouMapButton.h
#import <Foundation/Foundation.h>
#interface NouMapButton : UIButton
#property (nonatomic, readwrite, retain) NSObject *dataObj;
#end
NouMapButton.m
#import "NouMapButton.h"
#implementation NouMapButton
#end
set your button data and target in
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
btn01.dataObj = YOUR_DATA;
[btn01 addTarget:self action:#selector(map:) forControlEvents:UIControlEventTouchUpInside];
then you can get button custom dataObj in sender.dataObj
-(void)map:(NouMapButton *)sender{
MapViewController *nextView = [[MapViewController alloc] init];
nextView.dataObj = sender.dataObj;
//TODO....
}
I have a custom uitableviewcell and subclassed, and it is containing a uitextfield and delegate is also set, now when return key on keyboard is pressed I want to try few things
perform a segue(but issue is I am in uitableviewcell subclass).
modally present another view controller(but issue is uitableviewcell
do not allow this).
I want to display uiactionsheet(but again limitation is
uitableviewcell).
If i get rootviewcontroller reference then rootviewcontroller's view itself not displayed or not the active view so any thing you do will not present on screen, active view is required.
You could use a block property on your cell that is fired whenever your custom button action occurs. Your cell's block property might look something like this:
#interface CustomTableViewCell : UITableViewCell
#property (nonatomic, copy) void (^customActionBlock)();
#end
Your cell would then invoke this block from the custom button action like this:
#implementation CustomTableViewCell
- (IBAction)buttonTapped:(id)sender {
if ( self.customActionBlock ) {
self.customActionBlock();
}
}
#end
Then finally, you set the block in -cellForRowAtIndexPath: back in your view controller (or wherever) like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
CustomTableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:#"customCell" forIndexPath:indexPath];
cell.textLabel.text = [self.colors objectAtIndex:indexPath.row];
cell.customActionBlock = ^{
NSLog(#"Do the stuff!");
// present view controller modally
// present an action sheet
// etc....
};
return cell;
}
One word of caution, though. If you use blocks you run the risk of strongly referencing self and creating a memory leak. Blocks are fun and easy to use but you have to play by their rules. Here are some resources to help you get familiar with them:
Retain cycle on `self` with blocks
Reference to self inside block
http://aceontech.com/objc/ios/2014/01/10/weakify-a-more-elegant-solution-to-weakself.html
http://fuckingblocksyntax.com
You can attach action to your buttons even if they are in a tableView
[cell.button addTarget:self action:#selector(presentController:) forControlEvents:UIControlEventTouchUpInside];
presentController is referring to an IBAction
- (IBAction)presentController:(id)sender
{
//present
}
Implement button action in Tableview SuperClass.
Or You can use Custom delegate in UITableViewCell subclass. In UITableView Subclass declare a protocol.
#protocol customCellDelegate <NSObject>
#required
-(void)selectedButtonInIndexPath : (NSIndexPath *)indexpath;
#end
Set this property in UITableView Subclass
#property (nonatomic, strong)NSIndexPath *indexpath;
#property (nonatomic, strong) id <customCellDelegate> delegate;
And then in Your UITableView Subclass Button action add This lines
if(self.delegate){
[self.delegate selectedButtonInIndexPath: self.indexpath];
}
And in your tableview datasource method
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
Implement this code
cell.delegate = (id)self;
cell.indexpath = indexPath;
And in Uitableview super class just implement this method
-(void)selectedButtonInIndexPath : (NSIndexPath *)indexpath{
//display uiimagepickercontroller modally or anything else here
}
I am currently creating a custom grid view, which means that I am creating a class that has a lot in common with UITableView. One of the things that I want to get right is the communication of the cells and the grid view.
I was therefore wondering how a table view cell talks to its table view. For example, how does the cell notify the table view that its delete button was tapped and the cell needs to be removed from the table view?
There are several possible scenarios, but I am not sure which one is being used by Apple since the headers of UITableView or UITableViewCell reveal this (or am I overlooking something).
Ultimately, the goal is to let the cell and the grid view communicate in private, that is, without exposing any public methods or protocols (if this is possible).
Now a delete button might be a poor example because iOS has a built in method which allows you to delete rows and notify your datasource called:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
However, for the sake of understanding if you wanted to add a button to your tableview cell and have it perform an action that isn't in the standard iOS library you would create a delegate in your cell and set your tableview's datasource file as the delegate.
Basically you would subclass UITableViewCell like so
MyCustomCell.h
#protocol MyCustomCellDelegate;
#interface MyCustomCell : UITableViewCell
#property (nonatomic, unsafe_unretained) id <MyCustomCellDelegate> delegate; //Holds a reference to our tableView class so we can call to it.
#property (nonatomic, retain) NSIndexPath *indexPath; //Holds the indexPath of the cell so we know what cell had their delete button pressed
#end
/* Every class that has <MyCustomCellDelegate> in their .h must have these methods in them */
#protocol MyCustomCellDelegate <NSObject>
- (void)didTapDeleteButton:(MyCustomCell *)cell;
#end
MyCustomCell.m
#synthesize delegate = _delegate;
#synthesize indexPath = _indexPath;
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self)
{
/* Create a button and make it call to a method in THIS class called deleteButtonTapped */
UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
button.frame = CGRectMake(5, 5, 25, 25);
[button addTarget:self action:#selector(deleteButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
}
return self;
}
/**
* This is the method that is called when the button is clicked.
* All it does is call to the delegate. (Whatever class we assigned to the 'delegate' property)
*/
- (void)deleteButtonTapped:(id)sender
{
[self.delegate didTapDeleteButton:self];
}
Your TableView's datasource would look something like this.
MyDataSource.h
/* We conform to the delegate. Which basically means "Hey you know those methods that we defined in that #protocol I've got them and you can safely call to them" */
#interface MyDataSource : UIViewController <MyCustomCellDelegate, UITableViewDelegate, UITableViewDataSource>
#property (nonatomic,retain) NSArray *tableData;//We will pretend this is the table data
#property (nonatomic,retain) UITableView *tableView;// We will pretend this is the tableview
#end
MyDataSource.m
//We will pretend we synthesized and initialized the properties
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyCustomCell *cell = [tableView dequeueReusableCellWithIdentifier: #"MyCustomCell"];
if (!cell)
cell = [[DownloadQueueCell alloc] initWithStyle: UITableViewCellStyleDefault reuseIdentifier: #"MyCustomCell"];
cell.delegate = self; // Make sure we set the cell delegate property to this file so that it calls to this file when the button is pressed.
cell.indexPath = indexPath;// Set the indexPath for later use so we know what row had it's button pressed.
return cell;
}
- (void)didTapDeleteButton:(MyCustomCell *)cell;
{
// From here we would likely call to the apple API to Delete a row cleanly and animated
// However, since this example is ignoring the fact that they exist
// We will remove the object from the tableData array and reload the data
[self.tableData removeObjectAtIndexPath:cell.indexPath];
[self.tableView reloadData];
}
Basically, long story short. For your gridview you would just create a delegate method that tells the user a certain button was pressed.
UITableViewCell items are subviews of UITableView. So you could use it to communicate between cells and tableView. In its turn UITableView has the delegate and datasource to communicate with its controller. This might help.
I'm not sure that a private communication channel is needed.
The table view imposes a delete view adjacent to a given cell by resizing the table view cell and creating a new view in the open space.
The imposed delete view is instantiated with the table view, the index path, and the table view delegate. The delete view handles the touch and sends a message to the table view delegate including the table view and index path. The table view delegate does the work of removing the entry from the data source, animating the cell removal and refreshing the table view. Upon refresh, the table view redraws all the visible cells according to the data source.
You can have your custom cell UIViews have a private property of the type of your Grid View. When you add these cells to your GridView, update that property to the gridView.
I have my custom grid and do it this way.
Another way is having a method in your grid to pass a cell, and that will return you the index. UITableView has those methods too. That way when a button in a cell is pressed, all you have to do is get the cell and pass it to the grid, that will return an index. With that index you access the data...
You may use categories.
You declare your private methods in a separate category, and place it to the separate file. In the implementation file of class which wants to use these private methods, you import this file with private category, and use the private methods. So the public .h of the class which uses them is left intact.
Example:
MyGridViewCell.h:
#interface MyGridViewCell : UIView
// ...
#end
MyGridViewCell.m:
#implementation MyGridViewCell : UIView
// ...
#end
Now the private methods category interface:
MyGridViewCellPrivate.h:
#interface MyGridViewCell (Private)
- (void) privateMethod1;
#end
And implementation:
MyGridViewCellPrivate.m:
#implementation MyGridViewCell (Private)
- (void) privateMethod1
{
// ...
}
#end
Header remains the same as before:
MyGridView.h:
#interface MyGridView : UIView
- (void) publicMethod1;
#end
But the implementation may use the private API:
MyGridView.m:
#import "MyGridViewCell.h"
#import "MyGridViewCellPrivate.h"
- (void) publicMethod1
{
// Use privateMethod1
}
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;