UITableView has the method rectForRowAtIndexPath:, but this does not exist in UICollectionView. I'm looking for a nice clean way to grab a cell's bounding rectangle, perhaps one that I could add as a category on UICollectionView.
The best way I've found to do this is the following:
Objective-C
UICollectionViewLayoutAttributes *attributes = [self.collectionView layoutAttributesForItemAtIndexPath:indexPath];
Swift
let attributes = collectionView.layoutAttributesForItem(at: indexPath)
Then you can access the location through either attributes.frame or attributes.center
Only two lines of code is required to get perfect frame :
Objective-C
UICollectionViewLayoutAttributes * theAttributes = [collectionView layoutAttributesForItemAtIndexPath:indexPath];
CGRect cellFrameInSuperview = [collectionView convertRect:theAttributes.frame toView:[collectionView superview]];
Swift 4.2
let theAttributes = collectionView.layoutAttributesForItem(at: indexPath)
let cellFrameInSuperview = collectionView.convert(theAttributes.frame, to: collectionView.superview)
in swift 3
let theAttributes:UICollectionViewLayoutAttributes! = collectionView.layoutAttributesForItem(at: indexPath)
let cellFrameInSuperview:CGRect! = collectionView.convert(theAttributes.frame, to: collectionView.superview)
in swift you can just do:
//for any cell in collectionView
let rect = self.collectionViewLayout.layoutAttributesForItemAtIndexPath(clIndexPath).frame
//if you only need for visible cells
let rect = cellForItemAtIndexPath(indexPath)?.frame
How about
-(CGRect)rectForCellatIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [self cellForItemAtIndexPath:indexPath];
if (!cell) {
return CGRectZero;
}
return cell.frame;
}
as a category on UICollectionView?
#import <UIKit/UIKit.h>
#interface UICollectionView (CellFrame)
-(CGRect)rectForCellatIndexPath:(NSIndexPath *)indexPath;
#end
#import "UICollectionView+CellFrame.h"
#implementation UICollectionView (CellFrame)
-(CGRect)rectForCellatIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [self cellForItemAtIndexPath:indexPath];
if (!cell) {
return CGRectZero;
}
return cell.frame;
}
#end
Related
I'm getting all the visible cells in a UICollectionViewController like this.
NSArray<__kindof UICollectionViewCell *> *cells = [self.collectionView.visibleCells visibleCells];
How do I find the frame for the cell in the window?
Try this:
Swift
guard
let indexPath = collectionView.indexPath(for: yourVisibleCell),
let attributes = collectionView.layoutAttributesForItem(at: indexPath)
else {
assertionFailure("Can't get required attributes")
return
}
let frameInWindow = collectionView.convert(attributes.frame, to: nil)
Objective C
NSIndexPath *indexPath = [self.collectionView indexPathForCell:yourVisibleCell];
UICollectionViewLayoutAttributes *theAttributes = [collectionView layoutAttributesForItemAtIndexPath: indexPath];
CGRect cellFrameInSuperview = [collectionView convertRect:theAttributes.frame toView:nil];
Xcode friendly Swift variant of iPrabu's answer:
var screenCellFrame: CGRect? {
if let cell = <#visibleCell#>, let indexPath = collectionView?.indexPath(for: cell), let attributes = collectionView!.layoutAttributesForItem(at: indexPath) {
return collectionView!.convert(attributes.frame, to: nil)
} else {
return nil
}
}
I am trying to populate a Tableview with data from an array. I am trying to make a custom table view cell that includes a big square image, same width as the screen and the height should also be the same size as the screen's width. And under the image I want to add a text label. (You can think of it as a very bad copy of instagram)
However, the tableview only shows empty standard cells.
This is my custom cell code:
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self)
{
customImageView.contentMode = UIViewContentModeScaleAspectFit;
CGRect frame;
frame.origin.x=0;
frame.origin.y=0;
frame.size.width=self.frame.size.width;
frame.size.height=self.frame.size.width;
customImageView.frame=frame;
[customTextLabel sizeToFit];
frame.origin = CGPointMake(0, (self.frame.size.width+1));
customTextLabel.frame = frame;
customTextLabel.numberOfLines = 0;
customTextLabel.lineBreakMode = NSLineBreakByCharWrapping;
[self.contentView addSubview:customImageView];
[self.contentView addSubview:customTextLabel];
[self.contentView sizeToFit]; } return self;}
and the code from the tableview:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyCellIdentifier = #"MyCellIdentifier";
//UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:MyCellIdentifier];
FeedTableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:MyCellIdentifier];
if(cell == nil) {
cell = [[FeedTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MyCellIdentifier];
}
if(!emptyTable){
Log *log = [self.feedPosts objectAtIndex:indexPath.section];
cell.customImageView.image = [self loadImage:(int)log.logID];
cell.customTextLabel.text = log.logDescription;
}
return cell;}
Better solution is to use constraints
create constraints like on image below
Create outlets for your height width ( control drag your constraint to cell class)
Change it to screen size/ height on your
cell
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! TableViewCell
cell.heightConstraint.constant = UIScreen .mainScreen().bounds.height;
cell.widthConstraint.constant = UIScreen.mainScreen().bounds.width;
return cell
}
Don't forget to set height for row
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UIScreen .mainScreen().bounds.height;
}
Also, you can recreate it on your code programmatically, if you want
Hope this helps
Where are you allocate memory for the cell.customImageView and cell.customTextLabel? This problem should be caused by not generating customeImageView and customeTextLabel. you should according to the following code to modify:
//Setter/Getter
//allocate memory and init customImageView/customTextLable
- (UIImageView *)customeImageView{
if(!_ customImageView)_customImageView = [UIImageView new];
return _customImageView;
}
- (UILable *)customTextLabel{
if(!_customTextLable) _customTextLabel = [UIlable new];
return _cusomTextLable;
}
Then invoke them when initWithStyle:reuseIdentifier:.The other thing should be noticed is that tableView should custome cell's height by implement tableView:heightForRowAtIndexPath:
I almost did what DarkHorse said.
I forgot to do:
customImageView = [[UIImageView alloc] init];
customTextLabel = [[UILabel alloc] init];
and then set the height in
tableView:heightForRowAtIndexPath
I want to locate the frame of section header in UICollectionView. I have a similar situation for UITableView, and for that, I was able to get its rect by doing:
CGRect rect = [self.tableView rectForHeaderInSection:section];
Is there a way to get the same result in UICollectionView? Thanks!
Sorry, guys. It was actually easy.
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:section];
UICollectionViewLayoutAttributes *attributes = [self.collectionView layoutAttributesForSupplementaryElementOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];
CGRect rect = [attributes frame];
Swift version of #tsuyoski's answer:
let section = 0 // or whatever section you're interested in
let indexPath = IndexPath(item: 0, section: section)
let attributes = collectionView.layoutAttributesForSupplementaryElement(ofKind: UICollectionView.elementKindSectionHeader, at: indexPath)
guard let rect = attributes?.frame else { return }
i want to animate the UICollectionViewCell when action is called.
i have done UICollectionViewCell in Interface Builder, the UICollectionView also.
Now i want to get the correct indexPath at my actionBtnAddToCard method.
thats the way i try it now (method in ProduktViewCell.m):
- (IBAction)actionAddToCart:(id)sender {
XLog(#"");
// see this line
NSIndexPath *indexPath = ??** how can i access the correct indexPath**??;
SortimentViewController *svc = [[SortimentViewController alloc] initWithNibName:#"SortimentViewController_iPad" bundle:[NSBundle mainBundle]];
[svc.collectionViewProdukte cellForItemAtIndexPath:indexPath];
[svc collectionView:svc.collectionViewProdukte didSelectItemAtIndexPath:indexPath];
}
SortimentViewController is the viewController which inherits the UICollectionView.
how to acces the correct indexPath?
UPDATE 1: edited post for better understanding.
- (IBAction)actionAddToCart:(id)sender {
NSIndexPath *indexPath;
indexPath = [self.collectionView indexPathForItemAtPoint:[self.collectionView convertPoint:sender.center fromView:sender.superview]];
...
}
if you know the view hierarchy it is easy.
UIButton *button = (UiButton *) sender;
if the button is like this - > UITableViewCell - > button
then you can get cell like this
UITableViewCell *cell = (UITableViewCell *)[button superview];
if the button is like this - > UITableViewCell - > content view -> button
UITableViewCell *cell = (UITableViewCell *)[[button superview] superview];
and finally index path can be extracted like this
NSIndexPath *indexPath = [self.table_View indexPathForCell:cell];
Do Not Depend on view.
Try this.
CGPoint buttonPosition = [sender convertPoint:CGPointZero toView:self.collectionView];
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:buttonPosition];
NSLog(#"%ld", (long)indexPath.row);
Using code like [[button superview] superview] is fragile and not future-proof; indeed, it's not even guaranteed to work on all iOS versions unless you explicitly test it. I always use an iterative helper method for this purpose:-
- (UIView *)superviewWithClassName:(NSString *)className fromView:(UIView *)view
{
while (view)
{
if ([NSStringFromClass([view class]) isEqualToString:className])
{
return view;
}
view = view.superview;
}
return nil;
}
Then I call it from the button handler like so:-
- (IBAction)buttonClicked:(id)sender
{
UIButton *button = (UIButton *)sender;
UICollectionViewCell *cell = (UICollectionViewCell *)
[self superviewWithClassName:#"UICollectionViewCell"
fromView:button];
if (cell)
{
NSIndexPath *indexPath = [self.collectionView indexPathForCell:cell];
// do whatever action you need with the indexPath...
}
}
UPDATE: Swift version of superviewWithClassName. Made it a class method since it never references self.
static func superviewWithClassName(className:String, fromView view:UIView?) -> UIView? {
guard let classType = NSClassFromString(className) else {
return nil
}
var v:UIView? = view
while (v != nil) {
if v!.isKindOfClass(classType) {
return v
}
v = v!.superview
}
return nil
}
and some code to call it, either from prepareForSegue or a button handler:-
guard let cell = UIView.superviewWithClassName("UICollectionViewCell", fromView: sender as? UIView) as? UITableViewCell else {return}
Swift solution:
A UICollectionView extension like this one can be useful for this.
extension UICollectionView {
func indexPathForView(view: AnyObject) -> NSIndexPath? {
let originInCollectioView = self.convertPoint(CGPointZero, fromView: (view as! UIView))
return self.indexPathForItemAtPoint(originInCollectioView)
}
}
Usage becomes easy everywhere.
let indexPath = collectionView.indexPathForView(button)
You can do it like this, indexPathsForVisibleItems will return array of NSIndexPaths for items currently visible on view and first object returns the first one (if you have one cell per view).
NSIndexPath *indexPath = [[svc.collectionViewProdukte indexPathsForVisibleItems] firstObject]
If you want to animate a specific cell, you need to get a reference to that cell. Simply calling
[svc.collectionViewProdukte cellForItemAtIndexPath:indexPath];
does nothing. You need to keep the cell that the method returns, like this:
UICollectionViewCell *cell = [svc.collectionViewProdukte cellForItemAtIndexPath:indexPath];
After that, go ahead and animate:
[UIView animateWithDuration:0.2f animations:^{
cell.transform = CGAffineTransformMakeScale(0.5f, 0.5f);
}];
Swift 3 Solution : Based on Ishan Handa's Answer
extension UICollectionView {
func indexPathForView(view: AnyObject) -> IndexPath? {
let originInCollectioView = self.convert(CGPoint.zero, from: (view as! UIView))
return self.indexPathForItem(at: originInCollectioView) as IndexPath?
}
}
Usage:
func deleteCell(sender:UIButton){
var indexPath:IndexPath? = nil
indexPath = self.collectionView.indexPathForView(view: sender)
print("index path : \(indexPath)")
}
//Note: this is for a storyboard implementation
// here is code for finding the row and section of a textfield being edited in a uicollectionview
UIView *contentView = (UIView *)[textField superview];
UICollectionViewCell *cell = (UICollectionViewCell *)[contentView superview];
cell = (UICollectionViewCell *)[contentView superview];
// determine indexpath for a specific cell in a uicollectionview
NSIndexPath *editPath = [myCollectionView indexPathForCell:cell];
int rowIndex = editPath.row;
int secIndex = editPath.section;
Even though many answer i found here .this will be shortest and useful irrespective of the view hierarchy
- (void) actionAddToCart:(id)sender
{
id view = [sender superview];
while (view && [view isKindOfClass:[UICollectionViewCell class]] == NO)
{
view = [view superview];
}
NSIndexPath *thisIndexPath = [self.collectionView indexPathForCell:view];
NSLog(#"%d actionAddToCart pressed",thisIndexPath.row);
}
Xcode10. Swift 4.2 version.
extension UICollectionView {
func indexPathForView(view: AnyObject) -> IndexPath? {
guard let view = view as? UIView else { return nil }
let senderIndexPath = self.convert(CGPoint.zero, from: view)
return self.indexPathForItem(at: senderIndexPath)
}
}
Usage:
// yourView can be button for example
let indexPath = collectionView.indexPathForView(view: yourView)
You almost certainly have a UICollectionViewCell subclass. Just add a property and set the indexPath in cellForItemAtIndexPath.
internal func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: “cell_id”, for: indexPath)
let bttn_obj = UIButton(frame: CGRect(x: 5.5, y: 5.5, width: 22, height: 22))
bttn_obj.addTarget(self, action: #selector(bttn_action), for: UIControl.Event.touchUpInside)
cell.addSubview(bttn_obj)
return cell
}
#IBAction func bttn_action(_ sender: UIButton) -> Void {
let cell_view = sender.superview as! UICollectionViewCell
let index_p : IndexPath = self.collectionview.indexPath(for: cell_view)!
print(index_p)
}
I have a UITableView in which I have added a UIButton as accessory view for each cell. Note that I set the tag of the button as current row for future use.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// Configure the cell...
if(cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
cellButton = [UIButton buttonWithType:UIButtonTypeCustom];
cellButton.frame = CGRectMake(0, 0, 30, 30);
[cellButton setBackgroundImage:[UIImage imageNamed:#"cellButton.png"] forState:UIControlStateNormal];
[cellButton addTarget:self action:#selector(cellButtonAction:) forControlEvents:UIControlEventTouchUpInside];
cellButton.tag = indexPath.row; // <= Will use this in the next method
cell.accessoryView = cellButton;
}
//Load cell with row based data
return cell;
}
Now when one of these buttons is tapped, I need to make changes to the cell. So I implement cellButtonAction where I use the tag to get back the cell:
-(void)editCommentButtonAction:(id)sender
{
UIButton *button = sender;
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:button.tag inSection:0];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
[self makeChangesToCell:cell];
}
But this seems like a very round about way. Is there a cleaner way to do this?
So assuming that the button is in the contentView directly:
ask "sender" (ie the button) for its superview, which is the cell's contentView
ask that view for its superView, which is the cell
ask the tabview for the index of this cell:
- (NSIndexPath *)indexPathForCell:(UITableViewCell *)cell
EDIT: Actually, I use a general purpose method or function now that just walks up the superviews, looking for a view that is 'KindOf' a UITableViewCell or a UICollectionViewCell. Works like a champ!
The code in Swift:
func containingUITableViewCell(tableView: UITableView, var view: UIView) -> (UITableViewCell, NSIndexPath)? {
while let v = view.superview {
view = v
if view.isKindOfClass(UITableViewCell.self) {
if let cell = view as? UITableViewCell, let indexPath = tableView.indexPathForCell(cell) {
return (cell, indexPath)
} else {
return nil
}
}
}
return nil
}
func containingUICollectionViewCell(collectionView: UICollectionView, var view: UIView) -> (UICollectionViewCell, NSIndexPath)? {
while let v = view.superview {
view = v
if view.isKindOfClass(UICollectionViewCell.self) {
if let cell = view as? UICollectionViewCell, let indexPath = collectionView.indexPathForCell(cell) {
return (cell, indexPath)
} else {
return nil
}
}
}
return nil
}
You can do it in a easier way. You will get the table view cell using the sender parameter.
Check the following code.
-(void)editCommentButtonAction:(id)sender
{
UIButton *button = (UIButton *)sender;
UITableViewCell *cell = (UITableViewCell*)[button superview];
[self makeChangesToCell:cell];
}
Here,
You are casting the sender of type id to a UIButton
You are calling the getter superview of that button, it will give you the UITableViewCell
Doing your customization.
You can get the cell as follows.
-(void)editCommentButtonAction:(id)sender
{
NSIndexPath* indexPath = 0;
//Convert the bounds origin (0,0) to the tableView coordinate system
CGPoint localPoint = [self.tableView convertPoint:CGPointZero fromView:sender];
//Use the point to get the indexPath from the tableView itself.
indexPath = [self.tableView indexPathForRowAtPoint:localPoint];
//Here is the indexPath
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
[self makeChangesToCell:cell];
}
I had the same situation, the thing is that I had an imageView inside my tablecells, and I want to get the cell that holds the imageview that I tapped..
//MyCell is subclass of UITableViewCell
if ([[[[sender view] superview] superview] isKindOfClass:[MyCell class]]) {
MyCell *cell = (MyCell *)[[[sender view] superview] superview];
NSIndexPath *cellIndexPath = [myTable indexPathForCell:cell];
NSLog(#"cellIndexPath: %# - %#",cellIndexPath, [videoURLArray objectAtIndex:cellIndexPath.row]);
}
[sender view] - imageView
[[sender view] superview] -- where my imageView was placed (in this case, the superview of my imageView is the cell's contentView)
[[[sender view] superview] superview] --- where the contentView was placed -- the cell
The NSLog part should print the correct row and section of the tapped imageView.. Just Modify the code. ;)
Hello gigahari Use the following code.
- (void)cellStopDownloadButtonClicked:(id)sender
{
id viewType = [sender superview];
do {
viewType = [viewType superview];
}
while (![viewType isKindOfClass:[CustomCell class]] || !viewType);
CustomCell *cell = (CustomCell *)viewType;
// use the cell
}
This will work in all cases like ios 6 and ios 7. in ios 7 an extra view added in cell (content view).
if you use [[sender superview] superview] it will fail in some cases.
The way i usually do this:
cell stores the data for the given row or the index path
create a protocol with a method -didSelectSomething or -didSelectAtIndexPath:
the cell holds a reference to an instance of the protocol, which will be your datasource
wire the button action to the cell in your nib
have the cell call the delegate
DON'T FORGET to clean up the cell in prepareForReuse. Storing state in cells can lead to nasty bugs, so be sure to clean up on reuse.
The tag thing is a real hack, and it won't work if you have more than one section.
The sender superview will break if you reorder the views in your nib.
For this particular case (accessory view), isn't there a dedicated table delegate method?