I am trying to create custom UICollectionViewCell which contains few properties, and depending on values of those properties drawing components inside cell. I am using dequeueReusableCellWithReuseIdentifier for creating cell, then I am setting some properties, and at the end calling layoutIfNeeded function which is overridden inside my custom cell. Overridden function is setting some properties of cell also for example BOOL property is set to YES, and after refreshing cell (calling reloadData on collection view) function layoutIfNeeded is called again. When I try to read my BOOL property which is set to YES, i am always getting default value which is NO for the first time i call reloadData. When I call reloadData second time, property is set to YES. Any idea what am I doing wrong? Here is code I am using:
on button click I am calling:
[myCollectionView reloadData];
method cellForItemAtIndexPath looks like:
MyCustomCollectionCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"myCustomCell" forIndexPath: indexPath];
cell.device = [collectionArray objectAtIndex:indexPath.row];
[cell layoutIfNeeded];
return cell;
And code of layoutIfNeeded inside MyCustomCollectionCell.m
-(void)layoutIfNeeded{
NSLog(#"bool prop: %d",changedStatus);
changedStatus = YES;
}
BOOL property is defined in MyCustomCollectionCell.h :
#property (nonatomic, assign)BOOL changedStatus;
UPDATE:
I am sorry, I made a mistake in my post. I am not refreshing collection with reloadData, but with reloadItemsAtIndexPaths; This call causes init method of my custom cell to be called again (not just when collection view is loaded for the first time) and after that layoutIfNeeded. I thing problem is that cell is not reused, but created again, causing all properties to disappear. Any idea how to fix this?
You can't use cells to store state data. Cells get used, put in the reuse queue, and then recycled. The specific cell object that stores the data for a particular indexPath may change when the table is reloaded, when a cell is reloaded, when you scroll to expose new cells, etc.
Save state data in your data model.
What is the purpose of changedStatus property ?
Try setting changedStatus = YES; in layoutSubviews instead :
- (void)layoutSubviews
{
[super layoutSubviews];
self.changedStatus = YES;
}
Set this changedStatus Bool inside your ViewController instead of UICollectionViewCell.
BOOL property is defined in YourViewController.h :
#property (nonatomic, assign)BOOL changedStatus;
When you you want to refresh the CollectionView
[myCollectionView reloadData];
changedStatus = YES;
Then inside your MyCustomCollectionCell.m create a new method like,
-(void)customLayoutIfNeeded : (BOOL)status{
NSLog(#"bool prop: %d",changedStatus);
changedStatus = YES;
}
Add this to MyCustomCollectionCell.h as well.
Then inside cellForItemAtIndexPath do this,
MyCustomCollectionCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"myCustomCell" forIndexPath: indexPath];
cell.device = [collectionArray objectAtIndex:indexPath.row];
[cell customLayoutIfNeeded: changedStatus];
return cell;
Related
I won't go into the WHY on this one, I'll just explain what I need.
I have a property in my implementatioon file like so...
#property (nonatomic, strong) MyCustomCell *customCell;
I need to initialize this on viewDidLoad. I return it as a row in cellForRowAtIndexPath always for the same indexPath, by returning "self.customCell".
However it doesn't work, I don't appear to be allocating and initializing the custom cell correctly in viewDidLoad. How can I do this? If you know the answer, save yourself time and don't read any further.
Ok, I'll put the why here in case people are curious.
I had a table view which was 2 sections. The first section would always show 1 row (Call it A). The second section always shows 3 rows (Call them X, Y, Z).
If you tap on row A (which is S0R0[Section 0 Row]), a new cell would appear in S0R1. A cell with a UIPickerView. The user could pick data from the UIPickerView and this would update the chosen value in cell A.
Cell X, Y & Z all worked the same way. Each could be tapped, and they would open up another cell underneath with their own respective UIPickerView.
The way I WAS doing this, was all these cell's were created in the Storyboard in the TableView, and then dragged out of the View. IBOutlets were created for all. Then in cellForRAIP, I would just returned self.myCustomCellA, or self.myCustomCellY.
Now, this worked great, but I need something more custom than this. I need to use a custom cell, and not just a UITableViewCell. So instead of using IBOutlets for all the cells (8 cells, 4 (A,X,Y,Z) and their 4 hidden/toggle-able UIPickerView Cell's), I created #properties for them in my implementation file. But it's not working like it did before with the standard UITableViewCell.
I'm not sure I'm initializing them correctly? How can I properly initialize them in/off viewDidLoad so I can use them?
.m
#interface MyViewController ()
#property (strong, nonatomic) MyCustomCell *myCustomCellA;
...
viewDidLoad
...
self.myCustomCellA = [[MyCustomCell alloc] init];
...
cellForRowAtIndexPath
...
return self.myCustomCellA;
...
If only I understood your question correctly, you have 3 options:
I would try really hard to implement table view data source with regular dynamic cells lifecycle in code and not statically – this approach usually pays off when you inevitably want to modify your business logic.
If you are certain static table view is enough, you can mix this method with overriding data source / delegate methods in your subclass of table view controller to add minor customisation (e.g. hiding certain cell when needed)
Alternatively, you can create cells using designated initialiser initWithStyle:reuseIdentifier: to instantiate them outside of table view life cycle and implement completely custom logic. There is nothing particular that you should do in viewDidLoad, that you wouldn't do elsewhere.
If you have a particular problem with your code, please post a snippet so community can help you
I suggest you to declare all your cells in storyboard (with date picker at right position) as static table and then override tableView:heightForRowAtIndexPath:
Define BOOL for determine picker visibility and its position in table
#define DATE_PICKER_INDEXPATH [NSIndexPath indexPathForRow:1 inSection:0]
#interface YourViewController ()
#property (assign, nonatomic) BOOL isPickerVisible;
#end
Then setup initial value
- (void)viewDidLoad {
[super viewDidLoad];
self.isPickerVisible = YES;
}
Override tableView delegate method
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if ([indexPath isEqual:DATE_PICKER_INDEXPATH] && !self.isPickerVisible) {
return 0;
} else {
return [super tableView:tableView heightForRowAtIndexPath:indexPath];
}
}
And finally create method for toggling picker
- (void)togglePicker:(id)sender {
self.isPickerVisible = !self.isPickerVisible;
[self.tableView beginUpdates];
[self.tableView endUpdates];
}
which you can call in tableView:didSelectRowAtIndexPath:
According to your problem, you can create pairs (NSDictionary) of index path and bool if its visible and show/hide them according to that.
Here's what I was looking for:
MyCustomCell *cell = (MyCustomCell *)[[[UINib nibWithNibName:#"MyNibName" bundle:nil] instantiateWithOwner:self options:nil] firstObject];
I have a simple UICollectionView up and running and have it so you can select a cell then save your selection. But guess what? I have a problem!
After scrolling the selected cell off-screen and back on, it looses its selection. Pretty simple at the surface. How can I stop this?
Is this something to do with cell reuse? Here is my code:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"AircraftConfigurationCell";
SLAircraftConfigurationCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
//Customise cell...
return cell;
}
Do you have a datasource which is formed possibly from an array containing data models?
If so then you're off to a great start and can sort out this selection intermittent behaviour in no time!
In your data model .h file create a property like so property (nonatomic, retain) BOOL selectedState; and in the .m file initialise the selectedState to FALSE in your data object's init method.
Once that's done in your UICollectionView's didSelect method, set the dataObject's selectedState to YES that is associated to that cell/item index.
Then finally in your cellForItemAtIndexPath method, ensure you do an if check on the dataModel instance that youre going to set the cellItem with for the selectedState property. If its set to `NO' set the selectionStyle to none, otherwise set it to yes.
I used to have your problem before where selection would be lost when scrolling, but now its being managed and the result is persistent.
I'm swapping out the data being displayed in my collection view by changing the datasource. This is being done as part of a tab-like interface. When the new data loads, I would like to flash the scroll indicators to tell the user that there's more data outside of the viewport.
Immediately
Doing so immediately doesn't work because the collection view hasn't loaded the data yet:
collectionView.dataSource = dataSource2;
[collectionView flashScrollIndicators]; // dataSource2 isn't loaded yet
dispatch_async
Dispatching the flashScrollIndicators call later doesn't work either:
collectionView.dataSource = dataSource2;
dispatch_async(dispatch_get_main_queue(), ^{
[collectionView flashScrollIndicators]; // dataSource2 still isn't loaded
});
performSelector:withObject:afterDelay:
Executing the flashScrollIndicators after a timed delay does work (I saw it somewhere else on SO), but leads to a bit of lag with the scroll indicators being shown. I could decrease the delay, but it seems like it'll just leads to a race condition:
collectionView.dataSource = dataSource2;
[collectionView performSelector:#selector(flashScrollIndicators) withObject:nil afterDelay:0.5];
Is there a callback that I can hook on to to flash the scroll indicators as soon as the collection view has picked up on the new data and resized the content view?
Subclassing UICollectionView and overriding layoutSubviews can be a solution. You can call [self flashScrollIndicators] on the collection. Problem is that layoutSubviews gets called in multiple scenarios.
Initially when collection is created and datasource is assigned.
On scrolling, cells which go beyond the viewport get re-used & re-layout.
Explicitly change frame/reload the collection.
Workaround to this can be, keeping a BOOL property which will be made YES only when reloading datasource, otherwise will remain NO. Thus flashing of scroll bars will happen explicitly only when reloading collection.
In terms of source code,
MyCollection.h
#import <UIKit/UIKit.h>
#interface MyCollection : UICollectionView
#property (nonatomic,assign) BOOL reloadFlag;
#end
MyCollection.m
#import "MyCollection.h"
#implementation MyCollection
- (void) layoutSubviews {
[super layoutSubviews];
if(_reloadFlag) {
[self flashScrollIndicators];
_reloadFlag=NO;
}
}
Usage should be
self.collection.reloadFlag = YES;
self.collection.dataSource = self;
Put your call to flashScrollIndicators inside UICollectionViewLayout's method -finalizeCollectionViewUpdates.
From Apple's documentation:
"... This method is called within the animation block used to perform all of the insertion, deletion, and move animations so you can create additional animations using this method as needed. Otherwise, you can use it to perform any last minute tasks associated with managing your layout object’s state information."
Hope this helps!
Edit:
Ok, I got it. Since you mentioned the finalizeCollectionViewUpdates method was not being called I decided to try it myself. And you're right. The problem is (sorry I didn't notice this earlier) that method is only called after you update the Collection View (insert, delete, move a cell, for example). So in this case it doesn't work for you. So, I have a new solution; it involves using UICollectionView's method indexPathsForVisibleItems inside UICollectionViewDataSource's method collectionView:cellForItemAtIndexPath:
Every time you hand a new UICollectionViewCell to your collection view, check if it is the last of the visible cells by using [[self.collectionView indexPathsForVisibleItems] lastObject]. You will also need a BOOL ivar to decide if you should flash the indicators. Every time you change your dataSource set the flag to YES.
- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [cv dequeueReusableCellWithReuseIdentifier:#"MyCell" forIndexPath:indexPath];
cell.backgroundColor = [UIColor whiteColor];
NSIndexPath *iP = [[self.collectionView indexPathsForVisibleItems] lastObject];
if (iP.section == indexPath.section && iP.row == indexPath.row && self.flashScrollIndicators) {
self.flashScrollIndicators = NO;
[self.collectionView flashScrollIndicators];
}
return cell;
}
I tried this approach and it's working for me.
Hope it helps!
I want to alter the font size and color etc. for my UITableView cells. I've designed the cells custom in Xcode and got everything working.
First of I'll post my code here:
UITableViewController:
- (void)viewDidLoad
{
[super viewDidLoad];
[self.tableView registerClass:MainCategoryTableViewCell.class forCellReuseIdentifier:#"MainCategoryCell"];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"MainCategoryCell";
MainCategoryTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
return cell;
}
And my custom cell:
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.title.font = [Theme tableCellTitleFont];
self.title.textColor = [Theme tableCellTitleColor];
self.subcategories.font = [Theme tableCellSubTitleFont];
self.subcategories.textColor = [Theme tableCellSubTitleColor];
self.costs.font = [Theme tableCellValueFont];
self.costs.textColor = [Theme tableCellValueColor];
}
return self;
}
I'm confused now how this dequeue works:
As far as I understood if I register the class in the viewDidLoad, the initWithStyle method of the cell gets ONLY called, when theres no cell for reuse. If theres a cell for reuse it will be used. I've seen a lot of if(cell == nil) calls in other code snippets but is that really necessary? I thought the registerClass method takes care of that anyway?
And at the moment my cells will be displayed completely empty. Before I registered the class everything worked, however the initWithStyle didn't get called..
Complete cellForRowAtIndexPathMethod:
#pragma mark Delegate methods
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"MainCategoryCell";
MainCategoryTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Configure the cell...
MainCategory *mainCategory = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.title.text = mainCategory.name;
cell.subcategories.text = [NSString stringWithFormat:#"%i subcategories", [[mainCategory getNumberOfSpendingCategories] integerValue]];
cell.costs.text = [[mainCategory getMonthlyCostsOfAllSpendingCategories] getLocalizedCurrencyString];
if(!mainCategory.icon){
cell.icon.image = [UIImage imageNamed:#"DefaultIcon.png"];
} else {
cell.icon.image = [UIImage imageNamed:mainCategory.icon];
}
if(!mainCategory.color){
cell.backgroundColor = [PresetColor colorForPresetColor:PresetColorsWhite];
} else {
cell.backgroundColor = [PresetColor colorForPresetColor:(PresetColors)[mainCategory.color intValue]];
}
cell.cellBackground.image = [[UIImage imageNamed:#"content-bkg"] resizableImageWithCapInsets:UIEdgeInsetsMake(10, 10, 10, 10)];
return cell;
}
If you have defined the cell as "prototype cell" for the table view in the xib/storyboard file, then you don't have to register it at all. If the custom cell is in a separate nib file, you register the custom cell with registerNib, not registerClass. For example:
[self.tableView registerNib:[UINib nibWithNibName:#"MainCategoryTableViewCell" bundle:nil]
forCellReuseIdentifier:#"MainCategoryCell"];
For cells instantiated from a nib file, initWithCoder is called, not initWithStyle.
To configure any outlets of your custom cell, override awakeFromNib. The connections are
not yet established in initWithCoder.
For best understanding see the below image for just a deque reference.
Deque means you can add and delete cells from both the ends.
By ends I mean up and down.
Lets say you have 4 cell containg Acell,Bcell,Ccell and Dcell and height for row is for three cells.
so at a time only 3 cells would be visible.
when you scroll to see the Dcell , Acell would become as invisible row and memory for it will be reused for Dcell.
In the same way when you scroll to see the Acell , Dcell would become as invisible row and memory for it will be reused for Acell.
It says clearly in documentation
dequeueReusableCellWithIdentifier:forIndexPath:
For performance reasons, a table view's data source should generally
reuse UITableViewCell objects when it assigns cells to rows in its
tableView:cellForRowAtIndexPath: method. A table view maintains a
queue or list of UITableViewCell objects that the data source has
marked for reuse. Call this method from your data source object when
asked to provide a new cell for the table view. This method dequeues
an existing cell if one is available or creates a new one based on the
class or nib file you previously registered.
.
dequeueReusableCellWithIdentifier:
Return Value : A UITableViewCell object with the associated identifier
or nil if no such object exists in the reusable-cell queue.
Discussion : For performance reasons, a table view's data source
should generally reuse UITableViewCell objects when it assigns cells
to rows in its tableView:cellForRowAtIndexPath: method. A table view
maintains a queue or list of UITableViewCell objects that the data
source has marked for reuse. Call this method from your data source
object when asked to provide a new cell for the table view. This
method dequeues an existing cell if one is available or creates a new
one using the class or nib file you previously registered. If no cell
is available for reuse and you did not register a class or nib file,
this method returns nil.
If you registered a class for the specified identifier and a new cell
must be created, this method initializes the cell by calling its
initWithStyle:reuseIdentifier: method. For nib-based cells, this
method loads the cell object from the provided nib file. If an
existing cell was available for reuse, this method calls the cell’s
prepareForReuse method instead.
Before introducing storyboard.The tableview checks the returned cell which can be nil .So if nil we must reallocate the cell and tehn initialize and provide the cell in the datasource method
I created tableview and detailviewcontroller but once I select cell for example:I select the row Dog it's not writer Dog on navigationbar. How to do it?
I use with this code
But it's a warning that says local declaration of 'tableview' hides instance variable
DVC =DetailViewController
DVC.title = [[[self tableView:tableView cellForRowAtIndexPath:indexPath] textLabel] text];
Thanks!
You need to use this method: [self setTitle:#"Dog"];
This is not standard behavior of a UITableView. It only changes the Title if you switch to another ViewController.
You need to implement the method didSelectRowAtIndexPath in the ViewController. There you manually change the title of the Navigationbar.
I'm not 100% sure, but I think with that line of code you can change the title:
[viewController.navigationItem setTitle:#"Whatever you want"];
Edit:
The warning just says, that you have an instance variable called tableview and the delegate method also passes internally a variable called tableview. So just change the instance variable name which has the reference to the UITableView.
You should set your detailviewcontroller's title. You can set in your tableView's delegate method: didSelectRowAtIndexPath.
detailViewController.navigationController.title = #"Dog";
About your warning, it's because you have declared a variable called tableView but in the method you write
DVC.title = [[[self tableView:tableView cellForRowAtIndexPath:indexPath] textLabel] text];
there is also a variable called tableView.