Basically what i want to do is to have a cell in a UICollectionView, which part of it will respond to tap, and part of it won't.
This cell will be a normal cell that is expanded when tapped, showing a subview on the expanded area that doesn't respond to tap.
I considered expanding the cell and adding a subview above it, that wouldn't be clickable. But i think that is kind of a "bad" way of accomplishing it.
Is there a best way of doing this?
This is what i want to do:
Any help is appreciated, thanks!
You need subclass UICollectionViewCell, then override hitTest:withEvent rather than pointInside:withEvent, inside hitTest:withEvent: you should define the targetRect, then check if the point user clicked is inside of targetRect.
Here is the code which works on my tableViewCell (similar for collectionViewCell).
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
CGRect targetRect = CGRectMake(0, 0, CGRectGetWidth(self.bounds), 20);
if (CGRectContainsPoint(targetRect, point)) {
return nil;
} else {
return [super hitTest:point withEvent:event];
}
}
With in the selector
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath;
you must return a cell, and in this function you should determine whether or not the cell is clickable and then simply do
cell.userInteractionEnabled = shouldAllowTouch;
Related
First take a look at the attacthed image:
My hiearchy looks as following:
UIView
UIButton
UICollectionView
UICollectionViewCell
UICollectionViewCell
UICollectionViewCell
UICollectionViewCell
UICollectionView is added to UIVew as H:[collectionView(==270)]| and V:|-(70)-[collectionView]-(70)-|.
I have two requirements:
1) only the blue part of the UICollectionViewCell should trigger the collectionView::didSelectItemAtIndexPath: method. Which I have sucesfully implemented with
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *hitView = [super hitTest:point withEvent:event];
if (hitView == self.customView) {
return self;
}
return nil;
}
on the custom class UIColectionViewCell. The customView property is the blue/red UIView of the cell.
This works as expected.
2) I want any gestures (Tap/LongTap/Pan/...) that are done in the green part of the UICollectionViewCell or in the UICollectionVIew itself (the background that is here white with 0.5 opacity) to be passed down to the superview. For Example the turquoise UIButtion below.
The green part should also not scroll the CollectionView .. it just has to be completly transparent to any gestures.
How can I do that? Tried a lot of different approaches but none of them worked. I know how to do it with a regular UIView, but cannot get it to work on a UICollectionView and its cells.
Keep in mind that there will be other UIViews or UIButtions that will be interactable and placed under the green part of collection view. The green color will later just be UIClearColor.
Suggestion to just have the UICollectionView smaller width (width of the blue part) is not an option since the Blue/Red part of UICell has to strecth out to full width of the cell in some cases.
Here is the final solution for above example:
1) Require only red/blue part to be tappable.
This stays basically the same. I have a reference to the blue/red UIView names customView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *hitView = [super hitTest:point withEvent:event];
if (hitView == self.customView) {
return self;
}
return nil;
}
2) Any interactions inside the UICollectionView that are not done on blue/red UIViews should be ignored.
Subclassing UICollectionView and adding overwriting this metod:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
NSIndexPath *indexPath = [self indexPathForItemAtPoint:point];
LCollectionViewCell *cell = (LCollectionViewCell*)[self cellForItemAtIndexPath:indexPath];
if (cell && [self convertPoint:point toView:cell.customView].x >= 0) {
return YES;
}
return NO;
}
This will check if the CGPoint is done on the customView. With this we are also supporting variable widths:
Your answer helped me a ton, thanks!
Here's the solution in Swift 3.0:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
if hitView == self.customView {
return self
}
return nil
}
I overrode hitTest, and that works just fine. I want it to behave as if I hadn't overridden this method under certain conditions, and that's where the problem lies.
I'm using a subclassed UICollectionView to render cells over a MKMapView using a custom UICollectionViewLayout implementation. I needed to override hitTest in the UICollectionView subclass so that touch events can be passed to the mapView and it can be scrolled. That all works fine.
I have a toggle mechanism which animates between my UICollectionViewLayout (map) to a UICollectionViewFlowLayout (animate items on a map to a grid format). This works good too, but when I'm showing the flow layout, I want the user to be able to scroll the UICollectionView like a normal one (act as though hitTest isn't overridden). I can't figure out what to return in hitTest to have it's default behavior.
-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
if(self.tapThrough == YES){
NSArray *indexPaths = [self indexPathsForVisibleItems];
for(NSIndexPath *indexPath in indexPaths){
UICollectionViewCell *cell = [self cellForItemAtIndexPath:indexPath];
if(CGRectContainsPoint(cell.frame, point)){
return cell;
}
}
return nil;
} else {
return ???
}
}
I've tried returning a number of things. self, self.superview, etc... Nothing get it to behave normally (I cannot scroll the cells up and down).
If you want the normal behaviour of your hit test:
return [super hitTest:point withEvent:event];
This will return what the hit test usually returns when it is not overridden.
I have a UICollectionView that has elements that can be dragged and dropped around the screen. I use a UILongPressGestureRecognizer to handle the dragging. I attach this recognizer to the collection view cells in my collectionView:cellForItemAtIndexPath: method. However, the recognizer's view property occasionally returns a UIView instead of a UICollectionViewCell. I require some of the methods/properties that are only on UICollectionViewCell and my app crashes when a UIView is returned instead.
Why would the recognizer that is attached to a cell return a plain UIView?
Attaching the recognizer
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
EXSupplyCollectionViewCell *cell = (EXSupplyCollectionViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
UILongPressGestureRecognizer *longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:cell action:nil];
longPressRecognizer.delegate = self;
[cell addGestureRecognizer:longPressRecognizer];
return cell;
}
Handling the gesture
I use a method with a switch statement to dispatch the different states of the long press.
- (void)longGestureAction:(UILongPressGestureRecognizer *)gesture {
UICollectionViewCell *cell = (UICollectionViewCell *)[gesture view];
switch ([gesture state]) {
case UIGestureRecognizerStateBegan:
[self longGestureActionBeganOn:cell withGesture:gesture];
break;
//snip
default:
break;
}
}
When longGestureActionBeganOn:withGesture is called if cell is actually a UICollectionViewCell the rest of the gesture executes perfectly. If it isn't then it breaks when it attempts to determine the index path for what should be a cell.
First occurrence of break
- (void)longGestureActionBeganOn:(UICollectionViewCell *)cell withGesture:(UILongPressGestureRecognizer *)gesture
{
NSIndexPath *indexPath = [self.collectionView indexPathForCell:cell]; // unrecognized selector is sent to the cell here if it is a UIView
[self.collectionView setScrollEnabled:NO];
if (indexPath != nil) {
// snip
}
}
I also use other properties specific to UICollectionViewCell for other states of the gesture. Is there some way to guarantee that the recognizer will always give me back the view that I assigned it to?
Views like UICollectionView and UITableView will reuse their cells. If you blindly add a gestureRecognizer in collectionView:cellForItemAtIndexPath: you will add a new one each time the cell is reloaded. If you scroll around a bit you will end up with dozens of gestureRecognizers on each cell.
In theory this should not cause any problems besides that the action of the gestureRecognizer is called multiple times. But Apple uses heavy performance optimization on cell reuse, so it might be possible that something messes up something.
The preferred way to solve the problem is to add the gestureRecognizer to the collectionView instead.
Another way would be to check if there is already a gestureRecognizer on the cell and only add a new one if there is none. Or you use the solution you found and remove the gestureRecognizer in prepareForReuse of the cell.
When you use the latter methods you should check that you remove (or test for) the right one. You don't want to remove gestureRecognizers the system added for you. (I'm not sure if iOS currently uses this, but to make your app proof for the future you might want to stick to this best practice.)
I had a similar problem related to Long-Touch.
What I ended up doing is override the UICollectionViewCell.PrepareForReuse and cancel the UIGestureRecognizers attached to my view. So everytime my cell got recycled a long press event would be canceled.
See this answer
I'm trying to add a UIScrollView inside of a UICollectionViewCell. The idea is that you can use pinch to zoom the UIScrollView (and with it, the image within), but the scrollview doesn't seem to handle any gesture. I'm guessing they are being caught by the UICollectionView.
I've set the delegate of the UIScrollView to be the UICollectionViewCell, but none of the delegate methods are being called.
EDIT:
I've made a github repo with the code (simplified as much as I could). Even though it's just a few lines of code, I cannot see what I did wrong.
EDIT2: After the answer was found, I added the fixes to the above-mentioned repo, hope others find it helpful too :)
https://github.com/krummler/gallery-pinchzoom-example
I just made an implementation for SWIFT 3 on iOS 9.3+ and all i done was:
1. Place the image view inside a scrollview
2. Connect the scrollview delegate to collectionview cell class
3. Implement the code below on the collectionview subclass
class FullScreenImageTextDetailCollectionViewCell: UICollectionViewCell, UIScrollViewDelegate {
#IBOutlet var scrollview: UIScrollView!
#IBOutlet weak var backgroundImageView: UIImageView!
override func awakeFromNib() {
self.scrollview.minimumZoomScale = 0.5
self.scrollview.maximumZoomScale = 3.5
self.scrollview.delegate = self
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return self.backgroundImageView
}
}
No gesture recognizer adding or removing on the parent collectionview controller was necessary, worked like a charm!
Thanks for previous examples for reaching this!
You might want to try manipulating the UIGestureRecognizers in order to do that. In the GalleryViewController:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
GalleryImageCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"galleryImageCell" forIndexPath:indexPath];
ImageContext *imageContext = [self.images objectAtIndex:indexPath.row];
cell.imageContext = imageContext;
[self.collectionView addGestureRecognizer:cell.scrollView.pinchGestureRecognizer];
[self.collectionView addGestureRecognizer:cell.scrollView.panGestureRecognizer];
return cell;
}
From Apple's documentation on UIView:
Attaching a gesture recognizer to a view defines the scope of the represented gesture, causing it to receive touches hit-tested to that view and all of its subviews. The view retains the gesture recognizer.
So you'll also want to make sure to remove them when the cell is not showing anymore.
- (void)collectionView:(UICollectionView *)collectionView
didEndDisplayingCell:(UICollectionViewCell *)cell
forItemAtIndexPath:(NSIndexPath *)indexPath {
// Get the cell instance and ...
[self.collectionView removeGestureRecognizer:cell.scrollView.pinchGestureRecognizer];
[self.collectionView removeGestureRecognizer:cell.scrollView.panGestureRecognizer];
}
Since you're not modifying the UIGestureRecognizer's delegate, only its scope, it will still control the zooming of just that cell's scrollview.
EDIT:
I added the panGestureRecognizer to the above examples, following a suggestion from the OP that it was needed. The zooming itself is completely handled by the pinchGestureRecognizer, but it's true that in most cases, after zooming an image to a point where only a subset of it is visible, you'll want to pan to move the visible portion around. That is, it's part of a proper zooming experience.
Please add scrollview in your cell and your current cell image view add in scrollview.
then use below code:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
ImageContext *context = [self.images objectAtIndex:indexPath.row];
ImageCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"imageCell" forIndexPath:indexPath];
cell.cellScrollView.autoresizesSubviews = YES;
cell.cellScrollView.multipleTouchEnabled =YES;
cell.cellScrollView.maximumZoomScale = 4.0;
cell.cellScrollView.minimumZoomScale = 1.0;
cell.cellScrollView.clipsToBounds = YES;
cell.cellScrollView.delegate = self;
cell.cellScrollView.zoomScale = 1.0;
[cell.imageView setImage:[UIImage imageNamed:context.thumbImageUrl]];
return cell;
}
-(UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{ NSLog(#"%i",scrollView.subviews.count);
for (UIView *v in scrollView.subviews) {
if ([v isKindOfClass:[UIImageView class]]) {
return v;
}
}
}
Check if all related view's multitouch is on. iOS disabled multitouch by default on most views to save energy.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if (touch.view == [self myScrollView]) //<- whatever your scrollView is called
{
return YES;
}
return NO;
}
Don't know your code but try playing around with the above code to see if you can filter your touches to the objects you want. The above code is from the UIGestureRecognizerDelegate Protocol Reference.
The delegates are not getting called because you added it inside the UICollectionview.The touch events are availabel for the collection view which is the superview and hence not obtainable by the view inside.You may have to think of some other way to achieve this model.
UICollectionView is having the same pattern as UITableView,The problem occours in tableview inside scrollview ,that the touch event is accepted by scrollview[which is the superview] and to make the tableview scrollable in that case ,The scroll of scrollview must be disabled.This is another version of that problem
The apple docs says that the above said case make unpredictable results.So This may be same for your problem.
In my opinion you must go for better design which can achieve what you look for
I've set the delegate of the UIScrollView to be the UICollectionViewCell, but none of the delegate methods are being called.
For that just define one function in collectionViewCell
#IBOutlet weak var mainScrollView:UIScrollView!
func setup(){
mainScrollView.minimumZoomScale=1
mainScrollView.maximumZoomScale=10.0
mainScrollView.delegate=self
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return yourimageviewinside of scrollview
}
call this fuction in
collectionview(_,cellForItem ) method of collectionview
I have an ImageView, say imageViewA, in a UICollectionViewCell, say ComicStripViewCellA.xib. I need to reposition imageViewA if a condition is true. I can access imageViewA or change its alpha value in collectionView:cellForItemAtIndexPath, but cannot reposition it if this is the first time that this viewCell is dequeued.
But if this viewCell is already in the pool, I can reposition it the next time when collectionView:cellForItemAtIndexPath is executed.
-(UICollectionView *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
id <ComicStripViewCell> viewCell = [collectionView dequeueReusableCellWithReuseIdentifier:#"comicStripCell" forIndexPath:indexPath];
if (condition) {
viewCell.imageViewA.alpha = 0.5; // Always work.
[viewCell reposition:YES]; // Only work if viewCell is already in the pool.
}
return (UICollectionView *)viewCell;
}
The following routine is in ComicStripViewCellA.m:
-(void)reposition:(BOOL)flag
{
if (flag) {
CGPoint center = _imageViewA.center;
center.x -= 60;
_imageViewA.center = center;
}
}
I guess it has something to do with the view cell initialization not complete when the reposition routine is run, but I am not sure.
Any help is appreciated!
Thanks!
I've been experimenting around, it would appear that the UICollectionViewCell outlets can't be repositioned programmatically (or at least I couldn't get them to reposition, no matter what). One possible solution, however, is to have a simple UIView as the only outlet, and to add subviews to that outlet programmatically, in your case the UIImageView. Once you do that, you can freely reposition any subview in your UIView outlet. I hope this helps!