UIView animation in UITableViewCell works only once - ios

Here's a strange fact :
I have a custom UITableViewCell in which I have an animation (an image moves).
It works fine on the creation of the cell.
But once I scroll to hide the cell and then go back on it, the animation has stopped. That, I can understand. But I'm calling my animation method in my viewWillAppear too. And the wired part is that the method is called, I've put a breakpoint in it, nothing is desallocated...
My UITableViewCell is kept strongly (there's a music player in it, the music continues to play). I really don't get it.
Here's my code :
#property (nonatomic, strong) DWPlayerCellVC *playerView;
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"player"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"player"];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
NSArray *viewsToRemove = [cell.contentView subviews];
for (UIView *v in viewsToRemove) {
[v removeFromSuperview];
}
if(!self.playerView){
self.playerView = [[DWPlayerCellVC alloc] init];
}
[cell.contentView addSubview:self.playerView.view];
self.playerView.model = self.model[indexPath.row];
return cell;
DWPlayerCellVC :
#implementation DWPlayerCellVC
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
}
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self zoomIn];
}
- (void)setModel:(id)model
{
_model = model;
// ...
[self zoomIn];
}
- (void)zoomIn
{
UIViewAnimationOptions options =
UIViewAnimationOptionBeginFromCurrentState |
UIViewAnimationOptionCurveEaseInOut |
UIViewAnimationOptionRepeat |
UIViewAnimationOptionAutoreverse;
[UIView
animateWithDuration:10.f
delay:0.f
options:options
animations:^{
CGFloat scale = 1.4f;
self.imageCoverView.transform = CGAffineTransformMakeScale(scale, scale);
} completion:NULL];
}
If you have any ideas...
Thanks a lot !

It looks like the problem is because zoomIn will only ever be called by setModel. This is because UITabelViewCells are not expected to managed displaying UIViewControllers so viewWillAppear: will never be called.
There are two options that I can think of.
The first would be to set up your root view controller to be a containment view controller and add the DWPlayerCellVC as a childViewController. This option does involve some work to get it all running, I suggect reading Creating Custom Container View Controllers to see whets required to get it working.
The second (and the one I would use) would be to create a UITableViewCell subclass that handles running the animation. Then you can just implement the method prepareForReuse to restart the animation. That method is automatically called on a cell when you use dequeueReusableCellWithIdentifier:.

Your UITableViewCell will remember it's state from before. When it's put into the queue (when it scrolls out of view), it doesn't revert back to a pristine state. When you call your ZoomIn method, I think what's happening is that it's zooming in again, but you can't tell because it's already zoomed in.
Try - before you zoom in, ensure that it's zoomed out. So in ViewWillAppear, to a quick "Reset" of the Cell, and then call zoomIn, and see if that fixes the issue.

Related

Animating constraints causing subviews layout to be visible on screen

I have a messaging screen I am creating and I am almost done it. I built most of the views with nib files and constraints. I have one small bug however where I can visually see some of the cells laying themselves out when the keyboard dismisses because of the requirement to call [self.view layoutIfNeeded] in an animation block involving a constraint. Here is the problem:
- (void)keyboardWillHide:(NSNotification *)notification
{
NSNumber *duration = [[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey];
NSNumber *curve = [[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey];
[UIView animateWithDuration:duration.doubleValue delay:0 options:curve.integerValue animations:^{
_chatInputViewBottomSpaceConstraint.constant = 0;
// adding this line causes the bug but is required for the animation.
[self.view layoutIfNeeded];
} completion:0];
}
Is there any way around directly calling layout if needed on the view since this also causes my collection view to lay itself out which makes the cells layout visually on screen sometimes.
I tried everything I can think of but it I can't find a solution to the bug fix. I have already tried calling [cell setNeedLayout]; in every location possible, nothing happens.
How about this?
In your UITableViewCell implement a custom protocol called MYTableViewCellLayoutDelegate
#protocol MYTableViewCellLayoutDelegate <NSObject>
#required
- (BOOL)shouldLayoutTableViewCell:(MYTableViewCell *)cell;
#end
Create a delegate for this protocol
#property (nonatomic, weak) id layoutDelegate;
Then override layoutSubviews on your UITableViewCell:
- (void)layoutSubviews {
if([self.layoutDelegate shouldLayoutTableViewCell:self]) {
[super layoutSubviews];
}
}
Now, in your UIViewController you can implement the shouldLayoutTableViewCell: callback to control whether the UITableViewCell gets laid out or not.
-(void)shouldLayoutTableViewCell:(UITableViewCell *)cell {
return self.shouldLayoutCells;
}
Modify your keyboardWillHide method to disable cell layout, call layoutIfNeeded, and restore the cell layout ability in the completion block.
- (void)keyboardWillHide:(NSNotification *)notification {
NSNumber *duration = [[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey];
NSNumber *curve = [[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey];
self.shouldLayoutCells = NO;
[UIView animateWithDuration:duration.doubleValue delay:0 options:curve.integerValue animations:^{
_chatInputViewBottomSpaceConstraint.constant = 0;
[self.view layoutIfNeeded];
} completion:completion:^(BOOL finished) {
self.shouldLayoutCells = NO;
}];
}
I can't really test this since you didn't provide sample code, but hopefully this will put you on the right path.

popToRootViewController crashes when tableView is still scrolling

When I give a good swipe to my tableView and press the "Back" button before the tableView ended it's scrolling, my app crashes. I've tried the following:
- (void) closeViewController
{
[self killScroll];
[self.navigationController popToRootViewControllerAnimated:YES];
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)killScroll
{
CGPoint offset = sellersTableView.contentOffset;
[sellersTableView setContentOffset:offset animated:NO];
}
That didn't work, same crash. I don't see why, the error I'm getting is the following:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UITableView dataSource must return a cell from tableView:cellForRowAtIndexPath:'
So that means that the tableView is still requesting a cell when everything is already being deallocated. Makes no sense.
Then I tried this:
- (void) closeViewController
{
[self.navigationController popToRootViewControllerAnimated:YES];
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)dealloc
{
sellersTableView.dataSource = nil;
sellersTableView.delegate = nil;
sellersTableView = nil;
}
Gives me the same error. Any ideas?
Update:
My delegate methods
creation
if (textField == addSellerTextField) {
sellersTableView = [[UITableView alloc] initWithFrame:CGRectMake(addSellerTextField.frame.origin.x + addSellerTextField.frame.size.width + 10, addSellerTextField.frame.origin.y - [self heightForTableView] + 35, 200, [self heightForTableView])];
sellersTableView.delegate = self;
sellersTableView.dataSource = self;
sellersTableView.backgroundColor = [[UIColor grayColor] colorWithAlphaComponent:0.05];
sellersTableView.separatorColor = [[UIColor grayColor] colorWithAlphaComponent:0.15];
sellersTableView.rowHeight = 44;
sellersTableView.layer.opacity = 0;
[self.companyView addSubview:sellersTableView];
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{sellersTableView.layer.opacity = 1;} completion:nil];
}
cellForRowAtIndexPath
if (tableView == sellersTableView) {
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
cell.backgroundColor = [UIColor clearColor];
if ([sellersArray count] > 0) {
cell.textLabel.text = [sellersArray objectAtIndex:indexPath.row];
} else {
UILabel *noSellersYetLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, sellersTableView.frame.size.width, [self heightForTableView])];
noSellersYetLabel.text = #"no sellers yet";
noSellersYetLabel.textAlignment = NSTextAlignmentCenter;
noSellersYetLabel.textColor = [UIColor grayColor];
[cell addSubview:noSellersYetLabel];
sellersTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
}
}
removing
- (void) textFieldDidEndEditing:(UITextField *)textField
{
if (textField == addSellerTextField) {
[self updateSellers:textField];
}
}
- (void)updateSellers:(UITextField *)textField
{
[textField resignFirstResponder];
[self hideSellersTableView];
}
- (void)hideSellersTableView
{
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{sellersTableView.layer.opacity = 0;} completion:nil];
sellersTableView.dataSource = nil;
sellersTableView.delegate = nil;
[sellersTableView removeFromSuperview];
sellersTableView = nil;
}
Solution
So apparently putting the dataSource = nil and delegate = nil into textFieldDidEndEditing fixed the problem. Thanks everybody for the answers!
It's strange behaviour of UITableView. The easiest way to resolve this issue just set the dataSource and delegate property of UITAbleView to nil before you make a call of function popToRootViewControllerAnimated. Furthermore you can use more common solution and add the code that set the properties to nil into the -dealloc method. In addition you no need the -killScroll method.
After a short research I have realized what the problem is. This unusual behaviour appeared in iOS 7. The scroll view retained by its superview may send message to delegate after the delegate is released. It happens due to -removeFromSuperview implementation UIScrollView triggers -setContentOffset: and, eventually, send message to delegate.
Just add following lines at the beginning of dealloc method:
sellersTableView.delegate = nil;
sellersTableView.dataSource = nil;
No need to use hacks like your killScroll method.
Also, I can't see why you want to call both popToRootViewController and dismissViewController.
If you dismiss a view controller which is embedded in a navigation controller, navigation controller itself as well as all contained view controllers will be released.
In your case you'll have just weird animation.
setContentOffset method won't help you, try to set
sellersTableView.dataSource = nil;
somewhere in your viewWillDisappear method.
This is not a good practice of course.
Change you closeViewController like below and see if works
(void) closeViewController
{
sellersTableView.dataSource = nil;
sellersTableView.delegate = nil;
[self.navigationController popToRootViewControllerAnimated:YES];
[self dismissViewControllerAnimated:YES completion:nil];
}
I don't think that setting the tableView (or it's delegate) to nil is the issue. You should be able to perform both dismissViewControllerAnimated or popToRootViewController individually without having to modify the tableView in this way.
So the issue is most likely due to calling both of these methods at the same time (and with animated = YES), and in doing so asking your viewController setup to do something unnatural.
Looks like upon tapping a "close" button you are both popping to a rootViewController of a UINavigationController, as well as dismissing a modal viewController.
In doing so, you're dismissing a modal viewController which is likely presented by the topViewController of the navigationController (so top vc is holding a reference to modal vc). AND you're trying to kill the top vc via the popToRootViewController method call. And you're doing both of these things using animated = YES, which means they take some time to complete, and you can't be sure when each finishes (ie you can't be sure when dealloc will be called).
Depending on your needs you could do one of several things.
Consider adding a delegate property to your modal vc. Dismiss the modal vc, and in the completionBlock of the modal vc tell its delegate that it's finished dismissing. At that point call popToRootViewController (because at this point you can be sure that the modal is gone and scrolling wasn't interrupted).
If it's your navController that's been presented modally, then do this in the opposite order. Notifying the delegate that the pop operation has completed, and do the modal dismissal then.

Detect tap on CCTableView cell

I am using a CCTableView to make a table with CCNodes as cells. Those CCNodes have a button each. I want to be able to detect if a user taps on a cell and if it taps on the button. But the CCTableView doesn't have a tableView:didSelectRowAtIndexPath: method so how can I do this? Do you know of any open source class that has this method?
P.S. I am using version 3 of cocos2d
I took a different approach after trying numerous things
#interface WKTableCell : CCTableViewCell
#end
#implementation WKTableCell
- (instancetype) initWithTitle: (NSString *) title
{
self = [super init];
if (!self)
return nil;
[self.button setTitle:title];
// This is a transparent png (400x200) for my needs
CCSpriteFrame * frame = [CCSpriteFrame frameWithImageNamed:#"cell.png"] ;
[self.button setPreferredSize:CGSizeMake(frame.originalSize.width, frame.originalSize.height)];
[self.button setContentSizeType:CCSizeTypePoints];
[self.button setBackgroundSpriteFrame:frame forState:CCControlStateNormal];
}
// then in your table
[table setBlock:^(id sender) {
CCLOG(#"yup, this gets called.. ");
}];
this did work for me..
Your CCTableView, responds to the CCTouchDelegate, so you can use ccTouchBegan etc
to detect the point and then calculate what cell there was in that point. Here the class reference:
http://docs.huihoo.com/doxygen/cocos2d-x/2.1.2/d0/d38/classcocos2d_1_1_c_c_touch_delegate.html

UITextView not shown on screen

In my app I habe a view controller that calls several views. All these views are UIViews. That works fine, but not in every case. One of the views that are called has some labels, textfields and two UITextViews. Everything is shown correctly but the UITextViews. The view is called in that way:
[[self view] addSubview:tasteView];
//tasteView = [[TasteView alloc] init];
[self setCurrentView:tasteView];
I call the init method of the view to display the UITextViews:
EDIT: After a comment of Phillip Mills this was slightly changed! Init isn't called anymore.
- (id)init
{
if (self)
{
[tv1 setNeedsDisplay];
CGRect frame = tv1.frame;
frame.size.height += 1;
tv1.frame = frame;
}
return self;
}
As I saw that setNeedsDisplay had no effect, I changed the size of the corresponsing frame to force a redraw. Unfortunately that had no effect, too.
Btw, the view is initially loaded in the viewDidLoad of the view controller:
- (void)viewDidLoad
{
[super viewDidLoad];
[self setCurrentView:placeholder];
[self configureView];
wineryView = [self loadWineryView];
wineView = [self loadWineView];
tasteView = [self loadTasteView];
}
A method for loading the views looks like this:
- (UIView *) loadTasteView
{
NSArray *nibViews = [[NSBundle mainBundle] loadNibNamed:#"TasteView" owner:self options:nil];
UIView *tView;
for (id view in nibViews)
{
if ([view isKindOfClass:[TasteView class]])
{
tView = (TasteView*) view;
}
}
return tView;
}
I do not know why those UITextViews are not shown. Did I forget something? To show really everything, here are the connections that I made in InterfaceBuilder:
Does anyone know what I did wrong and can help me?
I think your initial code should be like this :
tasteView = [[TasteView alloc] init];
[[self view] addSubview:tasteView];
[self setCurrentView:tasteView];
addSubView after it is allocated
Hope it helps you
If you are creating the view in code (your first sample), alloc and init the view before trying to add it as a subview.
If you're loading it from another nib (last code section), you still need to add it to the view hierarchy.

UIButton in cell in collection view not receiving touch up inside event

The following code expresses my problem:
(It's self-contained in that you could create a Xcode project with an empty template, replace the contents of the main.m file, delete the AppDelegate.h/.m files and build it)
//
// main.m
// CollectionViewProblem
//
#import <UIKit/UIKit.h>
#interface Cell : UICollectionViewCell
#property (nonatomic, strong) UIButton *button;
#property (nonatomic, strong) UILabel *label;
#end
#implementation Cell
- (id)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame])
{
self.label = [[UILabel alloc] initWithFrame:self.bounds];
self.label.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.label.backgroundColor = [UIColor greenColor];
self.label.textAlignment = NSTextAlignmentCenter;
self.button = [UIButton buttonWithType:UIButtonTypeInfoLight];
self.button.frame = CGRectMake(-frame.size.width/4, -frame.size.width/4, frame.size.width/2, frame.size.width/2);
self.button.backgroundColor = [UIColor redColor];
[self.button addTarget:self action:#selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
[self.contentView addSubview:self.label];
[self.contentView addSubview:self.button];
}
return self;
}
// Overriding this because the button's rect is partially outside the parent-view's bounds:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
if ([super pointInside:point withEvent:event])
{
NSLog(#"inside cell");
return YES;
}
if ([self.button
pointInside:[self convertPoint:point
toView:self.button] withEvent:nil])
{
NSLog(#"inside button");
return YES;
}
return NO;
}
- (void)buttonClicked:(UIButton *)sender
{
NSLog(#"button clicked!");
}
#end
#interface ViewController : UICollectionViewController
#end
#implementation ViewController
// (1a) viewdidLoad:
- (void)viewDidLoad
{
[super viewDidLoad];
[self.collectionView registerClass:[Cell class] forCellWithReuseIdentifier:#"ID"];
}
// collection view data source methods ////////////////////////////////////
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 100;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
Cell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"ID" forIndexPath:indexPath];
cell.label.text = [NSString stringWithFormat:#"%d", indexPath.row];
return cell;
}
///////////////////////////////////////////////////////////////////////////
// collection view delegate methods ////////////////////////////////////////
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"cell #%d was selected", indexPath.row);
}
////////////////////////////////////////////////////////////////////////////
#end
#interface AppDelegate : UIResponder <UIApplicationDelegate>
#property (strong, nonatomic) UIWindow *window;
#end
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
ViewController *vc = [[ViewController alloc] initWithCollectionViewLayout:layout];
layout.itemSize = CGSizeMake(128, 128);
layout.minimumInteritemSpacing = 64;
layout.minimumLineSpacing = 64;
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
layout.sectionInset = UIEdgeInsetsMake(32, 32, 32, 32);
self.window.rootViewController = vc;
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
#end
int main(int argc, char *argv[])
{
#autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
Basically I'm creating a Springboard-type UI using collection views. My UICollectionViewCell subclass (Cell) has a button which lies partially outside the cell's contentView (i.e. its superview's) bounds.
The problem is that clicking on any part of the button outside of the contentView bounds (basically 3/4th of the button) doesn't invoke the button action. Only when clicking on the portion of the button that overlaps the contentView is the button's action method called.
I've even overridden -pointInside:withEvent: method in Cell so that touches in the button will be acknowledged. But that hasn't helped with the button clicking problem.
I'm guessing it might be something to do with how collectionView handles touches, but I don't know what. I know that UICollectionView is a UIScrollView subclass and I've actually tested that overriding -pointInside:withEvent: on a view (made subview to a scroll view) containing a partially overlapping button solves the button clicking problem, but it hasn't worked here.
Any help?
** Added:
For the record, my current solution to the problem involves insetting a smaller subview to contentView which gives the cell its appearance. The delete button is added to the contentView such that its rect actually lies within the bounds of contentView but only partially overlaps the visible part of the cell (i.e. the inset subview). So I've got the effect I wanted, and the button is working properly. But I'm still curious about the problem with the original implementation above.
The problem appears to be with hitTest/pointInside. I'm guessing the cell is returning NO from pointInside if the touch is on the part of the button that is outside the cell and thus the button doesn't get hit tested. To fix this you have to override pointInside on your UICollectionViewCell subclass to take the button into account. You also need to override hitTest to return the button if the touch is inside the button. Here are example implementations assuming your button is in a property in the UICollectionViewCell subclass called deleteButton.
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *view = [self.deleteButton hitTest:[self.deleteButton convertPoint:point fromView:self] withEvent:event];
if (view == nil) {
view = [super hitTest:point withEvent:event];
}
return view;
}
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
if ([super pointInside:point withEvent:event]) {
return YES;
}
//Check to see if it is within the delete button
return !self.deleteButton.hidden && [self.deleteButton pointInside:[self.deleteButton convertPoint:point fromView:self] withEvent:event];
}
Note that because hitTest and pointInside expect the point to be in the coordinate space of the receiver you have to remember to convert the point before calling those methods on the button.
In Interface Builder do you have set the object as UICollectionViewCell? Because erroneously one time I set a UIView and after assign to it the correct UICollectionViewCell class...but doing this things (buttons, labels, ecc.) are not added tor the contentView so they don't respond as they would...
So, remind in IB to take the UICollectionViewCell Object when drawing the interface :)
Swift version:
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
//From higher z- order to lower except base view;
for (var i = subviews.count-2; i >= 0 ; i--){
let newPoint = subviews[i].convertPoint(point, fromView: self)
let view = subviews[i].hitTest(newPoint, withEvent: event)
if view != nil{
return view
}
}
return super.hitTest(point, withEvent: event)
}
that's it ... for all subViews
I am successfully receiving touches to a button created as follows in the subclassed UICollectionViewCell.m file;
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self)
{
// Create button
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = CGRectMake(0, 0, 100, 100); // position in the parent view and set the size of the button
[button setTitle:#"Title" forState:UIControlStateNormal];
[button setImage:[UIImage imageNamed:#"animage.png"] forState:UIControlStateNormal];
[button addTarget:self action:#selector(button:) forControlEvents:UIControlEventTouchUpInside];
// add to contentView
[self.contentView addSubview:button];
}
return self;
}
I added the button in code after realising that buttons added in Storyboard did not work, not sure if this is fixed in latest Xcode.
Hope that helps.
I see two swift conversions of the original answer that aren't exactly swift conversions. So I just want to give the Swift 4 conversion of the original answer so everyone who wants to can use it. You can just paste the code into your subclassed UICollectionViewCell. Just make sure that you change closeButton with your own button.
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
var view = closeButton.hitTest(closeButton.convert(point, from: self), with: event)
if view == nil {
view = super.hitTest(point, with: event)
}
return view
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if super.point(inside: point, with: event) {
return true
}
return !closeButton.isHidden && closeButton.point(inside: closeButton.convert(point, from: self), with: event)
}
As accepted answer requested, we should make a hitTest in order to recieve touches inside the cell. Here is the Swift 4 code for hit test:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
for i in (0..<subviews.count-1).reversed() {
let newPoint = subviews[i].convert(point, from: self)
if let view = subviews[i].hitTest(newPoint, with: event) {
return view
}
}
return super.hitTest(point, with: event)
}
I had a similar problem trying to place a deletion button outside the bounds of a uicollectionview cell and it didn't seam to respond to tap events.
the way i solved it was to place a UITapGestureRecognizer on the collection and when a tap happend preform the following code
//this works also on taps outside the cell bouns, im guessing by getting the closest cell to the point of click.
NSIndexPath* tappedCellPath = [self.collectionView indexPathForItemAtPoint:[tapRecognizer locationInView:self.collectionView]];
if(tappedCellPath) {
UICollectionViewCell *tappedCell = [self.collectionView cellForItemAtIndexPath:tappedCellPath];
CGPoint tapInCellPoint = [tapRecognizer locationInView:tappedCell];
//if the tap was outside of the cell bounds then its in negative values and it means the delete button was tapped
if (tapInCellPoint.x < 0) [self deleteCell:tappedCell];
}
Honus has the best answer here in my opinion. In fact the only one that worked for me so I've been answering other similar questions and sending them this way:
I spent hours scouring the web for a solution to my UIButton's inside UICollectionView's not working. Driving me nuts until I finally found a solution that works for me. And I believe it's also the proper way to go: hacking the hit tests. It's a solution that can go a lot deeper (pun intended) than fixing the UICollectionView Button issues as well, as it can help you get the click event to any button buried under other views that are blocking your events from getting through:
UIButton in cell in collection view not receiving touch up inside event
Since that SO answer was in Objective C, I followed the clues from there to find a swift solution:
http://khanlou.com/2018/09/hacking-hit-tests/
--
When I would disable user interaction on the cell, or any other variety of answers I tried, nothing worked.
The beauty of the solution I posted above is that you can leave your addTarget's and selector functions how you are used to doing them since they were most likey never the problem. You need only override one function to help the touch event make it to its destination.
Why the solution works:
For the first few hours I figured the gesture wasn't being registered properly with my addTarget calls. It turns out the targets were registering fine. The touch events were simply never reaching my buttons.
The reality seems to be from any number of SO posts and articles I read, that UICollectionView Cells were meant to house one action, not multiple for a variety of reasons. So you were only supposed to be using the built in selection actions. With that in mind, I believe the proper way around this limitation is not to hack UICollectionView to disable certain aspects of scrolling or user interaction. UICollectionView is only doing its job. The proper way is to hack the hit tests to intercept the tap before it gets to UICollectionView and figure out which items they were tapping on. Then you simply send a touch event to the button they were tapping on, and let your normal stuff do the work.
My final solution (from the khanlou.com article) is to put my addTarget declaration and my selector function wherever I like (in the cell class or the cellForItemAt override), and in the cell class overriding the hitTest function.
In my cell class I have:
#objc func didTapMyButton(sender:UIButton!) {
print("Tapped it!")
}
and
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard isUserInteractionEnabled else { return nil }
guard !isHidden else { return nil }
guard alpha >= 0.01 else { return nil }
guard self.point(inside: point, with: event) else { return nil }
// add one of these blocks for each button in our collection view cell we want to actually work
if self.myButton.point(inside: convert(point, to: myButton), with: event) {
return self.myButton
}
return super.hitTest(point, with: event)
}
And in my cell class init I have:
self.myButton.addTarget(self, action: #selector(didTapMyButton), for: .touchUpInside)
I found this from here py4u.net
tried the solution that was at the very bottom. (the whole stuff was collected from this page, as I understand)
In my case the colution also worked. and then I just checked wether the User Interaction Enabled checkmark is checked on Collection view in xib and in it's contentView. Guess what. the contentView`s UserInteraction was disabled.
Enabling it fixed the issue with button's touchUpInside event and there was no need to override hitTest method.
You might not have to override hit test or do any of the complicated solutions above.
This issue can also be caused if you are using a custom button with lots of subviews.
What happens is when the button is hit tested, one of its subviews is being returned.
A cleaner solution here is just to set userInteractionEnabled = false on all of your button's subviews.
That way, hit testing your button will only ever return the button itself and none of the views on it.

Resources