I am trying to implement a table view that allows the user to "show more" rows to toggle the full number of rows or a smaller number of rows. I have all the data upfront when the view is loaded, so I do not need to fetch any extra data.
I am able to get it working, but the problem I am having is that my table view does not smoothly animate when expanding or collapsing. What happens is that if the "show more" action is triggered, the size of the table is updated all at once to the full height of the table with all the data in it. Then the rows will animate.
Likewise when hiding rows, the table height will shrink all at once to the end height and then the rows will animate.
Here is a picture of what is happening. It just "jumps" to the full height and then it animates the rows. But what I would like to happen would be for it to smoothly expand the height of the table unveiling the data as it smoothly expands downward. I would like the opposite, where it smoothly slides upward when pressing "show less".
The way my app is laid out is as follows:
UIScrollView
UIStackView
UIViewController
UIViewController
UITableView <-- the section I am working on here
UIView
...
Basically, I have a scrollable list of sections of different data. Some sections are tables, some are just ad-hoc views arranged with AutoLayout, others have collection views or page view controllers or other types of views.
But this section is represented by the UITableView in the previous list.
#import "MyTableView.h"
#import "MyAutosizingTableView.h"
#interface MyTableView ()
#property (strong, nonatomic) UILabel *titleLabel;
#property (strong, nonatomic) MyAutosizingTableView *tableView;
#end
#implementation MyTableView
- (instancetype)init {
return [self initWithFrame:CGRectZero];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
[self initialize];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self initialize];
}
return self;
}
- (void)initialize {
[self.titleLabel setText:#"Details"];
self.numberOfRows = 3;
[self addSubview:self.titleLabel];
[self addSubview:self.tableView];
[NSLayoutConstraint activateConstraints:#[
[self.titleLabel.leadingAnchor constraintEqualToAnchor:self.layoutMarginsGuide.leadingAnchor],
[self.titleLabel.topAnchor constraintEqualToAnchor:self.layoutMarginsGuide.topAnchor],
[self.titleLabel.trailingAnchor constraintEqualToAnchor:self.layoutMarginsGuide.trailingAnchor],
[self.tableView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[self.tableView.topAnchor constraintEqualToSystemSpacingBelowAnchor:self.titleLabel.bottomAnchor multiplier:1.0f],
[self.tableView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
[self.tableView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
[self.tableView.widthAnchor constraintEqualToAnchor:self.widthAnchor]
]];
}
- (UITableView *)tableView {
if (!self->_tableView) {
self->_tableView = [[MyAutosizingTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
self->_tableView.translatesAutoresizingMaskIntoConstraints = NO;
self->_tableView.rowHeight = UITableViewAutomaticDimension;
self->_tableView.estimatedRowHeight = UITableViewAutomaticDimension;
self->_tableView.allowsSelection = NO;
self->_tableView.scrollEnabled = NO;
self->_tableView.delegate = self;
self->_tableView.dataSource = self;
[self->_tableView registerClass:[MyTableViewCell class] forCellReuseIdentifier:#"dataCell"];
}
return self->_tableView;
}
- (UILabel *)titleLabel {
if (!self->_titleLabel) {
self->_titleLabel = [[UILabel alloc] init];
self->_titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
self->_titleLabel.numberOfLines = 0;
self->_titleLabel.userInteractionEnabled = YES;
self->_titleLabel.textAlignment = NSTextAlignmentNatural;
self->_titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
self->_titleLabel.baselineAdjustment = UIBaselineAdjustmentAlignBaselines;
self->_titleLabel.adjustsFontSizeToFitWidth = NO;
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(showMore)];
[self->_titleLabel addGestureRecognizer:tapGesture];
}
return self->_titleLabel;
}
- (void)showMore {
if (self.numberOfRows == 0) {
self.numberOfRows = 3;
} else {
self.numberOfRows = 0;
}
NSIndexSet *indexes = [NSIndexSet indexSetWithIndex:0];
[self.tableView reloadSections:indexes withRowAnimation:UITableViewRowAnimationFade];
}
- (void)setData:(NSArray<NSString *> *)data {
self->_data = data;
[self.tableView reloadData];
}
- (void)didMoveToSuperview {
//this has no effect unless the view is already in the view hierarchy
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (self.numberOfRows <= 0) {
return self.data.count;
}
return MIN(self.data.count, self.numberOfRows);
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *data = self.data[indexPath.row];
MyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"dataCell" forIndexPath:indexPath];
cell.data = data;
return cell;
}
#end
I have a UITableView subclass that I used in the previous code. The reason I have the table view subclass is to get the table to resize when the content changes. If anyone knows a better way to do this without the subclass, I would be very interested in learning that too.
#import "MyAutosizingTableView.h"
#implementation MyAutosizingTableView
- (CGSize)intrinsicContentSize {
return self.contentSize;
}
- (void)setContentSize:(CGSize)contentSize {
[super setContentSize:contentSize];
[self invalidateIntrinsicContentSize];
}
#end
Does anyone know what I can do to get smooth expansion animations? I would really like the table to expand smoothly and reveal the data that is in the new rows.
Update - a high level overview of what I am trying to do
Here I am going to try to describe what it is I am trying to accomplish and how I am going about it, so if anyone has any alternative approaches or improvements for laying this out, I would be very open to suggestions. Thank you in advance for reading this.
So I am working on a view controller that displays data about a product, imagine a car for example. The view controller has different sections responsible for displaying different data. For example, the first section displays pictures of the item and for this section, I am using a UIPageView that displays basically a carousel of product images.
Another section displays options about the current product. For example, someone might choose a different color for the car or different wheels. For this section, I am using a table view to display the list of available attributes, where each attribute is in a different section of the table. Upon pressing the section header, the table view adds a row that displays the available options for that attribute. For instance, assume the car has different colors available (red, green, blue, and yellow). Upon pressing the section header, the table view adds a row and animates that in (using the technique discussed in chapter 8 of "Programming iOS 12"). The row that is shown then contains a UICollectionView that scrolls horizontally allowing the user to choose between the colors (or wheel options or whatever attribute is being changed).
Another section displays what other customers have said about this product. For example, if someone leaves a writeup on the car, then that would go in this section. This section is has a table showing how the product fares across different criteria. Using the car example still, it might have a row for comfort and another for gas mileage and another for performance and so on. Then there is also a horizontal UICollectionView displaying the write ups about the product.
Yet another section is a list of attributes for the product. For example, with the car it might show engine size on the left and V12 on the right.
And there are other sections as well, but then to tie them all together I have a scroll view with a vertical UIStackView inside it. And this is what I am really curious about. What is the best way to display or layout all these sections? Is a stack view the way to go or should I use a table view or should I just have a scroll view with these views hooked together just using AutoLayout directly or is there some better way to do it than all of these?
One thing that is very important is that each section can be different sizes between products. For example, some sections might be one height for one product but then a completely different height for other products. So I need it to be able to dynamically size each section and I also need to be able to animate some size changes (for example, the section where it adds a row to the table and then removes it, that needs to animate and make the whole section larger in an animated fashion).
Also, I would like each section to be modular and to not end up having a giant view controller that manages everything. I would like each section to essentially be self contained. I just pass data to that section and then it handles everything related to layout and interaction. But again, the real issue is getting those views to be able to resize and to have that update in the parent view (stack view currently).
But I am really new to iOS development, and while I have been trying to learn as fast as I can, I am still not sure if the way that I am doing this currently (the scroll view with the stack view inside it) is the best approach or if there are better approaches. I tried this initially in a table view with static cells where each row was a different section. But then getting the child view controllers to resize the row in the parent table view was not working that well. Since then I have also changed from making each section be a separate view controller and just have each section be a view (rather than view controller) to make resizing hopefully easier, but I am still running into issues. I have also considered collection views, but I also need to be able to support iOS 12 (or I could drop iOS 12 support if I really need to and just support iOS 13+).
But to restate, my overall goals are to have some way to layout this interface which consists of different views that each handle different data. I would like each view to be able to resize and for that to be able to be smooth. And I would like each section of the interface to be modular so as to avoid one large view controller.
Update 2
After following the suggestion to wrap the table view in a UIView and then set the two bottom constraints with different priorities, this is what I am getting. I also get this effect anytime I try to animate things like the height constraint of views. I think this is because the overall view here is contained inside a stack view, so I think the stack view is doing something when its arranged subview changes size.
The picture here shows both expanding the table view and collapsing it. When it is collapsed it has three rows visible and when it is expanded it has five rows.
The yellow and blue views are representative of other views on the page. They are not part of this view and neither is the stack view they are contained within. They are just other parts of the page included here to show this issue. All the views here are contained in a stack view and I think that is what is causing the issue with the animation, but I am not sure how to fix it.
You really have a bunch of questions here - but to address specifically the issue with trying to expand/collapse your table view...
I think you'll be fighting a losing battle with this approach. By trying to use an "auto-sizing" table view, you're counter-acting much of a table view's behaviors/ And, since you are not getting the benefit of memory management with reusable cells, you're probably better off formatting your "spec list" with a vertical stack view.
In either case, to create a collapse / expand animation with a "reveal / hide" effect, you may want to embed the tableView or stackView in a "container" view, and then toggle constraint priorities for the height (the bottom) of the container.
Here is a quick example...
create a "container" UIView
add a vertical stack view to the container
add labels to the stack view
add a red UIView to place above the container
add a blue UIView to place below the container
define constraints for the height of the container
On each tap, the container will expand / collapse to reveal / hide the labels.
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController
#end
#interface ViewController ()
#property (strong, nonatomic) NSLayoutConstraint *collapsedConstraint;
#property (strong, nonatomic) NSLayoutConstraint *expandedConstraint;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIStackView *labelsStackView = [UIStackView new];
labelsStackView.axis = UILayoutConstraintAxisVertical;
labelsStackView.spacing = 4;
// let's add 12 labels to the stack view (12 "rows")
for (int i = 1; i < 12; i++) {
UILabel *v = [UILabel new];
v.text = [NSString stringWithFormat:#"Label %d", i];
[labelsStackView addArrangedSubview:v];
}
// add a view to show above the stack view
UIView *redView = [UIView new];
redView.backgroundColor = [UIColor redColor];
// add a view to show below the stack view
UIView *blueView = [UIView new];
blueView.backgroundColor = [UIColor blueColor];
// add a "container" view for the stack view
UIView *cView = [UIView new];
// clip the container's subviews
cView.clipsToBounds = YES;
for (UIView *v in #[redView, cView, blueView]) {
v.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:v];
}
// add the stackView to the container
labelsStackView.translatesAutoresizingMaskIntoConstraints = NO;
[cView addSubview:labelsStackView];
// constraints
UILayoutGuide *g = [self.view safeAreaLayoutGuide];
// when expanded, we'll use the full height of the stack view
_expandedConstraint = [cView.bottomAnchor constraintEqualToAnchor:labelsStackView.bottomAnchor];
// when collapsed, we'll use the bottom of the 3rd label in the stack view
UILabel *v = labelsStackView.arrangedSubviews[2];
_collapsedConstraint = [cView.bottomAnchor constraintEqualToAnchor:v.bottomAnchor];
// start collapsed
_expandedConstraint.priority = UILayoutPriorityDefaultLow;
_collapsedConstraint.priority = UILayoutPriorityDefaultHigh;
[NSLayoutConstraint activateConstraints:#[
// redView Top / Leading / Trailing / Height=120
[redView.topAnchor constraintEqualToAnchor:g.topAnchor constant:20.0],
[redView.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:20.0],
[redView.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-20.0],
[redView.heightAnchor constraintEqualToConstant:120.0],
// container Top==redView.bottom / Leading / Trailing / no height
[cView.topAnchor constraintEqualToAnchor:redView.bottomAnchor constant:0.0],
[cView.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:20.0],
[cView.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-20.0],
// blueView Top==stackView.bottom / Leading / Trailing / Height=160
[blueView.topAnchor constraintEqualToAnchor:cView.bottomAnchor constant:0.0],
[blueView.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:20.0],
[blueView.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-20.0],
[blueView.heightAnchor constraintEqualToConstant:160.0],
// stackView Top / Leading / Trailing
[labelsStackView.topAnchor constraintEqualToAnchor:cView.topAnchor],
[labelsStackView.leadingAnchor constraintEqualToAnchor:cView.leadingAnchor],
[labelsStackView.trailingAnchor constraintEqualToAnchor:cView.trailingAnchor],
_expandedConstraint,
_collapsedConstraint,
]];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// toggle priority between expanded / collapsed constraints
if (_expandedConstraint.priority == UILayoutPriorityDefaultHigh) {
_expandedConstraint.priority = UILayoutPriorityDefaultLow;
_collapsedConstraint.priority = UILayoutPriorityDefaultHigh;
} else {
_collapsedConstraint.priority = UILayoutPriorityDefaultLow;
_expandedConstraint.priority = UILayoutPriorityDefaultHigh;
}
// animate the change
[UIView animateWithDuration:0.5 animations:^{
[self.view layoutIfNeeded];
}];
}
#end
You may be able to implement that with your "auto-sizing" table view, but again, since you're not using the "normal" table view behaviors (scrolling, reusable cells, etc), this might be a better approach.
Hopefully, it can also give you some insight to sizing other views in your layout - particularly if you want to animate them in or out of view.
Edit - added a more complex example here: https://github.com/DonMag/Scratch2021
It uses a vertical stack view in a vertical scroll view as the "main view" UI, adding child view controllers as "components" in the stack view.
Related
Using Storyboards and Autolayout, I have a UIViewController with a UIScrollView as the main view. I have several container views embedded in the scroll view. Some of those embedded container views contain UITableViews, each having cells of different heights. I'll need the tableView's height to be large enough to show all cells at once, as scrolling will be disabled on the tableView.
In the main UIViewController, container view's height has to be defined in order for the scroll view to work properly. This is problematic because there's no way for me to know how large my tableView will be once all it's cells of varying heights are finished rendering. How can I adjust my container view's height at runtime to fit my non-scrolling UITableView?
So far, I've done the following:
// in embedded UITableViewController
//
- (void)viewDidLoad {
// force layout early so I can determine my table's height
[self.tableView layoutIfNeeded];
if (self.detailsDelegate) {
[self.detailsTableDelegate didDetermineHeightForDetailsTableView:self.tableView];
}
}
// in my main UIViewController
// I have an IBOutlet to a height constraint set up on my container view
// this initial height constraint is just temporary, and will be overridden
// once this delegate method is called
- (void)didDetermineHeightForDetailsTableView:(UITableView *)tableView
{
self.detailsContainerHeightConstraint.constant = tableView.contentSize.height;
}
This is working fine and I was pleased with the results. However, I have one or two more container views to add, which will have non-scrolling tableViews, and I'd hate to have to create a new delegate protocol for each container view. I don't think I can make the protocol I have generic.
Any ideas?
Here's what I ended up doing:
// In my embedded UITableViewController:
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 60.0;
// via storyboards, this viewController has been embeded in a containerView, which is
// in a scrollView, which demands a height constraint. some rows from our static tableView
// might not display (for lack of data), so we need to send our table's height. we'll force
// layout early so we can get our size, and then pass it up to our delegate so it can set
// the containerView's heightConstraint.
[self.tableView layoutIfNeeded];
self.sizeForEmbeddingInContainerView = self.tableView.contentSize;
}
// in another embedded view controller:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.sizeForEmbeddingInContainerView = self.tableView.contentSize;
}
// then, in the parent view controller, I do this:
// 1) ensure each container view in the storyboard has an outlet to a height constraint
// 2) add this:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.placeDetailsContainerHeightConstraint.constant = self.placeDetailsTableViewController.sizeForEmbeddingInContainerView.height;
self.secondaryExperiencesContainerHeightConstraint.constant = self.secondaryExperiencesViewController.sizeForEmbeddingInContainerView.height;
}
I haven't done this yet, but it'd probably be best to create a Protocol with a property of CGSize sizeForEmbeddingInContainerView that each child view controller can adopt.
Here's what worked for me perfectly.
- (void)updateSizeBasedOnChildViews {
// Set height of container to match embedded tableview
CGRect containerFrame = self.cardTableContainer.frame;
containerFrame.size.height = [[[self.cardTableContainer subviews] lastObject]contentSize].height;
self.cardTableContainer.frame = containerFrame;
// Set content height of scrollview according to container
CGRect scrollFrame = self.cardTabScrollView.frame;
scrollFrame.size.height = containerFrame.origin.y + containerFrame.size.height;
// + height of any other subviews below the container
self.cardTabScrollView.contentSize = scrollFrame.size;
}
I am trying to make a view controller which deals with the user login. Since I needed the view controller to be scrollable, contain a separate view (for the login), and contain a background, I decided to go with the route of making a tableviewcontroller, subclassing it, and than adding in the necessary views. I subclassed UITableViewController and added this code into the viewdidload()
UIImageView *tempImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"TableViewControllerBlurred.png"]];
[tempImageView setFrame:self.tableView.frame];
self.tableView.backgroundView = tempImageView;
[tempImageView release];
This successfully added my background image to the controller and at this point, the view controller looked like: http://imgur.com/ST4H8uf as it was supposed to.
Moving on, I began working with static cells, dropped in a view into one of the cells and began to design the sign in screen. At this point, my storyboard looked like: http://imgur.com/n6GKeGq&ST4H8uf but the problem comes about when I run the project.
When I run the project, I keep getting the same background screen as seen in the first image without any of the new static cells or views. All and any help is much appreciated as to what may be the cause of this problem. Thank you.
CellForRowAtIndexPath Code:
*/
(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:<##"reuseIdentifier"#> forIndexPath:indexPath];
// Configure the cell...
return cell;
}
*/
If what you want is a UITableView with just static cells, then learn to use UIScrollView with a UIViewController.
#interface vc : UIViewController
#property (nonatomic, strong) UIScrollView *scrollView;
#end
#implementation vc
- (id)init // or whatever initializer you are using to make your view controller
{
self = [super init];
if (self) {
_scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0,0,320,568)];
[_scrollView setContentSize:CGSizeMake(320,568)]; // equals one screen
[_scrollView setContentSize:CGSizeMake(320,568*2)]; // equals two screens, etc
// contentSize property determines how much you can scroll inside the UIScrollView view if that makes any sense to you.
[self.view addSubview:_scrollView]
// one way of adding a background
UIImageView *backgroundImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"imageName"]];
[self.view addSubview:backgroundImageView];
[_scrollView addSubview:[self newStaticCellAtPosition:CGRectMake(0,0,320,45)]];
[_scrollView addSubview:[self newStaticCellAtPosition:CGRectMake(0,45,320,45)]];
// add subviews, you can even use UITableViewCell if you want.
// I'd use simple UIView's and draw separators and whatnot myself if I were you.
}
return self;
}
- (UIView *)newStaticCellAtPosition:(CGRect)position
{
UIView *staticCell = [[UIView alloc] initWithFrame:position];
[staticCell setBackgroundColor:[UIColor redColor]];
return staticCell;
}
#end
For other properties you should check out UIScrollView documentation. Remember UITableView inherits from UIScrollView so if it's easy to pick and choose what you want.
first check datasource and delegate of tableview has to be set.
You might be geeting a problem beacuse of that.
Never use a UITableViewController! In almost every case I have come across it is much much easier to use a UIViewController and add a table view. You simply cannot get at the backgroundView of a UITableViewController and have it scroll properly. I realize that you can only make a "static" table view with a UITableViewController but its simple enough to mimic the exact same behaviour with a regular table view and you don't have to deal with the headache of not being able to add views behind the table (like a background image).
This question should not be mixed up with this here.. These are two different things.
There is a good example how to use a UITableView Header on SO.
This all works fine and the main header is fixed on top as long as the style is set to plain.
But if I use sections, the main header no longer sticks to top and moves away while scrolling to the bottom.
In this method, I am returning the header for each section.
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
In this method I am setting the height for the header section above:
- (CGFloat) tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
In this method, I am setting the real table header.
- (void)viewWillAppear:(BOOL)animated {
...
self.recordTableView.tableHeaderView = headerView;
}
Is it even possible having a fixed table header, while using sections?
What is an alternative solution to this please?
If you want a UITableViewController (static cells/keyboard handling) and have a fixed header then you should use Containment. You can do this from a Storyboard by setting up a UIViewController with your fixed header and then using a Container View to embed the UITableViewController.
Once you have your containing view setup, you right-click drag from the Container View to the View Controller you want to embed - the UITableViewController in this case.
You can access and get a reference to the contained View Controller (the UITableViewController) from the Container View Controller by implementing the prepareForSegue:sender: method.
There’s no way to maintain the header of a tableView fixed, but
an useful approach when you need a unique header, is to use a UIViewController rather than a UITableViewController, and set the header (UIView) out from the tableView.
Something like this:
If you want to keep the class as a UITableViewController you can add your header as a subview to the tableview's superview. You will have to also push the tableview top inset down so your headerview doesnt hide the table.
Here is a sample code to put inside your tableViewController subclass (This example assumes your tableview controller is inside a navigation controller, so it pushes the view to below the navigation bar):
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
self.automaticallyAdjustsScrollViewInsets = NO;
}
-(void)addHeaderView{
CGFloat yPosition = self.navigationController.navigationBar.frame.origin.y + self.navigationController.navigationBar.frame.size.height;
mainHeaderView = [[UIView alloc] init];
const CGFloat mainHeaderHeight = 44;
[mainHeaderView setFrame:CGRectMake(0, yPosition, self.view.frame.size.width, mainHeaderHeight)];
mainHeaderView.backgroundColor = [UIColor redColor];
[self.tableView.superview addSubview:mainHeaderView];
[self.tableView setContentInset:UIEdgeInsetsMake(yPosition + mainHeaderHeight, self.tableView.contentInset.left, self.tableView.contentInset.bottom, self.tableView.contentInset.right)];
}
I haven't done this, but the first thing I would think to try is to place my tableview in a UIView and make my own header there in that UIView. Seems a trivial matter to make that view appear to be the header of the table and it would certainly stay put.
I've been reading online tutorials on UICollectionView with different layouts. Also looked at a lot of SO Questions on the subject. But it seems what I am looking might be something more simple but I am stuck on how to go forward.
The Goal
I have a UIViewController that is embedded in a UINavigation controller. I am displaying data in a UITableView which includes:1 UIImageView and three UILabels in each cell. The data is fetched from a server and all works nicely.
I then wanted to have a UIButton that, when tapped, would kick off a cool animation that shows the cells transition into a nice grid view.
It suddenly dawned on me that I needed to use a UICollectionView to change between these two cells and ditch the UITableView completely. Tapping the button again, would switch back to the last state (Grid or UITableView style)
The grid cell needs to loose one label - but keep the image.
The problem
I have spent the last two days reading up on UICollectionView and UICollectionViewFlowLayout. I think I could use a Apple's pre-made UICollectionViewFlowLayout and just tweak it a little.
I don't know if I need two custom cells or one cell that changes shape between the two views and how the animations must work.
I'm not looking for the exact code to do this - I just need to know which direction I need to go in and if I need to use two custom cells - and how do I change between the two with animation and not reloading all the data again.
Appreciate any input.
Thanks all.
I finally found a solution that was acceptable to my need. If anyone ever has similar needs - this is how you use two different custom UICollectionViewCell's and how to change between the two different cells / layouts.
First thing is create the customCells in IB - creating the xib
files.
Then set the up as you need
Since my requirement needed the standard flow layout provided by the class UICollectionViewFlowLayout - I just needed to create two custom layouts and tweak them to my needs.
Create two (or more if needed) classes that subclass UICollectionViewFlowLayout
In the implementation - setup the layout as needed. Since I am subclassing the pre-made UICollectionViewFlowLayOut and all I need to do is tweak it - the implementation is pretty simple.
So - for the table view layout I did this:
tableViewFlowLayOut.m
-(id)init
{
self = [super init];
if (self){
self.itemSize = CGSizeMake(320, 80);
self.minimumLineSpacing = 0.1f;
}
return self;
}
This sets each cells width and height to the values I needed. self.minimumLineSpacing sets the spacing between the cells. (Spacing between the cell above / below )
Then for the grid layout:
gridFlowLayOut.m
-(id)init
{
self = [super init];
if (self){
self.itemSize = CGSizeMake(159, 200);
self.minimumInteritemSpacing = 0.1f;
self.minimumLineSpacing = 0.1f;
}
return self;
}
Same as before - however, this time I needed spacing between my cells right edge -
self.minimumInteritemSpacing = 0.1f'
takes care of that.
Right - now to put it all together - in the viewController that has the UICollectionView
viewController.m
// Import the new layouts needed.
#import "GridFlowLayOut.h"
#import "TableViewFlowLayOut.m"
//Create the properties
#property (strong, nonatomic) TableViewFlowLayOut *tableViewLayout;
#property (strong, nonatomic) GridFlowLayOut *grideLayout;
-(void)viewDidLow
{
//Register the two custom collection view cells you created earlier. Make sure you set the correct reuse identifier here.
[self.tradeFeedCollectionView registerNib:[UINib nibWithNibName:#"TableViewCell" bundle:nil] forCellWithReuseIdentifier:#"TableItemCell"];
[self.tradeFeedCollectionView registerNib:[UINib nibWithNibName:#"GridViewCell" bundle:nil] forCellWithReuseIdentifier:#"GridItemCell"];
}
-(void)viewWillAppear
{
//Create the layout objects
self.grideLayout = [[GridFlowLayOut alloc]init];
self.tableViewLayout = [[TableViewFlowLayOut alloc]init];
//Set the first layout to what it should be
[self.tradeFeedCollectionView setCollectionViewLayout:self.tableViewLayout];
}
Right - now to change between the layouts with some animation. This is actually very easy to do and only needs a few lines of code -
I called this code in a button method in viewController.m
-(void)changeViewLayoutButtonPressed
{
//BOOl value to switch between layouts
self.changeLayout = !self.changeLayout;
if (self.changeLayout){
[self.tradeFeedCollectionView setCollectionViewLayout:self.grideLayout animated:YES];
}
else {
[self.tradeFeedCollectionView setCollectionViewLayout:self.tableViewLayout animated:YES];
}
}
And lastly in cellForItemAtIndexPath
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{ static NSString *tableCellIdentifier = #"TableItemCell";
static NSString *gridCellIdentifier = #"GridItemCell";
//BOOL used to detect which layout is active
if (self.gridLayoutActive == NO){
CustomCollectionCellClass *tableItemCell = [collectionView dequeueReusableCellWithReuseIdentifier:tableCellIdentifier forIndexPath:indexPath];
//Setup the cell
}
return tableItemCell;
}else
{
CustomCollectionCellClass *gridItemCell= [collectionView dequeueReusableCellWithReuseIdentifier:gridCellIdentifier forIndexPath:indexPath];
//Setup the cell
}
return gridItemCell;
}
return nil;
}
Of course you will need to conform to the other UICollectionView delegates and setup the remaining stuff.
This actually took me a while to figure out. I really hope it helps others out there.
If anyone wants a demo project - I'll happily create one and upload to GitHub.
For anyone new to UICollectionViews I highly recommend reading Apple's programming guide on the subject - it was this document which lead me to this solution.
Reference:
https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/Introduction/Introduction.html
Context:
I have already done the following
1. Watched the relevant videos for UICollectionView from WWDC 2012. Unfortunately they do not discuss much about the decoration views.
2. Implemented a collection view with decoration view. I subclassed the UICollectionViewFlowLayout to include the decoration view.
3. I have read the UICollectionView programming guide from Apple.
4. I understand that the Decoration views are controlled solely by the Layout object.
Problem:
1. When I analyze the code with Instruments I find that my decoration views are causing a memory leak.
2. On further analysis of the code I found that the decoration view is not being reused as I would expect. Every time the decoration view is needed a new one is created.
3. When the collection view deallocs only the last created decoration view gets dellocated and all other decorations views leak.
4. I do not understand how the decoration view will be reused when there is no deque method for it.
Questions:
1. Do we have to manually manage the removal and addition of decoration views from the collection view?
I am confused as to how is the decoration view works in general. Any pointers for this would help. The code for my layout object is as below. In the code I am simply putting a 10 points wide bar at the top of my collection view. Everytime the bar is scrolled off the screen and then brought back into the screen, a new decoration view is allocated. When the collection view is deallocated only the last allocated decoration view gets deallocated. Rest all leak.
#import "CollectionViewFlowLayoutSubclass.h"
#import "CollectionViewDecorationView.h"
#define DECORATION_VIEW_KIND #"DecorationViewShelf"
#implementation CollectionViewFlowLayoutSubclass
- (void)awakeFromNib
{
[super awakeFromNib];
[self registerClass:[CollectionViewDecorationView class] forDecorationViewOfKind:DECORATION_VIEW_KIND];
}
- (void)prepareLayout
{
[super prepareLayout];
}
- (CGSize)collectionViewContentSize
{
return [super collectionViewContentSize];
}
- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray* larrayAttributes = [super layoutAttributesForElementsInRect:rect];
NSMutableArray* larrayMutableAttributes = [larrayAttributes mutableCopy];
CGRect lrectFrame = CGRectMake(0, 0, 320, 10);
if (CGRectIntersectsRect(rect, lrectFrame))
{
UICollectionViewLayoutAttributes* lobjLayoutAttributes =
[UICollectionViewLayoutAttributes layoutAttributesForDecorationViewOfKind:DECORATION_VIEW_KIND
withIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
lobjLayoutAttributes.frame = CGRectMake(0, 0, 320, 10);
[larrayMutableAttributes addObject:lobjLayoutAttributes];
}
else
{
NSLog(#"Rect %# does not intersect %#", NSStringFromCGRect(rect), NSStringFromCGRect(lrectFrame));
}
return [NSArray arrayWithArray:larrayMutableAttributes];
}
#end