How can I add a UITableView into my View-Based Application where the user will tap on more than one cell, and it will become selected, exactly like the Clock app's "New Alarm" setting named "Repeat" (Clock>Alarms> + >Repeat), and how can I get all of the selected cells in an array?
For multiple selection, add the line below in viewDidLoad()
tableView.allowsMultipleSelection = true
Configure each cell after dequeuing (or initializing) it in tableView(_:cellForRowAt:)
let selectedIndexPaths = tableView.indexPathsForSelectedRows
let rowIsSelected = selectedIndexPaths != nil && selectedIndexPaths!.contains(indexPath)
cell.accessoryType = rowIsSelected ? .checkmark : .none
// cell.accessoryView.hidden = !rowIsSelected // if using a custom image
Update each cell when it's selected/deselected
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)!
cell.accessoryType = .checkmark
// cell.accessoryView.hidden = false // if using a custom image
}
override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)!
cell.accessoryType = .none
// cell.accessoryView.hidden = true // if using a custom image
}
When you're done, get an array of all the selected rows
let selectedRows = tableView.indexPathsForSelectedRows
and get the selected data, where dataArray maps to the rows of a table view with only 1 section
let selectedData = selectedRows?.map { dataArray[$0.row].ID }
In your implementation of -tableView:didSelectRowAtIndexPath: you would set the table view cell's accessoryType property depending on its current value (so it would toggle on and off with multiple taps). For example:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)path {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:path];
if (cell.accessoryType == UITableViewCellAccessoryCheckmark) {
cell.accessoryType = UITableViewCellAccessoryNone;
} else {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
}
You could either maintain an array of selected states in addition to the cells' own accessory type state, or iterate over the cells in the table view querying for each one's state in order to read out the selected rows.
#BrendanBreg implementation didn't worked for me. #RaphaelOliveira provided good solution, but when you scrolls your table down - wrong rows become selected (because UITableView caches it's cells). So, I've slightly modified Raphael's solution:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell* cell = [tableView cellForRowAtIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell* cell = [tableView cellForRowAtIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryNone;
}
/*Here is modified part*/
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
/*
...
Your implementation stays here
we're just adding few lines to make sure
that only correct rows will be selected
*/
if([[tableView indexPathsForSelectedRows] containsObject:indexPath]) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
} else {
cell.accessoryType = UITableViewCellAccessoryNone;
}
}
self.tableView.allowsMultipleSelection = YES;
Just a quick tip in addition to the great answer above: to mimic Apple's style from the clock app (making the row select color fade back out after checking / unchecking the row), add this to the didSelectRowAtIndexPath, after the conditionals:
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
From Apple's TableMultiSelect guide.
Here it is what you needed
http://developer.apple.com/library/ios/#samplecode/TableMultiSelect/Introduction/Intro.html#//apple_ref/doc/uid/DTS40011189-Intro-DontLinkElementID_2
I have seen this problem with so many developers. Due to table view's nature of re-using cell it removes or haphazard the checks. I have created a working solution for that. Clone/download code from DevelopmentSupportWorkspace and Execute UITableViewTest project from there.
Here is code summery for that:
#interface CheckBoxTestTableViewController ()
#property (nonatomic, strong) NSArray *dataArray;
#property (nonatomic, strong) NSMutableDictionary *selectedIndexDictionary;
#end
#implementation CheckBoxTestTableViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
//
_dataArray = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"ImageList" ofType:#"plist"]];
_selectedIndexDictionary = [NSMutableDictionary dictionary];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return _dataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"checkMarkCell" forIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryNone;
// Configure the cell...
cell.textLabel.text = _dataArray[indexPath.row][#"text"];
if (_selectedIndexDictionary[indexPath] != nil) cell.accessoryType = UITableViewCellAccessoryCheckmark;
return cell;
}
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
if (_selectedIndexDictionary[indexPath] == nil) {
[_selectedIndexDictionary setObject:_dataArray[indexPath.row] forKey:indexPath];
[[tableView cellForRowAtIndexPath:indexPath] setAccessoryType:UITableViewCellAccessoryCheckmark];
}else{
[_selectedIndexDictionary removeObjectForKey:indexPath];
[[tableView cellForRowAtIndexPath:indexPath] setAccessoryType:UITableViewCellAccessoryNone];
}
// [tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
#end
The clock alarms repeat table view is not multiple selection. Think of it as a lit of checkboxes.
When a cell is selected, the font color and accessory type are changed and the selection fades out. When a checked cell is selected, the font color and accessory type are changed back and the selection fades out.
In your didSelectRowAtIndexPath delegate method, you would set the text color and accessory type for the selected cell, then deselect the cell. You would also record the new state in your data model. That could be as simple as a bit mask representing the selected state, but depends on what data you are displaying.
In your cellForRowAtIndexPath: dataSource method, set the text color and accessory type based on your data model.
Actual multiple selection would be similar. You have to keep track of which cells are selected, and set the selected cell of each state as it is created or shown. When the table view reports that a cell is selected, toggle the selection state in your data model and set the selected state of the cell accordingly.
You can't key off indexPath because the cells that refers to changes as you scoll. Put an NSLog in cellForRowAtIndexPath to see that. You can do the check/uncheck in willSelectRowAtIndexPath or didSelectRowAtIndexPath. That covers only the initial check or uncheck though, and will also have things appear as checked once you've scrolled because the underlying cell for a given indexPath changes.
So the solution I found is to have an array of selected things with something that is specific to that given cell, and do the initial check.
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell* cell = [tableView cellForRowAtIndexPath:indexPath];
if (![selectedIndexes containsObject:cell.textLabel.text]){
[selectedIndexes addObject:cell.textLabel.text];
cell.accessoryType = UITableViewCellAccessoryCheckmark;
} else {
[selectedIndexes removeObject:cell.textLabel.text];
cell.accessoryType = UITableViewCellAccessoryNone;
}
return indexPath;
}
You also have to have logic in cellForRowAtIndexPath to make sure the right stuff is checked or not as the view scrolls:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
...
cell.accessoryType = UITableViewCellAccessoryNone;
if ([selectedIndexes containsObject:cell.textLabel.text]) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
[cell setSelected:YES animated:YES];
}
return cell;
}
1 - Allow multiple selection and toggle of selected state:
tableView.allowsMultipleSelection = true
2 - Collect or get an array of all selected indices when you are done:
let selected = tableView.indexPathsForSelectedRows
Note: This is independent of which cells are currently showing on screen.
3 - Change the appearance of the cell depending on selected state: Override this method in your UITableViewCell subclass. If you don't have a subclass, make one.
// In UITableViewCell subclass
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
accessoryType = selected ? .Checkmark : .None
}
I know that this is an old posting, but for future use, the following code will solve the problem of ensuring that a checkmark will only appear on selected rows during scrolling:
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cell.accessoryType = (cell.isSelected) ? .checkmark : .none
}
Related
I am working with grouped tableview with multiple sections.
and I have to implement the functionality of multiple selection on didselectrow at indexpath
method. my code is as follows.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)path
{
UITableViewCell *cell = [tableView cellForRowAtIndexPath:path];
if (cell.accessoryType == UITableViewCellAccessoryCheckmark)
{
cell.accessoryType = UITableViewCellAccessoryNone;
}
else
{
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
}
which allows me to select multiple cells
but when I scroll my tableview at that time my selection disappears.
Your selection goes off when you scroll because it calls cellForRowAtIndexPath and there you have not handle selection.
To avoid this problem you can do as follows:
In didSelectRowAtIndexPath you can save index path of selected row as follows:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)path
{
UITableViewCell *cell = [tableView cellForRowAtIndexPath:path];
if (cell.accessoryType == UITableViewCellAccessoryCheckmark)
{
cell.accessoryType = UITableViewCellAccessoryNone;
//remove index path
[selectedIndexPathArray removeObject:path];
}
else
{
cell.accessoryType = UITableViewCellAccessoryCheckmark;
[selectedIndexPathArray addObject:path];
}
}
and in cellForRowAtIndexPath you can check whether cell is selected or not.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//If selectedIndexPathArray contains current index path then display checkmark.
if([selectedIndexPathArray containsObject:indexPath])
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
Try this
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
if (self.tableView.isEditing) {
cell.selectionStyle = UITableViewCellSelectionStyleBlue;
} else {
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
return cell;
}
-(UITableViewCellEditingStyle) tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewCellEditingStyleMultiSelect;
}
-(IBAction) switchEditing {
[self.tableView setEditing:![self.tableView isEditing]];
[self.tableView reloadData]; // force reload to reset selection style
}
Hope this helps solving your problem.(ref)
Your selection disapear cause the method CellForRowAtIndexPath will be called at scroll.
You need to set the accessory again.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
...
// here
...
}
You are getting this issue because you are not tracking your row selections.When the callback method cellForRowAtIndexPath gets called for the rows that had disappeared/(scrolled up/down) the cell object no longer remembers whether it was selected or not. (reason those selections are disappearing)
I would suggest you to use a collection like NSMutableArray/NSArray to track the selected rows.
You can use either of these approaches.
This would be a quick working fix:
Add/Remove the index path object in didSelectRowAtIndexPath based on the users selection
and then based on the contents of that array u can toggle the value of cell.accessoryType for the corresponding cell.
Ideally,you can use a data bean/model with some boolean member called selected and u can update its value based on the selection made.Then instead of simply adding those index path u can add those meaningful data bean objects onto your array and get the selections back from the selected property of the bean.This approach would help u in getting back the row selections even if the user kills and restarts the app provided u persist the bean objects in database/archive..(But it all depends on ur use case and requirements!)
Hope this helped!
I have two tableviews, one has several tableview cells each cell opens the same subview but initalized with new data..
There are around about 100 - 200 entries into the table and I have a accessory view that is a tick that when a cell is selected it ticks the cell then loads the main view again.
If I select that same cell to get the same dataset back it loads the previously selected cell in the middle of the screen (so it knows its index path) however the tick "depending on how deep in the list" will or will not be visible..
It tends to work in about the top 30/40% of the table but anything lower the tick will not be visible... that is unless I go back and forth getting deeper and deeper each time then sometimes I can get the tick to appear in the deeper part of the tableview.. Would anyone know why this is happening?
Has anyone had something of this nature happen to them before?
At further investigation I think something is going wrong inside this method..
First of all, in the subview once the user selects a cell this method is called
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Navigation logic may go here. Create and push another view controller.
[self.navigationController popViewControllerAnimated:YES]; //pops current view from the navigatoin stack
//accesses selected cells content
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
// now you can use cell.textLabel.text
//This if statment is based off which cell was selected in the parent view so that it knows which cell to pass the data back to
if (parentViewSelectedIndexPath.section == 0) {
if (parentViewSelectedIndexPath.row == 0) {
manufactureCellTextLabel = cell.textLabel.text; //passing label text over to NSString for use with delegate (check "viewwilldissapear")
[[self delegate] setManufactureSearchFields:manufactureCellTextLabel withIndexPath:indexPath]; //This is where I pass the value back to the mainview
}
// a few more If statements for the other methods I can pass data too.
//--- this if block allows only one cell selection at a time
if (oldCheckedData == nil) { // No selection made yet
oldCheckedData = indexPath;
[cell setAccessoryType:UITableViewCellAccessoryCheckmark];
}
else {
UITableViewCell *formerSelectedcell = [tableView cellForRowAtIndexPath:oldCheckedData]; // finding the already selected cell
[formerSelectedcell setAccessoryType:UITableViewCellAccessoryNone];
[cell setAccessoryType:UITableViewCellAccessoryCheckmark]; // 'select' the new cell
oldCheckedData = indexPath;
}
}
This passes Index path over to the main view method...
- (void) setManufactureSearchFields:(NSString *)cellLabeltext withIndexPath:(NSIndexPath *)myIndexPath
{
manufactureSearchObjectString = cellLabeltext;
manufactureResultIndexPath = myIndexPath;
[self.tableView reloadData]; //reloads the tabels so you can see the value.
}
//Which then sets the manufactureResultIndexPath that is used in the next method to pass it back to the subview
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Navigation logic may go here. Create and push another view controller.
//--- Idendify selected indexPath (section/row)
if (indexPath.section == 0) {
//--- Get the subview ready for use
VehicleResultViewController *vehicleResultViewController = [[VehicleResultViewController alloc] initWithNibName:#"VehicleResultViewController" bundle:nil];
// ...
//--- Sets the back button for the new view that loads
self.navigationItem.backBarButtonItem = [[[UIBarButtonItem alloc] initWithTitle:#"Back" style: UIBarButtonItemStyleBordered target:nil action:nil] autorelease];
// Pass the selected object to the new view controller.
[self.navigationController pushViewController:vehicleResultViewController animated:YES];
[vehicleResultViewController setDelegate:self];
if (indexPath.row == 0)
{
vehicleResultViewController.title = #"Manufacture";
[vehicleResultViewController setRequestString:#"ID.xml"]; //sets the request string in searchResultsViewController
vehicleResultViewController.dataSetToParse = #"ID"; // This is used to controll what data is shown on subview... logic
[vehicleResultViewController setAccessoryIndexPath:manufactureResultIndexPath]; //sends indexpath back to subview for accessory tick
vehicleResultViewController.parentViewSelectedIndexPath = indexPath;
}
//etc etc
}
And finaly I pass it to the method in my subview that passes the indexpath to oldCheckedData
- (void)setAccessoryIndexPath:(NSIndexPath *)myLastIndexPath
{
oldCheckedData = myLastIndexPath;
[self.tableView reloadData]; //<<---- this is where I reload the table to show the tick...
}
Try moving the cell.accessoryType = lines to the willDisplayCell: delegate function like so:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
// You can move this one here too:
cell.selectionStyle = UITableViewCellSelectionStyleNone; // no blue selection
if (indexPath == oldCheckedData) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
} else {
cell.accessoryType = UITableViewCellAccessoryNone;
}
}
I read that the willDisplayCell: method should be used for any basic visual related modifications to a cell like selectionStyle/accessoryType, and the cellForRowAtIndexPath: method for cell data related operations like setting text, images, etc...
I have recently come across this issue, if turned out the in my case the cell has a accessoryview set. This snippet withh ensure the view is removed.
public func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
..logic here to to determine if cell should be selected...
if ( cell.accessoryView != nil) {
cell.accessoryView?.removeFromSuperview()
cell.accessoryView = nil
}
cell.accessoryType = .checkmark
I've got a UITableView with each row containing a checkbox using UITableViewCellAccessoryCheckmark. I can't figure out how to uncheck all the checkboxes using the didSelectRowAtIndexPath method.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *oldCell;
int count = [self.myTableRowNamesArray count];
for (NSUInteger i = 0; i < count; ++i) {
// Uncheck all checkboxes
// OF COURSE THIS DOESN'T WORK
// BECAUSE i IS AN INTEGER AND INDEXPATH IS A POINTER
FOO: oldCell = [myTableView cellForRowAtIndexPath:(int)i];
// GOOD CODE:
oldCell = [penanceOptionsTableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
oldCell.accessoryType = UITableViewCellAccessoryNone;
}
UITableViewCell *newCell = [myTableView cellForRowAtIndexPath:indexPath];
newCell.accessoryType = UITableViewCellAccessoryCheckmark;
}
Instead of modifying the .accessoryType of all cells in didSelectRowAtIndexPath:, I suggest storing the selected index in some ivar, and change the .accessoryType in the data source's -tableView:cellForRowAtIndexPath: method, i.e.
-(void)tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
self.selectedIndexPath = indexPath;
[tableView reloadData];
}
-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath {
...
cell.accessoryType = [indexPath compare:self.selectedIndexPath] == NSOrderedSame
? UITableViewCellAccessoryCheckmark
: UITableViewCellAccessoryNone;
...
}
With this, only visible cells will be affected, and the million other cells outside of the screen won't need to be modified.
Quite right, here's a full implementation in Swift in the general case of selecting a cell .. you'd use selectedIndexPath elsewhere in the class as you see fit. For example, in cellForRowAtIndexPath to choose the appropriate cell prototype.
// SelectingTableViewController
import UIKit
class SelectingTableViewController: UITableViewController
{
internal var selectedIndexPath:NSIndexPath? = nil
override func viewDidLoad()
{
super.viewDidLoad()
tableView.estimatedRowHeight = 68.0
tableView.rowHeight = UITableViewAutomaticDimension
self.clearsSelectionOnViewWillAppear = false;
}
override func tableView
(tableView:UITableView, didSelectRowAtIndexPath indexPath:NSIndexPath)
{
print("did select....")
// in fact, was this very row selected,
// and the user is clicking to deselect it...
// if you don't want "click a selected row to deselect"
// then on't include this clause.
if selectedIndexPath == indexPath
{
print("(user clicked on selected to deselect)")
selectedIndexPath = nil
tableView.reloadRowsAtIndexPaths(
[indexPath],
withRowAnimation:UITableViewRowAnimation.None)
tableView.deselectRowAtIndexPath(indexPath, animated:false)
return
}
// in fact, was some other row selected??
// user is changing to this row? if so, also deselect that row
if selectedIndexPath != nil
{
let pleaseRedrawMe = selectedIndexPath!
// (note that it will be drawn un-selected
// since we're chaging the 'selectedIndexPath' global)
selectedIndexPath = indexPath
tableView.reloadRowsAtIndexPaths(
[pleaseRedrawMe, indexPath],
withRowAnimation:UITableViewRowAnimation.None)
return;
}
// no previous selection.
// simply select that new one the user just touched.
// note that you can not use Apple's willDeselectRowAtIndexPath
// functions ... because they are freaky
selectedIndexPath = indexPath
tableView.reloadRowsAtIndexPaths(
[indexPath],
withRowAnimation:UITableViewRowAnimation.None)
}
}
for (UITableViewCell *cell in [myTableView visibleCells]) {
cell.accessoryType = UITableViewCellAccessoryNone;
}
But really, you'd be better off just modifying the one cell that actually has the checkmark set. You have to have stored this information somewhere in your model anyway.
You're probably setting some kind of property with this method.
So what i do is:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// 1. first unsetting the property
[object someProperty:nil];
// 2. call the reloadData method to uncheck all the checkmarks
[tableView reloadData];
// 3. check the selected cell
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
[cell setAccessoryType:UITableViewCellAccessoryCheckmark];
// 4. set the checked property
[object setSomeProperty:[indexpath row]];
}
And in my cellForRowAtIndexPath methods i got something like the following code:
if([object someProperty] == [indexpath row]){
[cell setAccessoryType:UITableViewCellAccessoryCheckmark];
} else {
[cell setAccessoryType:UITableViewCellAccessoryNone];
}
Yes, cellForRowAtIndexPath: uses NSIndexPath instead of integer so make indexpath by using
indexPathForRow:inSection:
if you are using one section then your loop is fine just pass i in row and 0 for section.
Lets say we have a custom UITableViewCell
So whenever I click custom button on cell.. it should expand to the some extent (you can say 40 height more...) and when i click again to the same custom button it should collapse to the previous height.
Developer's please guide me.. how can I achieve this task
I'm not going to say anything here to contradict the accepted answer considering it is perfectly correct. However, I am going to go into more detail on how to accomplish this. If you don't want to read through all this and are more interested in playing with the source code in a working project, I've uploaded an example project to GitHub.
The basic idea is is to have a condition inside of the method -tableView: heightForRowAtIndexPath: that determines whether or not the current cell should be expanded. This will be triggered by calling begin/end updates on the table from within -tableView: didSelectRowAtIndexPath: In this example, I'll show how to make a table view that allows for one cell to be expanded at a time.
The first thing that you'll need to do is declare a reference to an NSIndexPath object. You can do this however you want, but I recommend using a property declaration like this:
#property (strong, nonatomic) NSIndexPath *expandedIndexPath;
NOTE: You do not need to create this index path inside viewDidLoad, or any other similar method. The fact that the index is initially nil will only mean that the table will not initially have an expanded row. If you would rather the table start off with a row of your choice expanded, you could add something like this to your viewDidLoad method:
NSInteger row = 1;
NSInteger section = 2;
self.expandedIndexPath = [NSIndexPath indexPathForRow:row inSection:section];
The next step is to head on over to your UITableViewDelegate method -tableView: didSelectRowAtIndexPath: to add the logic to alter the expanded cell index based on the users selection. The idea here is to check the index path that has just been selected against the index path stored inside the expandedIndexPath variable. If the two are a match, then we know that the user is trying to deselect the expanded cell in which case, we set the variable to nil. Otherwise, we set the expandedIndexPath variable to the index that was just selected. This is all done between calls to beginUpdates/endUpdates, to allow the table view to automatically handle the transition animation.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView beginUpdates]; // tell the table you're about to start making changes
// If the index path of the currently expanded cell is the same as the index that
// has just been tapped set the expanded index to nil so that there aren't any
// expanded cells, otherwise, set the expanded index to the index that has just
// been selected.
if ([indexPath compare:self.expandedIndexPath] == NSOrderedSame) {
self.expandedIndexPath = nil;
} else {
self.expandedIndexPath = indexPath;
}
[tableView endUpdates]; // tell the table you're done making your changes
}
Then the final step is in another UITableViewDelegate method -tableView: heightForRowAtIndexPath:. This method will be called after you've triggered beginUpdates once for each index path that the table determines needs updating. This is where you'll compare the expandedIndexPath against the index path that is currently being reevaluated.
If the two index paths are the same, then this is the cell that you wish to be expanded, otherwise it's height should be normal. I used the values 100 and 44, but you can use what ever suits your needs.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Compares the index path for the current cell to the index path stored in the expanded
// index path variable. If the two match, return a height of 100 points, otherwise return
// a height of 44 points.
if ([indexPath compare:self.expandedIndexPath] == NSOrderedSame) {
return 100.0; // Expanded height
}
return 44.0; // Normal height
}
Implement heightForRowAtIndexPath to calculate the right height. Then in the code for your button, force the table to reevaluate each cell's height with beginUpdates plus endUpdates:
[self.tableView beginUpdates];
[self.tableView endUpdates];
Changes to the tableview cells' heights will automatically be calculated with heightForRowAtIndexPath and the changes will be animated too.
In fact, instead of a button on your cell that does this, you might even just make selecting the cell do this in didSelectRowAtIndexPath.
Instead of using [tableView beginUpdates] and [tableView endUpdates] , I'm using the [tableView reloadRowsAtIndexPath:... withRowAnimation:...] method inside the didSelectRowAtIndexPath method.
I prefer this, because I had some problems with elements that should show, when I expand my UITableViewCell, when I used the begin & end updates methods. Another point is that you can choose between some animations like: Top, Bottom, Left, Right...
I have created an open source library for this. You just implement collapse and expand delegates in your code and voilà! you can also perform any drawings and animations. check out this.
I've made a reusable component that will do exactly what you're talking about. It's pretty easy to use, and there's a demo project.
GCRetractableSectionController on GitHub.
This is Mick's answer but for Swift 4. (IndexPath replaces NSIndexPath, which comes with an empty IndexPath as nil would crash Swift. Also, you can compare two instances of IndexPath using ==)
Declare the expandedIndexPath property.
var expandedIndexPath = IndexPath()
Optional viewDidLoad part.
expandedIndexPath = IndexPath(row: 1, section: 2)
Then the didSelectRow part.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.beginUpdates()
if indexPath == expandedIndexPath {
expandedIndexPath = IndexPath()
} else {
expandedIndexPath = indexPath
}
tableView.endUpdates()
}
Then the heightForRow part.
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath == expandedIndexPath {
return 100
}
return 44
}
I used Gcamp's source code and made my own version.
1) In a loadView method initialize a mutable array where you will save expanded or non-expanded states of your sections. It is critical to save expanded statuses in a separate array, that is not destroyed while table view scrolls (for instance if you store it in a headerView it will be redrawn and forget weather it was expanded or not). In my case it is _sectionStatuses array.
- (void)loadView
{
// At the beginning all sections are expanded
_sectionStates = [NSMutableArray arrayWithCapacity:self.tableView.numberOfSections];
for (int i = 0; i < self.tableView.numberOfSections; i++) {
_sectionStates[i] = [NSNumber numberWithBool:YES];
}
}
2) Create a custom headerView for a section with a button for expanding. Delegate an action from a button in your headerView to your TableViewController using delegation pattern. You can find suitable images in Gcamp's source code.
3) Create an action to remove or add rows. Here _foldersArray is my structure, that contains all the data. My section's headerView - MCExpandableAccountHeaderView knows it's own section number - I transfer it there when I create header views for each section. It is critical to transfer it to this method, since you have to know which section is now expanded or stretched.
- (void)expandClicked:(MCAccountHeaderView *)sender
{
MCExpandableAccountHeaderView *expandableAccountHeaderView = (MCExpandableAccountHeaderView*)sender;
// Finding a section, where a button was tapped
NSInteger section = expandableAccountHeaderView.section;
// Number of rows, that must be in a section when it is expanded
NSUInteger contentCount = [_foldersArray[section - 1][#"folders"] count];
// Change a saved status of a section
BOOL expanded = [_sectionStates[section] boolValue];
expanded = ! expanded;
expandableAccountHeaderView.expanded = expanded;
_sectionStates[section] = [NSNumber numberWithBool:expanded];
// Animation in a table
[self.tableView beginUpdates];
NSMutableArray* modifiedIndexPaths = [[NSMutableArray alloc] init];
for (NSUInteger i = 0; i < contentCount; i++) {
NSIndexPath* indexPath = [NSIndexPath indexPathForRow:i inSection:section];
[modifiedIndexPaths addObject:indexPath];
}
if (expandableAccountHeaderView.expanded) [self.tableView insertRowsAtIndexPaths:modifiedIndexPaths withRowAnimation:UITableViewRowAnimationFade];
else [self.tableView deleteRowsAtIndexPaths:modifiedIndexPaths withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
// Scroll to the top of current expanded section
if (expandableAccountHeaderView.expanded) [self.tableView scrollToRowAtIndexPath:INDEX_PATH(0, section) atScrollPosition:UITableViewScrollPositionTop animated:YES];
}
4) It is also important to return correct number or rows in a section depending on wheather it is expanded or not.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
BOOL expanded = [_sectionStates[section] boolValue];
return expanded ? [_foldersArray[section - 1][#"folders"] count] : 0;
}
initialize iSelectedIndex = -1; and declare
UITableView *urTableView;
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
return 10; //Section count
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 3; //row count
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
[cell.textLabel setText:[NSString stringWithFormat:#"sec:%d,row:%d",indexPath.section,indexPath.row]];
return cell;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{
// adding a label with the tap gesture to the header in each section
headerLabel = [[UILabel alloc]init];
headerLabel.tag = section;
headerLabel.userInteractionEnabled = YES;
headerLabel.backgroundColor = [UIColor greenColor];
headerLabel.text = [NSString stringWithFormat:#"Header No.%d",section];
headerLabel.frame = CGRectMake(0, 0, tableView.tableHeaderView.frame.size.width, tableView.tableHeaderView.frame.size.height);
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc]initWithTarget:self action:#selector(gestureTapped:)];
[headerLabel addGestureRecognizer:tapGesture];
return headerLabel;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section{
return 50.0; //adjust the height as you need
}
- (void)gestureTapped:(UITapGestureRecognizer *)sender{
UIView *theSuperview = self.view; // whatever view contains
CGPoint touchPointInSuperview = [sender locationInView:theSuperview];
UIView *touchedView = [theSuperview hitTest:touchPointInSuperview withEvent:nil];
if([touchedView isKindOfClass:[UILabel class]])
{
if (iSelectedIndex != touchedView.tag) { //if new header is selected , need to expand
iSelectedIndex = touchedView.tag;
}else{ // if the header is already expanded , need to collapse
iSelectedIndex = -1;
}
[urTableView beginUpdates];
[urTableView endUpdates];
}
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// Show or hide cell
float height = 0.0;
if (indexPath.section == iSelectedIndex) {
height = 44.0; // Show the cell - adjust the height as you need
}
return height;
}
For me it works to use:
on UITableViewDelegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("Did select row: \(indexPath.row).")
tableView.beginUpdates()
tableView.endUpdates()
}
on selectable/expandable UITableViewCell
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
configStyle(selected)
}
Important! tableView.rowHeight is .automatic and UITableViewCell is constraint to enable automatic height calculation, i.e. its height constraint are clearly defined like constraint to top/bottom or height constraints added or label intrinsic content size is used.
To add to 0x7fffffff's answer, I found I needed an extra condition in the if statement within didSelectRowAtIndexPath - thus:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView beginUpdates];
if (self.expandedIndexPath && [indexPath compare:self.expandedIndexPath] == NSOrderedSame) {
self.expandedIndexPath = nil;
} else {
self.expandedIndexPath = indexPath;
}
[tableView endUpdates];
}
Following this medium article on how to expand the cells based on the tap of a button and setting the numbersOfLine for a specific label, I was able to perform the animation using
tableView.beginUpdates()
tableView.performBatchUpdates({
cell.description.numberOfLines = !expanded ? 0 : 3
}, completion: nil)
tableView.endUpdates()
Notice performBatchUpdates is only available in iOS 11⬆️
I would like to know how to get the textLabel string value of the selected uitableviewcell.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
// now you can use cell.textLabel.text
}
You can get the cell using [self.tableView cellForRowAtIndexPath:] and then access its textLabel.text property, but there's usually a better way.
Typically you've populated your table based on some model array that your UITableViewController has access to. So a better way to handle this in most cases is to take the row number of the cell that was selected and use that to find the associated data in your model.
For example, let's say your controller has an array of Buddy objects, which have a name property:
NSArray *buddies;
You populate this array by running a query or something. Then in tableView:cellForRowAtIndexPath: you build a table view cell based on each buddy's name:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"BuddyCell"];
if (!cell) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"BuddyCell"] autorelease];
}
cell.textLabel.text = [buddies objectAtIndex:indexPath.row];
return cell;
}
Now when the user selects a row, you just pull the corresponding Buddy object out of your array and do something with it.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
Buddy *myBuddy = [buddies objectAtIndex:indexPath.row];
NSLog (#"Buddy selected: %#", myBuddy.name);
}
if (selected)
{
indicator.image = [UIImage imageNamed:#"IsSelected.png"];
[arrSlectedItem addObject:strselecteditem];
NSLog(#"-- added name is %#", strselecteditem);
}
else
{
indicator.image = [UIImage imageNamed:#"NotSelected.png"];
[arrSlectedItem removeObject:strselecteditem];
NSLog(#"--- remove element is -- %#", strselecteditem);
}
If anyone stumbles across this and is wondering how to do this in swift the code is below. Also remember to use optional binding to unwrap that optional and also avoid printing out "Optional("Tapped Item Label")".
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let cell = tableView.cellForRowAtIndexPath(indexPath)
// Unwrap that optional
if let label = cell?.textLabel?.text {
println("Tapped \(label)")
}
}
Here what i used;very simple
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath){
if editingStyle == UITableViewCellEditingStyle.Delete
{
//create cellobj with indexpath for get it text
let cell = tableView.cellForRowAtIndexPath(indexPath)
let celltext = (cell?.textLabel?.text!)! as String
//do what ever you want with value
print((cell?.textLabel?.text!)! as String)
}
}