I have some custom tableviewcells, each with a thumbnail having a gesture recognizer on it, to open a modal box.
In each tableviewcell there's a property containing a string.
In the method called by the gesture recognizer, i'm trying to access this property, by looping the superviews of the gesture recognizer.
- (void)handleLongPress:(UILongPressGestureRecognizer *)LongPressGestureRecognizer
{
//handle press
ZoomImageViewController *zoomImage = [self.storyboard instantiateViewControllerWithIdentifier:#"zoomImage"];
UIView *subview = LongPressGestureRecognizer.view;
while (![subview isKindOfClass:[UITableViewCell self]] && subview) {
subview = subview.superview;
}
UITableViewCell *cell = (UITableViewCell *)subview;
zoomImage.filename = cell.machinePicture;
zoomImage.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[self presentViewController:zoomImage animated:YES completion:nil];
}
In debugger the cell object is the tableviewcellcontroller containing the property i want to access.
However, on the following line i get the error "Property 'machinePicture' not found on object of type 'UITableViewCell *'"
zoomImage.filename = cell.machinePicture;
I don't get why the property cannot by found, while cell seems to be the correct object i'm looking for, containing the property i want...
The standard UITableViewCell does not contain a machinePicture property, did you extend the UITableViewCell class?
Use that custom class instead, casting accordingly:
MyUITableViewCell *cell = (MyUITableViewCell *)subview;
on UITableviewCell you wont be getting a property machinePicture since it is not a property in UITableViewCell. I think you are using a custom table view cell. So you must typecast it. Secondly you must use tableview:didSelectRowAtIndexPath: method of UITableViewDelegate to get the tableviewcell and not using gesture recognizer.
Related
I have a custom UITableViewCell subclass and I'm trying to add a pinch gesture recognizer using Interface Builder to one of the views. My app crashes with:
2016-09-11 17:37:22.425 MYAPPNAME[4619:1284144] *** Assertion failure in -[ULFeedView _dequeueReusableViewOfType:withIdentifier:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit/UIKit-3512.60.12/UITableView.m:6539
I've tried different gesture recognizers (e.g. tap recognizers) and different subviews, but they all crash my app.
An interesting observation: It doesn't crash if I add the recognizer to a view programmatically at awakeFromNib.
Here is some methods that might be relevant:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
if(indexPath.section != SECTION_CONTENT){
return; //index path section is NOT equal to SECTION CONTENT for the cell in question, so it will always return.
}
...
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
switch (indexPath.section) {
case SECTION_MAP:
{
ULMapCell *cell = [tableView dequeueReusableCellWithIdentifier:#"map" forIndexPath:indexPath];
return cell; //the cell in question is this one, so it will always return this cell.
}
...
}
UPDATE: I have no problems with registering nibs. It was already working perfectly before the gesture recognizer. Please stop telling me how to register nibs for table view, I already know that as a senior iOS developer.
UPDATE 2: I confirm that it is occuring only when I add it through Interface Builder and there is no problem if I add it anywhere programmatically.
Why would this be happening?
The objects in a nib are organised hierarchically. Often there is just one object at the top level: the root view (in your case that's the cell). However, nibs can contain multiple top-level objects. In fact, gesture recognizers are added to the nib as top-level objects.
Any code that loads a nib with multiple top-level objects needs to know how to deal with this. For example, the code loading the nib for a UITableViewCell could:
find the cell object in the array of top-level-objects
make sure there is no other cell (because it would be impractical to add a heuristic for which one to choose)
ignore all top-level UIGestureRecognizers since they are retained by the views they have been added to
make sure there is nothing else in the top-level objects array
Unfortunately, the way Apple chose to deal with multiple top-level objects when loading UITableViewCell and UICollectionViewCell nibs is to throw an exception. That's why we cannot add gesture recognisers to cells in Interface Builder.
Above your error Assertion failure in -[ULFeedView _dequeueReusableViewOfType:withIdentifier:] indicates
If you use the dequeueReusableCellWithIdentifier:forIndexPath:,we must register a class or nib file using the registerNib:forCellReuseIdentifier: or registerClass:forCellReuseIdentifier: method before calling this method as Apple Document Says
So you need to add first registerNib:forCellReuseIdentifier: or registerClass:forCellReuseIdentifier: in viewDidLoad method
in viewDidLoad method add below line of code first(Use either option 1 or option 2.It is your wish)
OPTION 1:
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:#"map"];
OPTION 2:
[self.tableView registerNib:[UINib nibWithNibName:#"ULMapCell" bundle:nil] forCellReuseIdentifier:#"map"];
Once we registered a class for the specified identifier and a new cell must be created, this method initializes the cell by calling its initWithStyle:reuseIdentifier: method. For nib-based cells, this method loads the cell object from the provided nib file. If an existing cell was available for reuse, this method calls the cell’s prepareForReuse method instead.
Assertion failure in dequeueReusableCellWithIdentifier:forIndexPath:
Then add pinch gesture recognizer code in cellForRowAtindexPath method
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
ULMapCell *cell = (ULMapCell *)[tableView dequeueReusableCellWithIdentifier:#"map" forIndexPath:indexPath];
// If there is no cell to reuse, create a new one
if(cell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"ULMapCell" owner:self options:nil];
cell = [nib objectAtIndex:0]; //If you have lot of cell in ULMapCell give your required index according to your need
}
//Add pinch gesture recognizer code here
UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(handlePinch:)];
[cell addGestureRecognizer:pinchGesture];
return cell;
}
- (void)handlePinch:(UIGestureRecognizer *)recognizer
{
if (recognizer.state == UIGestureRecognizerStateRecognized) {
NSLog(#"Pinch Handled Here");
}
}
Try this instead in you cellforrow method, case SECTION_MAP
[tableView registreNib:[UINib nibWithName#"ULMapCell" bundle:nil] forCellReuseIdentifier#"map"];
ULMapCell *cell = (ULMapCell*)[tableView dequeueReusableCellWithIdentifier:#"map"];
if(!cell){
//alloc and init the cell
}
return cell;
And I prefer add the gesture in CustomCell itself and set a delegate method.
Hope this helps.
I am trying to create custom UICollectionViewCell which contains few properties, and depending on values of those properties drawing components inside cell. I am using dequeueReusableCellWithReuseIdentifier for creating cell, then I am setting some properties, and at the end calling layoutIfNeeded function which is overridden inside my custom cell. Overridden function is setting some properties of cell also for example BOOL property is set to YES, and after refreshing cell (calling reloadData on collection view) function layoutIfNeeded is called again. When I try to read my BOOL property which is set to YES, i am always getting default value which is NO for the first time i call reloadData. When I call reloadData second time, property is set to YES. Any idea what am I doing wrong? Here is code I am using:
on button click I am calling:
[myCollectionView reloadData];
method cellForItemAtIndexPath looks like:
MyCustomCollectionCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"myCustomCell" forIndexPath: indexPath];
cell.device = [collectionArray objectAtIndex:indexPath.row];
[cell layoutIfNeeded];
return cell;
And code of layoutIfNeeded inside MyCustomCollectionCell.m
-(void)layoutIfNeeded{
NSLog(#"bool prop: %d",changedStatus);
changedStatus = YES;
}
BOOL property is defined in MyCustomCollectionCell.h :
#property (nonatomic, assign)BOOL changedStatus;
UPDATE:
I am sorry, I made a mistake in my post. I am not refreshing collection with reloadData, but with reloadItemsAtIndexPaths; This call causes init method of my custom cell to be called again (not just when collection view is loaded for the first time) and after that layoutIfNeeded. I thing problem is that cell is not reused, but created again, causing all properties to disappear. Any idea how to fix this?
You can't use cells to store state data. Cells get used, put in the reuse queue, and then recycled. The specific cell object that stores the data for a particular indexPath may change when the table is reloaded, when a cell is reloaded, when you scroll to expose new cells, etc.
Save state data in your data model.
What is the purpose of changedStatus property ?
Try setting changedStatus = YES; in layoutSubviews instead :
- (void)layoutSubviews
{
[super layoutSubviews];
self.changedStatus = YES;
}
Set this changedStatus Bool inside your ViewController instead of UICollectionViewCell.
BOOL property is defined in YourViewController.h :
#property (nonatomic, assign)BOOL changedStatus;
When you you want to refresh the CollectionView
[myCollectionView reloadData];
changedStatus = YES;
Then inside your MyCustomCollectionCell.m create a new method like,
-(void)customLayoutIfNeeded : (BOOL)status{
NSLog(#"bool prop: %d",changedStatus);
changedStatus = YES;
}
Add this to MyCustomCollectionCell.h as well.
Then inside cellForItemAtIndexPath do this,
MyCustomCollectionCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"myCustomCell" forIndexPath: indexPath];
cell.device = [collectionArray objectAtIndex:indexPath.row];
[cell customLayoutIfNeeded: changedStatus];
return cell;
I'm presenting a lot of data in format of a table with multiple columns. Almost each column has a button (up to 4 in total) and each row is a UITableViewCell.
How could I detect that the buttons were touched and where should I handle touch events of the buttons? I'm certain, it shouldn't be a didSelectRowAtIndexPath method though.
As soon as, I could detect which button was pressed, I would fetch the data in that particular row and manipulate it. So, I need to know the indexPath of the row, as well as what button was pressed on it.
You can subclass UIButton with two properties row and column and implement the logic below:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"
forIndexPath:indexPath];
MyButton *button1 = (MyButton *)[cell viewWithTag:1];
button1.row = indexPath.row;
button1.column = 1; // view tag
[button1 addTarget:self
action:#selector(clickAction:)
forControlEvents:UIControlEventTouchUpInside];
// button2,button3...
return cell;
}
-(void)clickAction:(MyButton *)sender {
// now you can known which button
NSLog(#"%ld %ld", (long)sender.row, (long)sender.column);
}
Generalized undetailed answer:
Create UITableviewcell subclass, link cell ui elements to this class.
Add method configureWithModel:(Model*)model; //Model being the information you want the cell to represent
Manipulate that information or
If you need to manipulate the screen or other objects. You need to give the table view cell subclass a reference to the other objects when the cell is created. (in code or in storyboard or in nib).
how to handle button presses in ios 7: Button in UITableViewCell not responding under ios 7 (set table cell selection to none)
how to link a button: http://oleb.net/blog/2011/06/creating-outlets-and-actions-via-drag-and-drop-in-xcode-4/
If those four views are UIButton then you will receive the tap events on each button or if they are not UIButton then you should add UITapGestureReconiser on each of this views
Several options here. But I would do the following:
Adopt a Delegate Protocol in your custom cell class (see here: How to declare events and delegates in Objective-C?) . This will handle the target selector for the buttons. Pass this message back to your view controller with the sender. To detect which cell it was in do the following:
CGPoint buttonPosition = [sender convertPoint:CGPointZero toView:self.tableView];
CGRect senderFrame = CGRectMake(buttonPosition.x, buttonPosition.y, sender.frame.size.width, sender.frame.size.height);
From here you can decide what the do. Use the buttons .x coordinate to determine which button it was or specify a different tag for each button in cellForRowAtIndexPath:. Or if you want to grab the index path of the cell you can do:
NSArray *indexPaths = [YOUR_TABLE_VIEW indexPathsForRowsInRect:senderFrame];
NSIndexPath *currentIndexPath = [indexPaths lastObject];
Because each button has a different action, the only thing you need to get at runtime is the indexPath of the button. That can be done by looking at the button's superviews until a cell is found.
- (IBAction)action1:(UIButton *)button
{
UITableViewCell *cell = [self cellContainingView:button];
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
MyDataModel *object = self.objects[indexPath.row];
// perform action1 on the data model object
// Now that the data model behind indexPath.row was done, reload the cell
[self.tableView reloadRowsAtIndexPaths:#[indexPath]
withRowAnimation: UITableViewRowAnimationAutomatic];
}
- (id)cellContainingView:(UIView *)view
{
if (view == nil)
return nil;
if ([view isKindOfClass:[UITableViewCell class]])
return view;
return [self cellContainingView:view.superview];
}
There: no delegates, no tags, and the action doesn't care about the internals of the cell.
You will still want to subclass UITableViewCell with the four buttons (call them button1, button2, button3, and button4 if you don't have better names). You can make all the connection is Interface Builder. This will only be needed for populating object data into the cell during -tableView:cellForRowAtIndexPath:.
Ideally, you should create a custom cell by subclassing UITableViewCell and implement the actions for each of these buttons in that cell. If your view controller needs to know about these actions, you can define a protocol, MyCustomCellDelegate or similar, and have your view controller conformed to that protocol. Then MyCustomCell will be able to send messages to the view controller when user interacts with its buttons or other controls.
As in the example code below, you can create a cell in storyboard or nib and hook one of the button's action to firstButtonAction method of CustomTableCell class.
Also, you need to set your view controller as delegate property of CustomTableCell object created and implement the method buttonActionAtIndex: of CustomTableCellDelegate in your view controller class. Use controlIndexInCell param passed to this method to determine which button might have generated the action.
#protocol CustomTableCellDelegate <NSObject>
- (void) buttonActionAtIndex:(NSInteger)controlIndexInCell
#end
In CustomTableCell.h class
#interface CustomTableCell: UITableViewCell
#property (nonatomic, weak) id <CustomTableCellDelegate> delegate
- (IBAction) firstButtonAction:(id)sender
#end
In CustomTableCell.m class
#implementation CustomTableCell
#synthesize delegate
- (IBAction) firstButtonAction:(id)sender{
if ([delegate respondToSelector:#selector(buttonActionAtIndex:)])
[delegate buttonActionAtIndex:0];
}
#end
This is a personal preference on how I like to handle situations like these, but I would first subclass UITableViewCell because your table cells do not look like a default iOS UITableViewCell. Basically you have a custom set up, so you need a custom class.
From there you should set up your 4 IBActions in your header file
- (IBAction)touchFirstButton;
- (IBAction)touchSecondButton;
- (IBAction)touchThirdButton;
- (IBAction)touchFourthButton;
You do not need to pass a sender in these actions, because you will not be using that object in these methods. They are being created to forward the call.
After that set up a protocol for your UITableViewSubClass
#protocol UITableViewSubClassDelegate;
Remember to put that outside and before the #interface declaration
Give your sell a delegate property
#property (nonatomic, strong) id<UITableViewSubClassDelegate> delegate;
and finally define your actual protocol, you will need to set up 4 methods, 1 for each button and take your subclass as a parameter
#protocol UITableViewSubClassDelegate <NSObject>
- (void)forwardedFirstButtonWithCell:(UITableViewSubClass*)cell;
- (void)forwardedSecondButtonWithCell:(UITableViewSubClass*)cell;
- (void)forwardedThirdButtonWithCell:(UITableViewSubClass*)cell;
- (void)forwardedFourthButtonWithCell:(UITableViewSubClass*)cell;
#end
This will be placed outside of the #interface #end section at the bottom
After that create a configureWithModel: method in your #interface and #implementation as well as a property for your model
#interface:
#property (nonatomic, strong) Model *model;
- (void)configureWithModal:(Model*)model;
#implementation:
- (void)configureWithModal:(Model*)model {
self.model = model;
// custom UI set up
}
From here you should configure your action methods in your #implementation file to call the delegate methods, i'm only showing the first one, but you would do this with all of the IBActions
- (void)configureWithModal:(Model*)model {
[self.delegate forwardFirstButtonWithCell:self];
}
From here your custom cell set up is done and we need to go back to the UIViewController that is displaying the UITableView. First go into the header file of the view controller, and import your custom UITableViewCellSubClass and then setup the class to implement this protocol.
It should look something like this
#interface MYViewController : UIViewController <UITableViewSubClassDelegate>
from there you should go into your cellForRowAtIndexPath: method and configure your custom UITableViewCell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCellSubClass *cell = [tableView dequeueReusableCellWithIdentifier:#"CellIdentifier"];
cell.delegate = self;
Model *cellModel = self.tableData[indexPath.row];
[cell configureWithModel:cellModel];
return cell;
}
Now go into your cell class and copy paste all of the protocol methods into your viewController class. I will display one as an example.
In your UIViewController:
- (void)forwardedFirstButtonWithCell:(UITableViewSubClass*)cell {
Model *cellModel = cell.model;
// do stuff with model from outside of the cell
}
do that for all methods and you should be good.
Remember to have all your #imports in so there's no forward declarations and remember to link up the IBActions to your storyboard or xib files. If you want a custom xib for your table cell you will have to check if the cell is nil and then allocate a new one, but if you are using prototype cells then this should be sufficient.
For simplicity sakes i put forwardFirstButtonWithCell: but i would encourage making the name something that describes what it's doing such as, displayPopOverToEnterData or something similar. From there you could even change the parameters of the delegate protocol methods to take models instead so instead of
- (void) displayPopOverToEnterDataWithCell:(UITableViewSubClass*)cell;
make it
- (void) displayPopOverToEnterDataWithModel:(Model*)model;
but, i don't know what type of information you need to access from the cell. So update these methods as you see fit.
I have a UITableView with a UITextField in each of the UITableViewCells. I have a method in my ViewController which handles the "Did End On Exit" event for the text field of each cell and what I want to be able to do is update my model data with the new text.
What I currently have is:
- (IBAction)itemFinishedEditing:(id)sender {
[sender resignFirstResponder];
UITextField *field = sender;
UITableViewCell *cell = (UITableViewCell *) field.superview.superview.superview;
NSIndexPath *indexPath = [_tableView indexPathForCell:cell];
_list.items[indexPath.row] = field.text;
}
Of course doing field.superview.superview.superview works but it just seems so hacky. Is there a more elegant way? If I set the tag of the UITextField to the indexPath.row of the cell its in in cellForRowAtIndexPath will that tag always be correct even after inserting and deleting rows?
For those paying close attention you might think that I have one .superview too many in there, and for iOS6, you'd be right. However, in iOS7 there's an extra view (NDA prevents me form elaborating) in the hierarchy between the cell's content view and the cell itself. This precisely illustrates why doing the superview thing is a bit hacky, as it depends on knowing how UITableViewCell is implemented, and can break with updates to the OS.
Since your goal is really to get the index path for the text field, you could do this:
- (IBAction)itemFinishedEditing:(UITextField *)field {
[field resignFirstResponder];
CGPoint pointInTable = [field convertPoint:field.bounds.origin toView:_tableView];
NSIndexPath *indexPath = [_tableView indexPathForRowAtPoint:pointInTable];
_list.items[indexPath.row] = field.text;
}
One slightly better way of doing it is to iterate up through the view hierarchy, checking for each superview if it's an UITableViewCell using the class method. That way you are not constrained by the number of superviews between your UITextField and the cell.
Something along the lines of:
UIView *view = field;
while (view && ![view isKindOfClass:[UITableViewCell class]]){
view = view.superview;
}
You can attach the UITableViewCell itself as a weak association to the UITextField, then pluck it out in the UITextFieldDelegate method.
const char kTableViewCellAssociatedObjectKey;
In your UITableViewCell subclass:
- (void)awakeFromNib {
[super awakeFromNib];
objc_setAssociatedObject(textField, &kTableViewCellAssociatedObjectKey, OBJC_ASSOCIATION_ASSIGN);
}
In your UITextFieldDelegate method:
UITableViewCell *cell = objc_getAssociatedObject(textField, &kTableViewCellAssociatedObjectKey);
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
//...
I'd also recommend re-associating every time a cell is dequeued from the UITableView to ensure that the text field is associated with the correct cell.
Basically in this case, I would prefer you to put the IBAction method into cell instead of view controller. And then when an action is triggered, a cell send a delegate to a view controller instance.
Here is an example:
#protocol MyCellDelegate;
#interface MyCell : UITableViewCell
#property (nonatomic, weak) id<MyCellDelegate> delegate;
#end
#protocol MyCellDelegate <NSObject>
- (void)tableViewCell:(MyCell *)cell textFieldDidFinishEditingWithText:(NSString *)text;
#end
In a implementation of a cell:
- (IBAction)itemFinishedEditing:(UITextField *)sender
{
// You may check respondToSelector first
[self.delegate tableViewCell:self textFieldDidFinishEditingWithText:sender.text];
}
So now a cell will pass itself and the text via the delegate method.
Suppose a view controller has set the delegate of a cell to self. Now a view controller will implement a delegate method.
In the implementation of your view controller:
- (void)tableViewCell:(MyCell *)cell textFieldDidFinishEditingWithText:(NSString *)text
{
NSIndexPath *indexPath = [_tableView indexPathForCell:cell];
_list.items[indexPath.row] = text;
}
This approach will also work no matter how Apple will change a view hierarchy of a table view cell.
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?