UITableView - smooth expand and collapse showing a UICollectionView - ios

I am working on a feature in an app where the user can tap on some attribute about a car (such as its color) and a list of different colors will be shown.
This is just part of the page. The whole page consists of different views and view controllers all wrapped together in a vertical stack view, but I do not think that is what is causing the animation issues here. This is the overall structure of the page:
UIScrollView
UIStackView
UIViewController
UIView
UITableViewController <-- this is the class being shown in this question
...
But for this question, I am just showing the section of the page where this table is.
So I am having an issue with the animation of the table as can be seen in the gifs later on. The way this section is structured is that it is a UITableViewController subclass that takes in a model (a title and a list of car colors) and it displays a different table section for each model element.
Each section has a section header, which is the part where the user taps. It shows the color and it shows a preview image. When that is tapped, a table row is added to the table for that section and that change is animated. The table view row that is added to the table contains a UICollectionView that lays out content horizontally. This idea came from the WWDC 2010 - Mastering Table Views video.
It is basically something like this. Each section header is the interactive part where you can tap on it. Then the row that is shown in the section has a collection view.
* SectionHeader
* SectionHeader
* Table row containing a UICollectionView
* SectionHeader
The problem I am having is that the animation is behaving in a strange manner, as can be seen in the following images.
The first image shows what happens if a row is expanded that has other rows below it. In this case, it appears as if the other two rows in the table are essentially floating above the content of the new row that is animating in. Additionally, at the bottom of the section, those two rows come in from the bottom as the previous two rows sort of get vertically squished until they are gone.
The second animation here shows the bottom row expanding. This one is closer to what I want, except that the row content still briefly shows at the top of the row above the divider (which is the section footer of the previous section).
Here is my code for the table view controller class.
MyAttributeTableViewController.h
#import <UIKit/UIKit.h>
#import "MyAttribute.h"
#import "MyAttributeTableViewCell.h"
NS_ASSUME_NONNULL_BEGIN
#interface MyAttributeTableViewController : UITableViewController<MyAttributeDelegate>
//this represents each section of the table. It contains a title (color, for example), a selected value (red),
//and a list of possible values that will be used to display the collection view when the section is expanded.
#property (strong, nonatomic) NSArray<MyAttribute *> *modelAttributes;
#end
NS_ASSUME_NONNULL_END
MyAttributeTableViewController.m
#import "MyAttributeTableViewController.h"
#import "MyAttributeTableHeaderView.h"
#import "MyAttributeTableViewCell.h"
#interface MyAttributeTableViewController ()
//this keeps track of which sections are expanded. Initially, all sections start out
//not expanded. Then, upon tap, a section's expanded status is toggled here. This is
//used by the table view's data source to know when to display a row in a section.
#property (strong, nonatomic) NSMutableArray<NSNumber *> *itemsExpanded;
#end
#implementation MyAttributeTableViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = UITableViewAutomaticDimension;
self.tableView.sectionHeaderHeight = UITableViewAutomaticDimension;
self.tableView.estimatedSectionHeaderHeight = UITableViewAutomaticDimension;
self.tableView.estimatedSectionFooterHeight = CGFLOAT_MIN;
//this xib file only contains the UICollectionView that is shown upon expanding the section.
//There is nothing that interesting in this file, but I have some screenshots of the
//attribute inspector for this xib file after this code.
[self.tableView registerNib:[UINib nibWithNibName:#"MyAttributeTableViewCell" bundle:nil] forCellReuseIdentifier:#"MyAttributeTableViewCell"];
[self.tableView registerClass:[MyAttributeTableHeaderView class] forHeaderFooterViewReuseIdentifier:#"AttributeHeaderView"];
[self.tableView invalidateIntrinsicContentSize];
}
- (void)setAttributeOptions:(NSArray<MyAttribute *> *)modelAttributes {
self->_modelAttributes = modelAttributes;
self->_itemsExpanded = [NSMutableArray arrayWithCapacity:[self->_modelAttributes count]];
for (NSUInteger x=0; x<[self->_modelAttributes count]; x++) {
[self->_itemsExpanded addObject:#NO];
}
[self.tableView reloadData];
}
//TODO: is this necessary?
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
self.preferredContentSize = self.tableView.contentSize;
}
#pragma mark - MyAttributeDelegate
- (void)twister:(MyAttributeTableViewCell *)cell didSelectItemAtIndex:(NSInteger)index {
UITableViewHeaderFooterView *headerView = [self.tableView headerViewForSection:cell.sectionIndex];
if ([headerView isKindOfClass:[MyAttributeTableHeaderView class]]) {
MyAttributeTableHeaderView *twisterHeaderView = (MyAttributeTableHeaderView *)headerView;
twisterHeaderView.twister.selectedSwatchIndex = [NSNumber numberWithInteger:index];
[twisterHeaderView updateAttributeIfNeeded];
}
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.modelAttributes.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.itemsExpanded[section].boolValue ? 1 : 0;
}
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
MyAttributeTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"MyAttributeTableViewCell" forIndexPath:indexPath];
MyAttribute *twister = self.modelAttributes[indexPath.section];
cell.swatches = twister.swatches;
NSNumber *swatchCellHeight = cell.swatchCellHeight;
if (swatchCellHeight) {
return swatchCellHeight.floatValue + 20.0f; //TODO: need to get this from the collection view
}
return 352.0f;
}
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
if (section == self.modelAttributes.count) {
return CGFLOAT_MIN;
}
return 1.0f;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MyAttributeTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"MyAttributeTableViewCell" forIndexPath:indexPath];
MyAttribute *twister = self.modelAttributes[indexPath.section];
cell.swatches = twister.swatches;
cell.sectionIndex = indexPath.section;
cell.delegate = self;
if ([twister isSwatchSelected]) {
NSInteger selectedIndex = twister.selectedSwatchIndex.integerValue;
[cell.collectionView selectItemAtIndexPath:[NSIndexPath indexPathForItem:selectedIndex inSection:0] animated:YES scrollPosition:UICollectionViewScrollPositionCenteredHorizontally];
}
return cell;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
MyAttributeTableHeaderView *cell = (MyAttributeTableHeaderView *)[tableView dequeueReusableHeaderFooterViewWithIdentifier:#"AttributeHeaderView"];
if (cell.gestureRecognizers.count == 0) {
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(toggleSection:)];
[cell addGestureRecognizer:tapRecognizer];
}
cell.sectionIndex = section;
cell.expanded = self.itemsExpanded[section].boolValue;
cell.twister = self.modelAttributes[section];
return cell;
}
- (void)toggleSection:(UITapGestureRecognizer *)gesture {
MyAttributeTableHeaderView *destinationView = ((MyAttributeTableHeaderView *)gesture.view);
BOOL expanded = [destinationView toggleExpanded];
self.itemsExpanded[destinationView.sectionIndex] = [NSNumber numberWithBool:expanded];
UIView *viewToLayout = self.tableView;
while ([viewToLayout superview]) {
viewToLayout = viewToLayout.superview;
}
if (expanded) {
[UIView beginAnimations:#"expandTableAnimationId" context:nil];
[UIView setAnimationDuration:1.0f];
[CATransaction begin];
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:0 inSection:destinationView.sectionIndex]] withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
[viewToLayout layoutIfNeeded];
[CATransaction commit];
[UIView commitAnimations];
} else {
[UIView beginAnimations:#"collapseTableAnimationId" context:nil];
[UIView setAnimationDuration:1.0f];
[CATransaction begin];
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:0 inSection:destinationView.sectionIndex]] withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
[viewToLayout layoutIfNeeded];
[CATransaction commit];
[UIView commitAnimations];
}
}
#end
MyAttributeTableViewCell
Does anyone know what I am doing wrong or how to get these animations to look correct (no doubling of rows and no collection view showing up above the row when it is animating in)? Or if you know a better way to handle this that might be less complicated, I am open to that as well. I am just trying to have a list of expandable sections, where each section has a collection view of selectable items.

Related

Expand and Collapse All Table View Cells

I need some UI advice.
I have a view that needs to load data formatted like this:
{
"heading": "This is a header",
"content": "This is some detailed content about the header"
},
{
"heading": "This is another headline.",
"content": " These are more details about the headline. "
}
Here are the parameters: On load, it should just display the headings in a table view. Tapping a heading will expand that cell and load the the content or details about it. Here is a rough sketch:
On Load:
Heading 1
Heading 2
Heading 3
Heading 4
On Tap of Heading 2:
Heading 1
Heading 2
Content for Heading 2 get displayed here
Heading 3
Heading 4
There also needs to be a bar button item that will either expand or collapse all cells. Which would like this:
Collapse All:
Heading 1
Heading 2
Heading 3
Heading 4
Expand All:
Heading 1
Content for Heading 1 get displayed here
Heading 2
Content for Heading 2 get displayed here
Heading 3
Content for Heading 3 get displayed here
Heading 4
Content for Heading 4 get displayed here
I used some weird parent/child logic to get the individual cells to expand, but I think I went down a dark path because now I am trying to implement the expand/collapse all and I am stuck.
Does anyone know of any open source code that does this type of accordion table view and/or any suggestion on how to setup a view controller to do this? I have seen a few libraries expand and collapse individual cells, but being able to do all of them is getting tricky.
this is how I do it, perhaps a slightly simpler, although definitely similar approach :)
#import "ViewController.h"
//dont worry, the header is empty except for import <UIKit/UIKit.h>, this is a subclass on UIViewController
#interface ViewController ()<UITableViewDataSource, UITableViewDelegate>
#property (weak, nonatomic) UITableView *tableView;
#end
#implementation ViewController
{
//ivars
BOOL sectionIsOpen[4]; //we will use this BOOL array to keep track of the open/closed state for each section. Obviously I have the number of sections fixed at 4 here, but you could make a more dynamic array with malloc() if neccesary..
}
- (void)viewDidLoad {
[super viewDidLoad];
UITableView *tv = [[UITableView alloc]initWithFrame:self.view.bounds style:UITableViewStyleGrouped];
tv.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
tv.dataSource = self;
tv.delegate = self;
[self.view addSubview:tv];
self.tableView = tv;
// Do any additional setup after loading the view, typically from a nib.
}
#pragma mark - UITableViewDataSource
-(NSInteger )numberOfSectionsInTableView:(UITableView *)tableView{
return 4;
}
-(NSInteger )tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return ((sectionIsOpen[section]) ? [self numberOfRowsInSection:section] : 0);
}
-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section{
//put your switch() here...
return [NSString stringWithFormat:#"I am section %i", (int)section ];
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *cellId = #"cellID";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId];
if (!cell) {
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellId];
}
//etc etc decorate your cell...
cell.textLabel.text = [NSString stringWithFormat:#"cell %i / %i", (int)indexPath.section, (int)indexPath.row ];
return cell;
}
#pragma mark - UITableViewDelegate
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{
const CGRect fr = CGRectMake(0, 0, 320.0, 40.0 );
UIButton *btn = [[UIButton alloc]initWithFrame:fr];
[btn setTitle:[self tableView:tableView titleForHeaderInSection:section] forState:UIControlStateNormal ];
[btn setTag:section];
[btn addTarget:self action:#selector(sectionOpenToggle:) forControlEvents:UIControlEventTouchUpInside];
// add an image, colour etc if you like
return btn;
}
#pragma mark - tableViewHelpers
//the number of rows in sectionX when it is open...
-(NSInteger )numberOfRowsInSection:(NSInteger )section{
return section + 1;
}
//opening/closing a section
-(void )setSection:(NSInteger )section toOpen:(BOOL )open{
if (open != sectionIsOpen[section]) {
//build an array of indexPath objects
NSMutableArray *indxPths = [NSMutableArray array];
for (NSInteger i = 0; i < [self numberOfRowsInSection:section]; i ++) {
[indxPths addObject: [NSIndexPath indexPathForRow:i inSection:section ]
];
}
[self.tableView beginUpdates];
if (open) {
[self.tableView insertRowsAtIndexPaths:indxPths withRowAnimation:UITableViewRowAnimationFade];
//nb there is a large ENUM of tableViewRowAnimation types to experiment with..
}else{
[self.tableView deleteRowsAtIndexPaths:indxPths withRowAnimation:UITableViewRowAnimationFade];
}
sectionIsOpen[section] = open;
[self.tableView endUpdates];
}
}
-(void )sectionOpenToggle:(id )sender{
[self setSection:[sender tag] toOpen: !sectionIsOpen[[sender tag]] ];
}
// open/close all sections.
-(void )setAllSectionsOpen:(BOOL )open{
for (NSInteger i = 0; i < [self numberOfSectionsInTableView:self.tableView]; i ++) {
[self setSection:i toOpen:open];
}
}
//these two for your convenience, hook up to navbar items etc..
-(IBAction)openAllSections:(id)sender{
[self setAllSectionsOpen:YES];
}
-(IBAction)closeAllSections:(id)sender{
[self setAllSectionsOpen:NO];
}
#end
I'm not a native english speaker, so if some of my explanations aren't clear enough, tell me, and I'll try to rephrase them.
Anyway, could be better ways to do it, but that was just at the top of my head,
The below code might look scary and complicated, but I personally think it is really
straight-forward,
I've just added a lot of comments, explaining each step, so the code actually not as
long/complicated/messy as it might appear at first glance.
From your example above, it seems to me that your data source is an NSArray which contains NSDictionary objects,
And that each NSDictionary contains one header (section), and only one content (row) for that section,
So the below example is set to handle just that- a table view, with multiple sections, and one row on each section,
Which its data source is NSArray which contains NSDictionary objects.
Since I don't know if your current data source is mutable or not, in my example I will first create an mutable copy of it, and will use it throughout the code.
Throughout the code I will assume self.tableView is your table view, and self.dataArray is the array of dictionaries you've posted above,
I also assume that you've already set the table view's delegate and dataSource to be self, either in code or in storyboard.
// Here we define the height of each header,
// I've arbitrarily chosen 50 points. You can change it as you like.
// The reason I've declare it like this, is that I'm using its
// height to also create a UIView for the header, so this way
// if you want to change the height, you need to only change it once.
#define kHeaderHeight 50
-(void)viewDidLoad {
...
...
// The method below will be called to create an mutable copies of the
// dictionaries in your data source, plus add them another object
// which will indicate in our code if the correspond header
// should be expanded or collapsed
[self createDataSource];
// The below line is not mandatory, but I personally like to add it
// So collapsed sections won't have the row's 'bounds' under them.
self.tableView.tableFooterView = [[UIView alloc] initWithFrame: CGRectZero];
...
...
}
-(void)createDataSource {
// Here we are basically going to create a temporary mutable array,
// then we are going to iterate through self.dataArray array,
// Make a mutable copy of every dictionary in it, Add a BOOL value
// it that indicates if the row is expanded or not, add the new mutable
// dictionary to the temporary array, and then make self.dataArray
// point to an immutable copy of the new array we've created
NSMutableArray *array = [[NSMutableArray alloc] init];
for(int i = 0; i < [self.dataArray count]; i++) {
NSMutableDictionary *dict = [self.dataArray[i] mutableCopy];
[dict setObject:#(NO) forKey:#"expanded"];
[array addObject:dict];
}
self.dataArray = [array copy];
}
// Now we will set the height of each header.
-(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return kHeaderHeight;
}
// Here we create a custom view for the header, so we can make its title clickable,
// That will expand/collapse each section
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
// Here we are creating a view for the header, and use our defined header
// height to set its height appropriately.
UIView *header = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, kHeaderHeight)];
// Then create a button
// I've arbitrarily chosen a size of 100x20 and created a frame to be placed in the
// middle of the above header
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(header.frame.size.width/2 - 50, header.frame.size.height/2 - 10, 100, 20)];
NSString *headerTitle = [self.dataArray[section] objectForKey:#"heading"];
[button setTitle:headerTitle forState:UIControlStateNormal];
// We set the button tag to correspond to its section for future use
button.tag = section;
// I've arbitrarily chose to set the button colour to gray colour
button.titleLabel.textColor = [UIColor grayColor];
// Then we need to actually add an action to the button
[button addTarget:self action:#selector(updateTableView:) forControlEvents:UIControlEventTouchUpInside];
// Then we need to add the button to the header view itself
[header addSubview:button];
return header;
}
// Here we are just setting the number of sections to correspond to the
// number of items we have in our data array
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return [self.dataArray count];
}
// Here we are finally using the BOOl value we've added to our dictionary at the
// beginning of our code. If the "expanded" BOOL value is YES, return 1 row,
// else, the section is collapsed, so return 0 rows
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if([[self.dataArray[section] objectForKey:#"expanded"] boolValue]) {
return 1;
} else {
return 0;
}
}
// Here is just a simple method to create the cells that will use our dataArray
// as their source for their title
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *identifier = #"cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if(!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
}
cell.textLabel.text = [self.dataArray[indexPath.row] objectForKey:#"content"];
return cell;
}
// This method being called when clicking on the table view's headers
// it uses the button tag we've set earlier, to determine which section we're trying
// to collape/expand, it retrieves the current "expanded" bool value, and then store
// the opposite value back in our dictionary (so if table was collapsed, meaning
// its "expanded" value is NO, after this will run, it will be "YES", which
// will be evaluated after our next line, which tells the table view to reload
// this specific section).
// NOTE that while our dataArray is immutable array, meaning we can't modify it,
// it points to an mutable dictionaries, so we have no problem modifying them.
-(void)updateTableView:(UIButton *)sender {
BOOL expanded = [[self.dataArray[sender.tag] objectForKey:#"expanded"] boolValue];
[self.dataArray[sender.tag] setObject:#(!expanded) forKey:#"expanded"];
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:sender.tag] withRowAnimation:UITableViewRowAnimationFade];
}
// Connect the below methods to your collapse/expand all buttons.
// What the below method actually does, is iterating thru all of the dictionaries
// in dataArray, changes their value to be corresponding to what we are trying to
// do (expand or collapse all) and then calling reloadSections:withRowAnimation:
// on our table view.
// You might wonder why I just don't call reloadData.
// Although it will do the job, I like the following more, because I personally
// feel it gives more 'smooth' update of the UI
-(void)expandAll:(UIButton *)sender {
for(int i = 0; i < [self.dataArray count]; i++) {
[self.dataArray[i] setObject:#(YES) forKey:#"expanded"];
}
[self.tableView reloadSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.dataArray.count)] withRowAnimation:UITableViewRowAnimationFade];
}
-(void)collapseAll:(UIButton *)sender {
for(int i = 0; i < [self.dataArray count]; i++) {
[self.dataArray[i] setObject:#(NO) forKey:#"expanded"];
}
[self.tableView reloadSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.dataArray.count)] withRowAnimation:UITableViewRowAnimationFade];
}
Good luck mate.

Programmatically change Size of Table

The Storyboard: http://s7.directupload.net/images/140717/z5hwmezv.png
Hey guys,
Ive got an app that recursively triggers a the same tableview controller (lets say there are files and folders in it) until you trigger a file instead of a folder. When a file is clicked, it jumps into the GLKit View Controller.
Now I want to resize the tableView programmatically, which wont work. I already got the window size, which I'm going to use for calculation the position and size of the tableView:
CGFloat screenWidth = screenRect.size.width;
CGFloat screenHeight = screenRect.size.height;
I tried different ways to change the size like the following, which dont change anything.
mainTableView.frame = CGRectMake(0, 0, screenWidth, screenHeight);
It works If I programmatically create the mainTableView, but then my segue gets deleted and I did not found any solution to create a segue programmatically.
It would be great if you can help me to find a solution that works with a storyboard tableView.
Step 1: Add delegate UITableViewDataSource,UITableViewDelegate
#interface viewController: UIViewController<UITableViewDataSource,UITableViewDelegate>
{
UITableView *tableView;
}
Step 2:
-(void)viewDidLoad
{
tableView=[[UITableView alloc]init];
tableView.frame = CGRectMake(10,30,320,400);
tableView.dataSource=self;
tableView.delegate=self;
tableView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:#"Cell"];
[tableView reloadData];
[self.view addSubview:tableView];
}
Step 3: Properties for tableview
//-- For table sections
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 1;
}
//-- For no of rows in table
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 10;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
//-- Table header height if needed
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return 50;
}
//-- Assign data to cells
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath] ;
if (cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
cell.textLabel.text=[your_array objectAtIndex:indexPath.row]; ***(or)*** cell.textLabel.text = #"Hello";
return cell;
}
//-- Operation when touch cells
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Your custom operation
}
Looking at your pic makes me think your CustomerListVC is a UITableView subclass which means the tableview is the root view and if you are using a UINavigationController based flow then you can't resize the root view readily.
What you can do is place the UITableView in a container and manipulate its constraints from the controller.
first change
class CustomerListVC : UITableViewController
to
class CustomerListVC : UIViewController
next throw away the Customer List instance in Interface Builder and drag in a new UIViewController instance instead. Xcode doesn't like you changing the base class of its stock objects.
make the new UIViewController instance a CustomerListVC type FileOwner and drag a UITableView into the content view.
Set edge constraints and add outlets to the constraints in your view controller.
From there play with the table as you see fit. e.g
-(void)squishAtBottomLeftAnimated:(BOOL)animate {
CGFloat animateTime = animate? 0.5:0;
[UIView animateWithDuration:animateTime animations:^{
self.topEdgeConstraint.constant = 400;
self.bottomEdgeConstraint.constant = 5;
self.leadingEdgeConstraint.constant = 5;
self.trailingEdgeConstraint.constant = 200;
[self.view layoutIfNeeded];
}];
}

sliding UITableView over image

The user first sees an image at the top of the page; the rest of the page is a UITableView directly below the image.
When the user slides up on the table, I'd like to slide the entire UITableView up so that it covers the image, and then start scrolling the table cells. Sliding down (once the first cell is at the top) the UITableView would then slide down to reveal the image again.
This is similar to what the Crackle app (and other apps) do. What is a good / elegant way to do this?
Change the position of TableView by setting its frame,
self.tableView.frame=self.view.frame in
-scrollViewWillBeginDragging:
There are undoubtedly several ways to accomplish this. On way involves having that image inside the first cell, which would be different than the other cells with your data. When you scroll, the position of that image in the cell would be moved down, which would make it appear that the image is being covered by the cell below. The code below shows how to do this (I modified this from another project that had alternating rows that appeared to float over fixed images).
#interface TableController ()
#property (strong,nonatomic) NSArray *theData;
#end
#implementation TableController
- (void)viewDidLoad {
[super viewDidLoad];
self.theData = #[#"One",#"Two",#"Three",#"Four",#"Five",#"Six",#"Seven",#"Eight",#"Nine",#"Black",#"Brown",#"Red",#"Orange",#"Yellow",#"Green",#"Blue"];
}
-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
RDCell *topCell = (RDCell *)[self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
[topCell updateImageViewWithOffset:scrollView.contentOffset.y];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.theData.count + 1;
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat height = (indexPath.row == 0)? 140 : 44;
return height;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row == 0) {
RDCell *cell = [tableView dequeueReusableCellWithIdentifier:#"ImageCell" forIndexPath:indexPath];
return cell;
}else{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
cell.textLabel.text = self.theData[indexPath.row - 1];
return cell;
}
}
The code in the custom cell was just this one method,
-(void)updateImageViewWithOffset:(CGFloat) offset {
CGFloat cellY = self.frame.origin.y;
self.topCon.constant = offset - cellY;
}
topCon is an IBOutlet to an NSLayoutConstraint, between the top of the cell and the top of the image view in the cell -- it's important the the image view have constraints to the top and sides of the cell, and a height constraint (equal to the height of the cell), but no constraint to the bottom of the cell. If you want to see if this approach gives you the look you want, you can download the sample project from here, http://jmp.sh/zOVz6Ev.

iOS 7 UITableView default separator gets weird after reordering

I have a problem with the default separator on UITableView in iOS 7.
When used as default the first and last separators have no insets, others are a bit inset. The original situation can be seen below:
Everything is ok. The first and last separators spread through the entire width of the table while the others are a bit smaller. Now I have the table view set to editing and I allow the user to reorder cells. And when the user does so the separators get messed up and do not appear correctly. The situation can be seen on the images below:
Do I really need to reload the data in order to fix this issue or is it an iOS 7 bug or am I doing something wrong?
How to fix this?
EDIT
Added some info about my implementation. I return NO on - (BOOL)tableView:(UITableView *)tableView shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath *)indexPath and UITableViewCellEditingStyleNone on - (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath. My - (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath is:
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell)
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
cell.shouldIndentWhileEditing = NO;
cell.textLabel.font = [UIFont someFont];
UIColor *color = [UIColor randomColor];
cell.textLabel.text = #"Some text";
CGRect rect = CGRectMake(0, 0, 15, 15);
UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0);
CGContextRef ctx = UIGraphicsGetCurrentContext();
[color set];
CGContextFillEllipseInRect(ctx, rect);
cell.imageView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return cell;
}
and
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
{
if (sourceIndexPath.row == destinationIndexPath.row) return;
NSString *tmp = [itemOrder objectAtIndex:sourceIndexPath.row];
[itemOrder removeObjectAtIndex:sourceIndexPath.row];
[itemOrder insertObject:tmp atIndex:destinationIndexPath.row];
didReorder = YES;
}
This seems to be a problem in the iOS SDK. One way to solve it would be to manually apply the correct separator insets to all cells. However, I personally prefer to let the system do that.
If you reload a cell after reordering, the correct separator insets will be applied by the system. Therefore, you should simply make sure to reload the relevant cells after reordering.
Fix for user reorderings
There's a private method tableView:didEndReorderingRowAtIndexPath: that's called on your table view's delegate after the reordering (animation) has ended. In this method you should reload the cells:
- (void)tableView:(UITableView *)tableView didEndReorderingRowAtIndexPath:(NSIndexPath *)indexPath
{
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:indexPath.section]
withRowAnimation:UITableViewRowAnimationNone];
}
This fix only works if rows are only moved within a section (and not between sections).
(It would be more efficient to only reload the moved cell and the cell at the old location using the method reloadRowsAtIndexPaths:withRowAnimation:. However, you'd have to store the old location before the cell is moved. which would be more complex to implement. For small sections/simple cells reloading the whole section might be a little bit easier.)
Fix for programmatic moves
Unfortunately, the private delegate method used in the previous fix is only called after reorderings by the user, and not after programmatically moving a row (using moveRowAtIndexPath:toIndexPath:). Therefore, we'll have to use another fix for that.
Since table views internally use CALayer animations, we can use a CATransaction to get to know when the animation has ended. (You do need to add QuartzCore.framework to your project for this.) My fix looks like this:
UITableView+NicelyMoves.h
#import <UIKit/UIKit.h>
#interface UITableView (NicelyMoves)
- (void)nicelyMoveRowAtIndexPath:(NSIndexPath *)indexPath
toIndexPath:(NSIndexPath *)newIndexPath;
#end
UITableView+NicelyMoves.m
#import <QuartzCore/QuartzCore.h>
#import "UITableView+NicelyMoves.h"
#implementation UITableView (NicelyMoves)
- (void)nicelyMoveRowAtIndexPath:(NSIndexPath *)indexPath
toIndexPath:(NSIndexPath *)newIndexPath
{
[CATransaction begin];
[CATransaction setCompletionBlock:^{
// This is executed after the animations have finished
[self reloadRowsAtIndexPaths:#[indexPath, newIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
}];
[self moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];
[CATransaction commit];
}
#end
Usage
Now, instead of using moveRowAtIndexPath:toIndexPath:, you should use the custom method nicelyMoveRowAtIndexPath:toIndexPath:. For example, you could do this:
#import "UITableView+NicelyMoves.h"
#implementation YourTableViewController
- (void)moveTheRow
{
// Move row 0 to 2 in section 0
NSIndexPath *from = [NSIndexPath indexPathForRow:0 inSection:0];
NSIndexPath *to = [NSIndexPath indexPathForRow:2 inSection:0];
[self.tableView nicelyMoveRowAtIndexPath:from toIndexPath:to];
}
#end
Please try forcing inset size or setting them to zero in the viewDidLoad to ensure tableView respects them.
This is going to set your tableView separator insets to 30.
- (void)viewDidLoad {
[super viewDidLoad];
if ([self.tableView respondsToSelector:#selector(setSeparatorInset:)]) {
[self.tableView setSeparatorInset:UIEdgeInsetsMake(0, 30, 0, 0)];
}
}
You can also set separator insets only on specific cells as:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"MyCellId"];
if (indexPath.section == 0 && indexPath.row == 0) {
[cell setSeparatorInset:UIEdgeInsetsMake(0, 30, 0, 0)];
} else {
[cell setSeparatorInset:UIEdgeInsetsZero];
}
return cell;
}

Expanding and Collapsing table view cells in ios

I have a table view of custom cells and some buttons in each cell.Clicking on any of the button inside the cell will reveal another custom view below that cell.Next click on the same button will collapse the view and need this same for all cells.I tried with insertrow method on the button click but in vain.How can i do this with using only the table view delegates.
This is what i tried:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *simpleTableIdentifier = #"CustomCell_For_Dashboard";
CustomCellFor_Dashboard *customCell = (CustomCellFor_Dashboard *)[tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];
if (customCell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"CustomCellFor_Dashboard" owner:self options:nil];
customCell = [nib objectAtIndex:0];
}
[customCell.howyoulfeelBtn addTarget:self action:#selector(buttonclicked:) forControlEvents:UIControlEventTouchUpInside];
customCell.nameLabel.text = #"test";
customCell.imgView.image = [UIImage imageNamed:#"Default.png"];
// customCell.prepTimeLabel.text = [prepTime objectAtIndex:indexPath.row];
return customCell;
}
-(void)buttonclicked:(id)sender{
NSIndexPath *indexPath = [myTable indexPathForCell:sender];
[myTable beginUpdates];
NSIndexPath *insertPath = [NSIndexPath indexPathForRow:indexPath.row inSection:indexPath.section];
[myTable insertRowsAtIndexPaths:[NSArray arrayWithObject:insertPath] withRowAnimation:UITableViewRowAnimationTop];
}
can anyone help me?
I got the same task on one project with just one thing different: There were no buttons, just tapping on cell will expand or collapse it.
There are several things you should edit in your code. First, the button method code will look something like this:
- (void) collapseExpandButtonTap:(id) sender
{
UIButton* aButton = (UIButton*)sender; //It's actually a button
NSIndexPath* aPath = [self getIndexPathForCellWithButtonByMagic:aButton];
//expandedCells is a mutable set declared in your interface section or private class extensiont
if ([expandedCells containsObject:aPath])
{
[expandedCells removeObject:aPath];
}
else
{
[expandedCells addObject:aPath];
}
[myTableView beginEditing];
[myTableView endEditing]; //Yeah, that old trick to animate cell expand/collapse
}
Now the second thing is UITableViewDelegate method:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([expandedCells containsObject:indexPath])
{
return kExpandedCellHeight; //It's not necessary a constant, though
}
else
{
return kNormalCellHeigh; //Again not necessary a constant
}
}
Key thing here is to determine if your cell should be expanded/collapsed and return right height in delegate method.
Going off of what #eagle.dan.1349 said, this is how to do it on the clicking of the cell. In storyboard, you also need to set the table cell to clip subviews, otherwise the content that would be hidden will show.
.h
#property (strong, nonatomic) NSMutableArray *expandedCells;
.m
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([self.expandedCells containsObject:indexPath])
{
[self.expandedCells removeObject:indexPath];
}
else
{
[self.expandedCells addObject:indexPath];
}
[tableView beginUpdates];
[tableView endUpdates];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
CGFloat kExpandedCellHeight = 150;
CGFloat kNormalCellHeigh = 50;
if ([self.expandedCells containsObject:indexPath])
{
return kExpandedCellHeight; //It's not necessary a constant, though
}
else
{
return kNormalCellHeigh; //Again not necessary a constant
}
}
Saw this post and just wanted to give my 2 cents as my solution to this is very similar to the chosen answer (the tapping of a whole area).
Many people architect this by using just cells alone, but I believe there is a way to build this that might align better with what people are trying to achieve:
There are headers and there are cells. Headers should be tappable, and then cells underneath the headers would show or hide. This can be achieved by adding a gesture recognizer to the header, and when tapped, you just remove all of the cells underneath that header (the section), and viceversa (add cells). Of course, you have to maintain state of which headers are "open" and which headers are "closed."
This is nice for a couple of reasons:
The job of headers and cells are separated which makes code cleaner.
This method flows nicely with how table views are built (headers and cells) and, therefore, there isn't much magic - the code is simply removing or adding cells, and should be compatible with later versions of iOS.
I made a very simple library to achieve this. As long as your table view is set up with UITableView section headers and cells, all you have to do is subclass the tableview and the header.
Link: https://github.com/fuzz-productions/FZAccordionTableView
I also had a same situation and my solution was to put a button on top of the Section Title with viewForHeaderInSection method.
noOfRows defines how many rows are there in each section and button.tag keeps which button of section is pressed.
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
UIButton *btnSection = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, tableView.frame.size.width, tableView.frame.size.height)];
btnSection.tag = section;
[btnSection setTitle:[sectionArray objectAtIndex:section] forState:UIControlStateNormal];
[btnSection addTarget:self action:#selector(sectionButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
return btnSection;
}
- (void)sectionButtonTapped:(UIButton *)button {
sectionIndex = button.tag;
if (button.tag == 0) {
noOfRows = 3;
} else if (button.tag == 1) {
noOfRows = 1;
} else if (button.tag == 2) {
noOfRows = 2;
}
[self.tableView reloadData];
}
Hope this will help you..

Resources