I have a tableview with 7 custom cells. The cells are big enough that only 5 of them fit on screen at any one time. The different cells are user-configurable in terms of content, which is how I noticed that there is something strange going on. When the table first loads, and you scroll down to view all the cells for the first time, the cell contents are all correct. What is odd, however, is that once you scroll up and down a few times such that the top cells and the bottom cells disappear off screen a couple of times, the content of the bottom cell will pick up properties of the first cell.
Note that there is no code at all in willDisplayCell, so there is no reason for the content to change just due to scrolling. Also, all of the scrolling is without registering a touch in any of the cells. I have a strong feeling this is a dequeuing problem of some sort. What do I need to do to make sure the content of the cells remains stable regardless of the amount of scrolling?
Here is the TVC code:
import Foundation
import UIKit
class adventureTVC: UITableViewController {
var focusArray: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
// General Setup
self.title = selectedGenre
self.tableView.rowHeight = 98.0
self.tableView.separatorStyle = UITableViewCellSeparatorStyle.None
self.view.backgroundColor = UIColor(red: 0.8, green: 0.8, blue: 0.8, alpha: 1.0)
self.tableView.contentInset.bottom = 49
// Registering the custom cell
let nib = UINib(nibName: "StarsAcrossCell", bundle: nil)
tableView.registerNib(nib, forCellReuseIdentifier: "starsCell")
// Data used by this TVC. This should be the Genre for which ever cell was touched to get here
focusArray = bookage.focusArray()
}
override func viewWillAppear(animated: Bool) {
tableView.reloadData()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func numberOfSectionsInTableView(tableView: UITableView?) -> Int {
return 1
}
override func tableView(tableView: UITableView?, numberOfRowsInSection section: Int) -> Int {
return focusArray.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: StarsAcrossCell = self.tableView.dequeueReusableCellWithIdentifier("starsCell") as! StarsAcrossCell
cell.selectionStyle = .None
cell.groupTypeLabel.text = focusArray[indexPath.row]
cell.cellBorder.backgroundColor = getCellColor(indexPath.row)
let statusArray = bookage.getReadStatusArrayForFocus(selectedCategory, genre: selectedGenre, focus: focusArray[indexPath.row])
setStarImages(statusArray, cell: cell)
return cell
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
}
}
It seems cell reuse issue. You need to implement -prepareForReuse method in your custom cell class and set all cell properties to default value.
- (void)prepareForReuse
If a UITableViewCell object is reusable—that is, it has a reuse
identifier—this method is invoked just before the object is returned
from the UITableView method dequeueReusableCellWithIdentifier:. For
performance reasons, you should only reset attributes of the cell that
are not related to content, for example, alpha, editing, and selection
state. The table view's delegate in tableView:cellForRowAtIndexPath:
should always reset all content when reusing a cell. If the cell
object does not have an associated reuse identifier, this method is
not called. If you override this method, you must be sure to invoke
the superclass implementation.
Refer here for more, https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableViewCell_Class/#//apple_ref/occ/instm/UITableViewCell/prepareForReuse
First of all create customtableviewcell (if you haven't created it)
RatingViewCell.h
#interface RatingViewCell : UITableViewCell<RatingViewDelegate>
#property (strong, nonatomic) IBOutlet UILabel *lblTitle;
#property (strong, nonatomic) RatingView *ratingView;
#end
RatingViewCell.m
#implementation RatingViewCell
#synthesize lblTitle,ratingView2;
- (void)awakeFromNib
{
// Initialization code
ratingView2 = [[RatingView alloc] initWithFrame:CGRectMake(-10,22,130,15)
selectedImageName:#"RatingStartBig"
unSelectedImage:#"RatingStartBigBlank"
minValue:0
maxValue:5
intervalValue:0.5
stepByStep:NO];
ratingView2.userInteractionEnabled = NO;
ratingView2.delegate = self;
[self.contentView addSubview:ratingView2];
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
Then use this customCell in tableview's CellforRow method as
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"CellID";
RatingViewCell *cell = (RatingViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(cell == nil){
cell = [[RatingViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier];
}
cell.lblTitle.text = #"Title"; // use array to pass values dynamically
cell.ratingView2.value = 2.0; // use array to pass values dynamically
return cell;
}
Related
I used CollectionView inside TableViewCell. All works fine and shown all as expected. But if I scrolled the TableView very fast, items (i used images in collectionView) from one collection replaced with items (images) from another collection and override it on View (on Debug mode in code al works fine, its just displaying of them).
UITableView GetCell():
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
var item = _view.Items[indexPath.Row];
var cell = (MyTableCell)tableView.DequeueReusableCell(“cell”);
cell.TextLabelView.Text = item.Title;
cell.YesButtonView.Hidden = item.IsCategory;
cell.NoButtonView.Hidden = item.IsCategory;
if (item.IsImagePoint)
{
cell.ImagesCollectionView.DataSource = new ItemsDataSource(item.Images, cell.ImagesCollectionView);
cell.ImagesCollectionView.Delegate = new ItemsDelegate(item, _view);
}
return cell;
}
UICollectionView GetCell():
public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
{
var cell = (ImageViewCell)_collectionView.DequeueReusableCell(new NSString(“ImageViewCell”), indexPath);
var image = _images[indexPath.Row];
var imagePath = image.ThumbnailPath;
if (!string.IsNullOrEmpty(imagePath))
{
cell.ImagePath = imagePath;
}
return cell;
}
Swift 5
Add this in your custom UITableViewCell class.
override func prepareForReuse() {
collectionView.dataSource = nil
collectionView.delegate = nil
collectionView.reloadData()
collectionView.dataSource = self
collectionView.delegate = self
}
It's probably because of the reuse system of cells in UITableView. Do you set up properly your data when you configure the cell? Do you call CollectionView's reloadData()?
EDIT: You should call it in the tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell where you configure your cell. This way, each time a cell is reused, you update its content.
EDIT 2: Just like I said try to add the collection view reloadData() when you set up your tableview cell. You also have to clean your datasource and delegate, because it's a reused cell so it may already have been used with another value.
if (item.IsImagePoint)
{
cell.ImagesCollectionView.DataSource = new ItemsDataSource(item.Images, cell.ImagesCollectionView);
cell.ImagesCollectionView.Delegate = new ItemsDelegate(item, _view);
}
else
{
cell.ImagesCollectionView.DataSource = null;
cell.ImagesCollectionView.Delegate = null;
}
cell.ImagesCollectionView.ReloadData()
return cell;
I have multiple cells in my table view; e.g. Cat Cell, Dog Cell, Chicken Cell. In the Cat Cell I have two views: a graphs view and an images view.
If the graphs view is hidden and the images view is visible, then I want to set my cell height to 200, and 350 in the opposite case.
How would I achieve that? I am registering my nib file in the heightForRowAt delegate method of the table view and I have tried different things to no avail. These are the two views in my cell:
#property (weak, nonatomic) IBOutlet UIView *imagesView;
#property (weak, nonatomic) IBOutlet UIView *graphView;
...and this is my heightForRowAt method:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
TrainingImageCell *cell = (TrainingImageCell*)[self.tableView dequeueReusableCellWithIdentifier:#"TrainingImageCell"];
if (cell.isGraphViewOnTop == YES) {
return 350;
}
else {
return 200;
}
return 200;
}
Instead of initiating a new cell, you need to access the one what is already in the tableView.
Instead of
TrainingImageCell *cell = (TrainingImageCell*)[self.tableView dequeueReusableCellWithIdentifier:#"TrainingImageCell"];
Do
TrainingImageCell *currentCell = (TrainingImageCell*)[tableView cellForRowAtIndexPath:indexPath];
And than take a look on the property.
EDIT:
Looking at the lengthy discussion we have, here is a very reliable example for an approach you could use in Swift.
import UIKit
class RightAlignedCell: UITableViewCell {
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var subLabel: UILabel!
// Your boolean property you would like to map
var randomBool: Bool = false
}
class RightAlignedTableViewController: UIViewController {
// This is just an example for the dataSource, you should have this somewhere already
lazy var dataSource: [Bool] = {
var dataSource: [Bool] = []
for index in 0..<10 {
dataSource.append(index % 2 == 0)
}
return dataSource
}()
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.reloadData()
}
}
extension RightAlignedTableViewController: UITableViewDelegate {}
extension RightAlignedTableViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// Take a look in the !!!dataSource!!!, what is value for isHidden
let isHidden = self.dataSource[indexPath.row]
// return the right height
return isHidden ? 60 : 100
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: RightAlignedCell = tableView.dequeueReusableCell(withIdentifier: "RightAlignedCell", for: indexPath) as! RightAlignedCell
cell.titleLabel.text = "Something"
cell.subLabel.text = "Nothing"
cell.randomBool = self.dataSource[indexPath.row]
return cell
}
}
Why are registering nib file in heightForRowAt. You are registering new cell hence cannot access the existing cell. If you want it working in your style instead of registering new cell in height for row at IndexPath you can access existing cell using indexPath as
TrainingImageCell *currentCell = (TrainingImageCell*)[tableView cellForRowAtIndexPath:indexPath];
and access check property.
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
TrainingImageCell *currentCell = (TrainingImageCell*)[tableView cellForRowAtIndexPath:indexPath];
if (cell.isGraphViewOnTop == YES) {
return 350;
}
return 200;
}
If you dequeueReusableCellWithIdentifier you just get a new cell, it will not have any data to check if isGraphViewOnTop
My advice will be to return UITableViewAutomaticDimension and set cell views with constants, so when the element is hidden, a cell will have a smaller size.
I'm just making a simple project for fun but I am having a very strange bug in my program.
This function returns 29 which is correct however it is called three times when my table view is instantiated.
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
print("ingredient count: \(Sushi.ingredients.count)")
return Sushi.ingredients.count
}
And I think that (_: numberOfRowsInSection:) being called 3 times is what leads this code to execute almost three times the number of elements in the ingredients array.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
guard let cell = tableView.dequeueReusableCell(withIdentifier: "IngredientCell", for: indexPath) as? IngredientCell
else
{
return UITableViewCell(style: .default, reuseIdentifier: nil)
}
cell.ingredientLabel.text = Sushi.ingredients[indexPath.row]
print("ingredient label text: \(cell.ingredientLabel.text)\nindex path: \(indexPath)\ncell index: \(cell.index)\nbutton tag: \(cell.selectButton.tag)\nindex path row: \(indexPath.row)\ncell object: \(cell)\n")
return cell
}
The table view is populated appropriately as far as it lists all the ingredients in the array correctly. However, each row of the tableView is a prototype cell that has a UILabel, a UIButton, and a UIImage. When the button is pressed the text and color change for that button but also for every thirteenth button in the tableview cells.
This class is for my individual cells
class IngredientCell: UITableViewCell
{
public var userSelected = false
public var index: Int = 0
#IBOutlet weak var picture: UIImageView!
#IBOutlet weak var selectButton: UIButton!
#IBOutlet weak var ingredientLabel: UILabel!
//This method is called when a button is pressed
#IBAction func selectIngredient(_ sender: UIButton)
{
if userSelected == false
{
userSelected = true
selectButton.setTitleColor(.red, for: .normal)
selectButton.setTitle("Remove", for: .normal)
}
else
{
userSelected = false
selectButton.setTitle("Select", for: .normal)
selectButton.setTitleColor(.blue, for: .normal)
}
}
}
I decided to debug using print statements to see if I could figure out what exactly is going on and I found that numberOfRowsInSection is called 3 times and cellForRowAtIndexPath is called a varying amount of times. I keeps iterating over the table view giving the cell objects the same memory addresses which I suppose is what causes multiple buttons to change when only one is pressed. My console output proved that different buttons in the storyboard had the same addresses in memory.
Sorry, I can only write in Objective-C. But anyway, I am just going to show you the idea.
One of the ways to update the UI contents of just a single item of a table is to reload that specific cell.
For example: Your cell contains just a button. When you press a specific button you want to change the title of that button only. Then all you have to do is to create an dictionary which references all the titles of all buttons in your table, and then set the title to that button upon reload of that specific cell.
You can do it this way:
#interface ViewController ()<UITableViewDelegate, UITableViewDataSource>
#property NSMutableDictionary *buttonTitlesDict;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
buttonTitles = [[NSMutableDictionary alloc] init];
}
(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *identifier = #"SimpleTableItem";
UITableViewCell *tableCell = [tableView dequeueReusableCellWithIdentifier:identifier];
if(tableCell == nil)
{
tableCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
}
tableCell.button.title = [_buttonTitles objectForKey:#(indexPath.row)];
tableCell.button.tag = indexPath.row;
}
//reloads only a specific cell
- (IBAction)updateButton:(id)sender
{
NSInteger row = [sender tag];
[_buttonTitlesDict setObject:#"changedTitle" forKey:#(row)];
[_tableView reloadRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:row inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
}
As you can see, I just created a dictionary that holds the button title for each button with the index of the cell as the reference key. Every time I want to update the title of the button, I will get that title from the button every time the cell is loaded upon cellForRowAtIndexPath. take note that when you scroll the table, cellForRowAtIndexPath will be called when new cells appear in front of you. So as you scroll, the correct titles will be updated to the buttons.
Hope this helps.
I have a view controller with a table view and a separate nib for the table cell template. The cell template has some buttons. I want to access the button click along with the index of the cell clicked inside the view controller where I have defined the Table view.
So I have ViewController.h and ViewController.m where I have the UITableView and TableTemplate.h, TableTemplate.m and TableTemplate.xib where I have the nib defined. I want the button click event with cell index in ViewController.m.
Any help on how can I do that?
1) In your cellForRowAtIndexPath: method, assign button tag as index:
cell.yourbutton.tag = indexPath.row;
2) Add target and action for your button as below:
[cell.yourbutton addTarget:self action:#selector(yourButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
3) Code actions based on index as below in ViewControler:
-(void)yourButtonClicked:(UIButton*)sender
{
if (sender.tag == 0)
{
// Your code here
}
}
Updates for multiple Section:
You can check this link to detect button click in table view for multiple row and section.
Delegates are the way to go.
As seen with other answers using views might get outdated. Who knows tomorrow there might be another wrapper and may need to use cell superview]superview]superview]superview]. And if you use tags you would end up with n number of if else conditions to identify the cell. To avoid all of that set up delegates. (By doing so you will be creating a re usable cell class. You can use the same cell class as a base class and all you have to do is implement the delegate methods.)
First we need a interface (protocol) which will be used by cell to communicate(delegate) button clicks. (You can create a separate .h file for protocol and include in both table view controller and custom cell classes OR just add it in custom cell class which will anyway get included in table view controller)
#protocol CellDelegate <NSObject>
- (void)didClickOnCellAtIndex:(NSInteger)cellIndex withData:(id)data;
#end
Include this protocol in custom cell and table view controller. And make sure table view controller confirms to this protocol.
In custom cell create two properties :
#property (weak, nonatomic) id<CellDelegate>delegate;
#property (assign, nonatomic) NSInteger cellIndex;
In UIButton IBAction delegate click : (Same can be done for any action in custom cell class which needs to be delegated back to view controller)
- (IBAction)buttonClicked:(UIButton *)sender {
if (self.delegate && [self.delegate respondsToSelector:#selector(didClickOnCellAtIndex:withData:)]) {
[self.delegate didClickOnCellAtIndex:_cellIndex withData:#"any other cell data/property"];
}
}
In table view controller cellForRowAtIndexPath after dequeing the cell, set the above properties.
cell.delegate = self;
cell.cellIndex = indexPath.row; // Set indexpath if its a grouped table.
And implement the delegate in table view controller:
- (void)didClickOnCellAtIndex:(NSInteger)cellIndex withData:(id)data
{
// Do additional actions as required.
NSLog(#"Cell at Index: %d clicked.\n Data received : %#", cellIndex, data);
}
This would be the ideal approach to get custom cell button actions in table view controller.
Instead of playing with tags, I took different approach. Made delegate for my subclass of UITableViewCell(OptionButtonsCell) and added an indexPath var. From my button in storyboard I connected #IBAction to the OptionButtonsCell and there I send delegate method with the right indexPath to anyone interested. In cell for index path I set current indexPath and it works :)
Let the code speak for itself:
Swift 3 Xcode 8
OptionButtonsTableViewCell.swift
import UIKit
protocol OptionButtonsDelegate{
func closeFriendsTapped(at index:IndexPath)
}
class OptionButtonsTableViewCell: UITableViewCell {
var delegate:OptionButtonsDelegate!
#IBOutlet weak var closeFriendsBtn: UIButton!
var indexPath:IndexPath!
#IBAction func closeFriendsAction(_ sender: UIButton) {
self.delegate?.closeFriendsTapped(at: indexPath)
}
}
MyTableViewController.swift
class MyTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, OptionButtonsDelegate {...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "optionCell") as! OptionButtonsTableViewCell
cell.delegate = self
cell.indexPath = indexPath
return cell
}
func closeFriendsTapped(at index: IndexPath) {
print("button tapped at index:\(index)")
}
This should help :-
UITableViewCell* cell = (UITableViewCell*)[sender superview];
NSIndexPath* indexPath = [myTableView indexPathForCell:cell];
Here sender is the UIButton instance that is sending the event.
myTableView is the UITableView instance you're dealing with.
Just get the cell reference right and all the work is done.
You may need to remove the buttons from cell's contentView &
add them directly to UITableViewCell instance as it's subview.
Or
You can formulate a tag naming scheme for different UIButtons in cell.contentView.
Using this tag, later you can know the row & section information as needed.
Following code might Help you.
I have taken UITableView with custom prototype cell class named UITableViewCell inside UIViewController.
So i have ViewController.h, ViewController.m and TableViewCell.h,TableViewCell.m
Here is the code for that:
ViewController.h
#interface ViewController : UIViewController<UITableViewDataSource,UITableViewDelegate>
#property (strong, nonatomic) IBOutlet UITableView *tblView;
#end
ViewController.m
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return (YourNumberOfRows);
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *cellIdentifier = #"cell";
__weak TableViewCell *cell = (TableViewCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
if (indexPath.row==0) {
[cell setDidTapButtonBlock:^(id sender)
{
// Your code here
}];
}
return cell;
}
Custom cell class :
TableViewCell.h
#interface TableViewCell : UITableViewCell
#property (copy, nonatomic) void (^didTapButtonBlock)(id sender);
#property (strong, nonatomic) IBOutlet UILabel *lblTitle;
#property (strong, nonatomic) IBOutlet UIButton *btnAction;
- (void)setDidTapButtonBlock:(void (^)(id sender))didTapButtonBlock;
#end
and
UITableViewCell.m
#implementation TableViewCell
- (void)awakeFromNib {
// Initialization code
[self.btnAction addTarget:self action:#selector(didTapButton:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
- (void)didTapButton:(id)sender {
if (self.didTapButtonBlock)
{
self.didTapButtonBlock(sender);
}
}
Note: Here I have taken all UIControls using Storyboard.
Hope that can help you...!!!
Use Swift closures :
class TheCell: UITableViewCell {
var tapCallback: (() -> Void)?
#IBAction func didTap(_ sender: Any) {
tapCallback?()
}
}
extension TheController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: TheCell.identifier, for: indexPath) as! TheCell {
cell.tapCallback = {
//do stuff
}
return cell
}
}
The reason i like below technique because it also help me to identify the section of table.
Add Button in cell cellForRowAtIndexPath:
UIButton *selectTaskBtn = [UIButton buttonWithType:UIButtonTypeCustom];
[selectTaskBtn setFrame:CGRectMake(15, 5, 30, 30.0)];
[selectTaskBtn setTag:indexPath.section]; //Not required but may find useful if you need only section or row (indexpath.row) as suggested by MR.Tarun
[selectTaskBtn addTarget:self action:#selector(addTask:) forControlEvents:UIControlEventTouchDown];
[cell addsubview: selectTaskBtn];
Event addTask:
-(void)addTask:(UIButton*)btn
{
CGPoint buttonPosition = [btn convertPoint:CGPointZero toView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:buttonPosition];
if (indexPath != nil)
{
int currentIndex = indexPath.row;
int tableSection = indexPath.section;
}
}
Hopes this help.
Tarun's code doesnt work on iOS7, since the UITableViewCell structure changed and now he would get "UITableViewCellScrollView" instead.
This post Getting UITableViewCell with superview in iOS 7 has a good solution creating a loop to find the correct parent view, regardless of any future changes in the structure. It boils down to creating a loop:
UIView *superView = [sender superview];
UIView *foundSuperView = nil;
while (nil != superView && nil == foundSuperView) {
if ([superView isKindOfClass:[UITableViewCell class]]) {
foundSuperView = superView;
} else {
superView = superView.superview;
}
}
The link has code for a more reusable solution, but this should work.
Its Work For me.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UIButton *Btn_Play = (UIButton *)[cell viewWithTag:101];
[Btn_Play addTarget:self action:#selector(ButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
}
-(void)ButtonClicked:(UIButton*)sender {
CGPoint buttonPosition = [sender convertPoint:CGPointZero toView:self.Tbl_Name];
NSIndexPath *indexPath = [self.Tbl_Name indexPathForRowAtPoint:buttonPosition];
}
Swift 2.2
You need to add target for that button.
myButton.addTarget(self, action: #selector(ClassName.FunctionName(_:), forControlEvents: .TouchUpInside)
FunctionName: connected // for example
And of course you need to set tag of that button since you are using it.
myButton.tag = indexPath.row
You can achieve this by subclassing UITableViewCell. Use it in interface builder, drop a button on that cell, connect it via outlet and there you go.
To get the tag in the connected function:
func connected(sender: UIButton) {
let buttonTag = sender.tag
// Do any additional setup
}
Swift 3 with a Closure
A nice solution is using a closure in a custom UITableViewCell to callback to the viewController for an action.
In cell:
final class YourCustomCell: UITableViewCell {
var callbackClosure: (() -> Void)?
// Configure the cell here
func configure(object: Object, callbackClosure: (() -> Void)?) {
self.callbackClosure = callbackClosure
}
// MARK: - IBAction
extension YourCustomCell {
#IBAction fileprivate func actionPressed(_ sender: Any) {
guard let closure = callbackClosure else { return }
closure()
}
}
In View Controller: Tableview Delegate
extension YourViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
guard let cell: YourCustomCell = cell as? YourCustomCell else { return }
cell.configure(object: object, callbackClosure: { [weak self] in
self?.buttonAction()
})
}
}
fileprivate extension YourViewController {
func buttonAction() {
// do your actions here
}
}
I find it simplest to subclass the button inside your cell (Swift 3):
class MyCellInfoButton: UIButton {
var indexPath: IndexPath?
}
In your cell class:
class MyCell: UICollectionViewCell {
#IBOutlet weak var infoButton: MyCellInfoButton!
...
}
In the table view's or collection view's data source, when dequeueing the cell, give the button its index path:
cell.infoButton.indexPath = indexPath
So you can just put these code into your table view controller:
#IBAction func handleTapOnCellInfoButton(_ sender: MyCellInfoButton) {
print(sender.indexPath!) // Do whatever you want with the index path!
}
And don't forget to set the button's class in your Interface Builder and link it to the handleTapOnCellInfoButton function!
edited:
Using dependency injection. To set up calling a closure:
class MyCell: UICollectionViewCell {
var someFunction: (() -> Void)?
...
#IBAction func didTapInfoButton() {
someFunction?()
}
}
and inject the closure in the willDisplay method of the collection view's delegate:
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
(cell as? MyCell)?.someFunction = {
print(indexPath) // Do something with the indexPath.
}
}
If you want to pass parameter value from cell to UIViewController using closure then
//Your Cell Class
class TheCell: UITableViewCell {
var callBackBlockWithParam: ((String) -> ()) = {_ in }
//Your Action on button
#IBAction func didTap(_ sender: Any) {
callBackBlockWithParam("Your Required Parameter like you can send button as sender or anything just change parameter type. Here I am passing string")
}
}
//Your Controller
extension TheController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: TheCell.identifier, for: indexPath) as! TheCell {
cell.callBackBlockWithParam = { (passedParamter) in
//you will get string value from cell class
print(passedParamter)
}
return cell
}
}
// Add action in cell for row at index path -tableView
cell.buttonName.addTarget(self, action: #selector(ViewController.btnAction(_:)), for: .touchUpInside)
// Button Action
#objc func btnAction(_ sender: AnyObject) {
var position: CGPoint = sender.convert(.zero, to: self.tableView)
let indexPath = self.tableView.indexPathForRow(at: position)
let cell: UITableViewCell = tableView.cellForRow(at: indexPath!)! as
UITableViewCell
}
for swift 4:
inside the cellForItemAt ,
cell.chekbx.addTarget(self, action: #selector(methodname), for: .touchUpInside)
then outside of cellForItemAt
#objc func methodname()
{
//your function code
}
#Mani answer is good, however tags of views inside cell's contentView often are used for other purposes. You can use cell's tag instead (or cell's contentView tag):
1) In your cellForRowAtIndexPath: method, assign cell's tag as index:
cell.tag = indexPath.row; // or cell.contentView.tag...
2) Add target and action for your button as below:
[cell.yourbutton addTarget:self action:#selector(yourButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
3) Create method that returns row of the sender (thanks #Stenio Ferreira):
- (NSInteger)rowOfSender:(id)sender
{
UIView *superView = sender.superview;
while (superView) {
if ([superView isKindOfClass:[UITableViewCell class]])
break;
else
superView = superView.superview;
}
return superView.tag;
}
4) Code actions based on index:
-(void)yourButtonClicked:(UIButton*)sender
{
NSInteger index = [self rowOfSender:sender];
// Your code here
}
CustomTableCell.h is a UITableViewCell:
#property (weak, nonatomic) IBOutlet UIButton *action1Button;
#property (weak, nonatomic) IBOutlet UIButton *action2Button;
MyVC.m after imports:
#interface MYTapGestureRecognizer : UITapGestureRecognizer
#property (nonatomic) NSInteger dataint;
#end
Inside "cellForRowAtIndexPath" in MyVC.m:
//CustomTableCell
CustomTableCell *cell = (CustomTableCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
//Set title buttons
[cell.action1Button setTitle:[NSString stringWithString:NSLocalizedString(#"action1", nil)] forState:UIControlStateNormal];
[cell.action2Button setTitle:[NSString stringWithString:NSLocalizedString(#"action2", nil)] forState:UIControlStateNormal];
//Set visibility buttons
[cell.action1Button setHidden:FALSE];
[cell.action2Button setHidden:FALSE];
//Do 1 action
[cell.action1Button addTarget:self action:#selector(do1Action :) forControlEvents:UIControlEventTouchUpInside];
//Do 2 action
MYTapGestureRecognizer *action2Tap = [[MYTapGestureRecognizer alloc] initWithTarget:self action:#selector(do2Action :)];
cancelTap.numberOfTapsRequired = 1;
cancelTap.dataint = indexPath.row;
[cell.action2Button setUserInteractionEnabled:YES];
[cell.action2Button addGestureRecognizer:action2Tap];
MyVC.m:
-(void)do1Action :(id)sender{
//do some action that is not necessary fr data
}
-(void)do2Action :(UITapGestureRecognizer *)tapRecognizer{
MYTapGestureRecognizer *tap = (MYTapGestureRecognizer *)tapRecognizer;
numberTag = tap.dataint;
FriendRequest *fr = [_list objectAtIndex:numberTag];
//connect with a WS o do some action with fr data
//actualize list in tableView
[self.myTableView reloadData];
}
cell.show.tag=indexPath.row;
[cell.show addTarget:self action:#selector(showdata:) forControlEvents:UIControlEventTouchUpInside];
-(IBAction)showdata:(id)sender
{
UIButton *button = (UIButton *)sender;
UIStoryboard *storyBoard;
storyBoard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
SecondViewController *detailView = [storyBoard instantiateViewControllerWithIdentifier:#"SecondViewController"];
detailView.string=[NSString stringWithFormat:#"%#",[_array objectAtIndex:button.tag]];
[self presentViewController:detailView animated:YES completion:nil];
}
Without using a storyboard we could simply drag a UIView onto the canvas, lay it out and then set it in the tableView:viewForHeaderInSection or tableView:viewForFooterInSection delegate methods.
How do we accomplish this with a StoryBoard where we cannot drag a UIView onto the canvas
Just use a prototype cell as your section header and / or footer.
add an extra cell and put your desired elements in it.
set the identifier to something specific (in my case SectionHeader)
implement the tableView:viewForHeaderInSection: method or the tableView:viewForFooterInSection: method
use [tableView dequeueReusableCellWithIdentifier:] to get the header
implement the tableView:heightForHeaderInSection: method.
-(UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
static NSString *CellIdentifier = #"SectionHeader";
UITableViewCell *headerView = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (headerView == nil){
[NSException raise:#"headerView == nil.." format:#"No cells with matching CellIdentifier loaded from your storyboard"];
}
return headerView;
}
Edit: How to change the header title (commented question):
Add a label to the header cell
set the tag of the label to a specific number (e.g. 123)
In your tableView:viewForHeaderInSection: method get the label by calling:
UILabel *label = (UILabel *)[headerView viewWithTag:123];
Now you can use the label to set a new title:
[label setText:#"New Title"];
I know this question was for iOS 5, but for the benefit of future readers, note that effective iOS 6 we can now use dequeueReusableHeaderFooterViewWithIdentifier instead of dequeueReusableCellWithIdentifier.
So in viewDidLoad, call either registerNib:forHeaderFooterViewReuseIdentifier: or registerClass:forHeaderFooterViewReuseIdentifier:. Then in viewForHeaderInSection, call tableView:dequeueReusableHeaderFooterViewWithIdentifier:. You do not use a cell prototype with this API (it's either a NIB-based view or a programmatically created view), but this is the new API for dequeued headers and footers.
In iOS 6.0 and above, things have changed with the new dequeueReusableHeaderFooterViewWithIdentifier API.
I have written a guide (tested on iOS 9), which can be summarised as such:
Subclass UITableViewHeaderFooterView
Create Nib with the subclass view, and add 1 container view which contains all other views in the header/footer
Register the Nib in viewDidLoad
Implement viewForHeaderInSection and use dequeueReusableHeaderFooterViewWithIdentifier to get back the header/footer
I got it working in iOS7 using a prototype cell in the storyboard. I have a button in my custom section header view that triggers a segue that is set up in the storyboard.
Start with Tieme's solution
As pedro.m points out, the problem with this is that tapping the section header causes the first cell in the section to be selected.
As Paul Von points out, this is fixed by returning the cell's contentView instead of the whole cell.
However, as Hons points out, a long press on said section header will crash the app.
The solution is to remove any gestureRecognizers from contentView.
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
static NSString *CellIdentifier = #"SectionHeader";
UITableViewCell *sectionHeaderView = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
while (sectionHeaderView.contentView.gestureRecognizers.count) {
[sectionHeaderView.contentView removeGestureRecognizer:[sectionHeaderView.contentView.gestureRecognizers objectAtIndex:0]];
}
return sectionHeaderView.contentView; }
If you aren't using gestures in your section header views, this little hack seems to get it done.
If you use storyboards you can use a prototype cell in the tableview to layout your header view. Set an unique id and viewForHeaderInSection you can dequeue the cell with that ID and cast it to a UIView.
If you need a Swift Implementation of this follow the directions on the accepted answer and then in you UITableViewController implement the following methods:
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return tableView.dequeueReusableCell(withIdentifier: "CustomHeader")
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 75
}
The solution I came up with is basically the same solution used before the introduction of storyboards.
Create a new, empty interface class file. Drag a UIView on to the canvas, layout as desired.
Load the nib manually, assign to the appropriate header/footer section in viewForHeaderInSection or viewForFooterInSection delegate methods.
I had hope that Apple simplified this scenario with storyboards and kept looking for a better or simpler solution. For example custom table headers and footers are straight forward to add.
When you return cell's contentView you will have a 2 problems:
crash related to gestures
you don't reusing contentView (every time on viewForHeaderInSection call, you creating new cell)
Solution:
Wrapper class for table header\footer.
It is just container, inherited from UITableViewHeaderFooterView, which holds cell inside
https://github.com/Magnat12/MGTableViewHeaderWrapperView.git
Register class in your UITableView (for example, in viewDidLoad)
- (void)viewDidLoad {
[super viewDidLoad];
[self.tableView registerClass:[MGTableViewHeaderWrapperView class] forHeaderFooterViewReuseIdentifier:#"ProfileEditSectionHeader"];
}
In your UITableViewDelegate:
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
MGTableViewHeaderWrapperView *view = [tableView dequeueReusableHeaderFooterViewWithIdentifier:#"ProfileEditSectionHeader"];
// init your custom cell
ProfileEditSectionTitleTableCell *cell = (ProfileEditSectionTitleTableCell * ) view.cell;
if (!cell) {
cell = [tableView dequeueReusableCellWithIdentifier:#"ProfileEditSectionTitleTableCell"];
view.cell = cell;
}
// Do something with your cell
return view;
}
I've been in trouble within a scenario where Header was never reused even doing all the proper steps.
So as a tip note to everyone who want to achieve the situation of show empty sections (0 rows) be warn that:
dequeueReusableHeaderFooterViewWithIdentifier will not reuse the header until you return at least one row
Hope it helps
I used to do the following to create header/footer views lazily:
Add a freeform view controller for the section header/footer to the storyboard
Handle all stuff for the header in the view controller
In the table view controller provide a mutable array of view controllers for the section headers/footers repopulated with [NSNull null]
In viewForHeaderInSection/viewForFooterInSection if view controller does not yet exist, create it with storyboards instantiateViewControllerWithIdentifier, remember it in the array and return the view controllers view
Similar to laszlo answer but you can reuse the same prototype cell for both the table cells and the section header cell. Add the first two functions below to your UIViewController subClass
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cell = tableView.dequeueReusableCell(withIdentifier: "DataCell") as! DataCell
cell.data1Label.text = "DATA KEY"
cell.data2Label.text = "DATA VALUE"
return cell
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 75
}
// Example of regular data cell dataDelegate to round out the example
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "DataCell", for: indexPath) as! PlayerCell
cell.data1Label.text = "\(dataList[indexPath.row].key)"
cell.data2Label.text = "\(dataList[indexPath.row].value)"
return cell
}
To follow up on Damon's suggestion, here is how I made the header selectable just like a normal row with a disclosure indicator.
I added a Button subclassed from UIButton (subclass name "ButtonWithArgument") to the header's prototype cell and deleted the title text (the bold "Title" text is another UILabel in the prototype cell)
then set the Button to the entire header view, and added a disclosure indicator with Avario's trick
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
static NSString *CellIdentifier = #"PersonGroupHeader";
UITableViewCell *headerView = (UITableViewCell *) [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(headerView == nil)
{
[NSException raise:#"headerView == nil, PersonGroupTableViewController" format:[NSString stringWithFormat:#"Storyboard does not have prototype cell with identifier %#",CellIdentifier]];
}
// https://stackoverflow.com/a/24044628/3075839
while(headerView.contentView.gestureRecognizers.count)
{
[headerView.contentView removeGestureRecognizer:[headerView.contentView.gestureRecognizers objectAtIndex:0]];
}
ButtonWithArgument *button = (ButtonWithArgument *)[headerView viewWithTag:4];
button.frame = headerView.bounds; // set tap area to entire header view
button.argument = [[NSNumber alloc] initWithInteger:section]; // from ButtonWithArguments subclass
[button addTarget:self action:#selector(headerViewTap:) forControlEvents:UIControlEventTouchUpInside];
// https://stackoverflow.com/a/20821178/3075839
UITableViewCell *disclosure = [[UITableViewCell alloc] init];
disclosure.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
disclosure.userInteractionEnabled = NO;
disclosure.frame = CGRectMake(button.bounds.origin.x + button.bounds.size.width - 20 - 5, // disclosure 20 px wide, right margin 5 px
(button.bounds.size.height - 20) / 2,
20,
20);
[button addSubview:disclosure];
// configure header title text
return headerView.contentView;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return 35.0f;
}
-(void) headerViewTap:(UIGestureRecognizer *)gestureRecognizer;
{
NSLog(#"header tap");
NSInteger section = ((NSNumber *)sender.argument).integerValue;
// do something here
}
ButtonWithArgument.h
#import <UIKit/UIKit.h>
#interface ButtonWithArgument : UIButton
#property (nonatomic, strong) NSObject *argument;
#end
ButtonWithArgument.m
#import "ButtonWithArgument.h"
#implementation ButtonWithArgument
#end
You should use Tieme's solution as a base but forget about the viewWithTag: and other fishy approaches, instead try to reload your header (by reloading that section).
So after you sat up your custom cell-header view with all the fancy AutoLayout stuff, just dequeue it and return the contentView after your set up, like:
-(UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
static NSString *CellIdentifier = #"SectionHeader";
SettingsTableViewCell *sectionHeaderCell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
sectionHeaderCell.myPrettyLabel.text = #"Greetings";
sectionHeaderCell.contentView.backgroundColor = [UIColor whiteColor]; // don't leave this transparent
return sectionHeaderCell.contentView;
}
What about a solution where the header is based on a view array :
class myViewController: UIViewController {
var header: [UILabel] = myStringArray.map { (thisTitle: String) -> UILabel in
let headerView = UILabel()
headerView.text = thisTitle
return(headerView)
}
Next in the delegate :
extension myViewController: UITableViewDelegate {
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return(header[section])
}
}
Add cell in StoryBoard, and set reuseidentified
Code
class TP_TaskViewTableViewSectionHeader: UITableViewCell{
}
and
Use:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = tableView.dequeueReusableCell(withIdentifier: "header", for: IndexPath.init(row: 0, section: section))
return header
}
Here is #Vitaliy Gozhenko's answer, in Swift.
To summarize you will create a UITableViewHeaderFooterView that contains a UITableViewCell. This UITableViewCell will be "dequeuable" and you can design it in your storyboard.
Create a UITableViewHeaderFooterView class
class CustomHeaderFooterView: UITableViewHeaderFooterView {
var cell : UITableViewCell? {
willSet {
cell?.removeFromSuperview()
}
didSet {
if let cell = cell {
cell.frame = self.bounds
cell.autoresizingMask = [UIViewAutoresizing.FlexibleHeight, UIViewAutoresizing.FlexibleWidth]
self.contentView.backgroundColor = UIColor .clearColor()
self.contentView .addSubview(cell)
}
}
}
Plug your tableview with this class in your viewDidLoad function:
self.tableView.registerClass(CustomHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: "SECTION_ID")
When asking, for a section header, dequeue a CustomHeaderFooterView, and insert a cell into it
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = self.tableView.dequeueReusableHeaderFooterViewWithIdentifier("SECTION_ID") as! CustomHeaderFooterView
if view.cell == nil {
let cell = self.tableView.dequeueReusableCellWithIdentifier("Cell")
view.cell = cell;
}
// Fill the cell with data here
return view;
}