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!
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 change the value of 2 UILabels in my "viewDidLoad" method, but I need the view to refresh after that in order to display the values. As it currently stands, the UILabels display the value of the previously selected cell. I need to do the refresh right after I change the labels' values. The "setNeedsDisplay" method is not doing the job.
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
_nameLabel.text = _selectedLocation.name;
_addressLabel.text = _selectedLocation.address;
[self.view setNeedsDisplay];
}
Based on your comments, I think you are trying to do something like:
- (void)updateLabelTexts {
_nameLabel.text = _selectedLocation.name;
_addressLabel.text = _selectedLocation.address;
}
and wherever you are changing the _selectedLocation values:
//Just an example
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
_selectedLocation = _yourLocationsArray[indexPath.row];
//now you call your update method
[self updateLabelTexts];
}
The point is that you have to call [self updateLabelTexts]; just after you update the values.
A very stupid bug. Turns out when I made the segue to transition into the next view, I actually dragged it from a physical cell on to the destination controller. However, I should've simply connected the sending uiview controller to the destination viewcontroller with the segue, and then manually handled the transition. That fixed it, so there's no need to "refresh or reload" the UIView as I was trying to do.
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;
I have a UIViewController which has the following implementation for the didSelectItemAtIndexPath
#interface
id section1Item
NSMutableArray *section2Items
NSMutableArray *section3Items
#end
#implementation
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.section == 0) {
;
} else if (indexPath.section == 1) {
self.section1Item = [self.section2Items objectAtIndex:indexPath.row];
} else { // indexPath.section == 2
id newSection2Item = [self.section3Items objectAtIndex:indexPath.row];
[self.section2Items addObject:newSection2Item];
[self.section3Items removeObject:newSection2Item];
}
[collectionView reloadData];
}
#end
The idea behind the code is that my collectionView has a static number of sections, and taping on an item in section 3 moves the item to section 2, and tapping on an item in section 2 makes it an item in section 1.
However once I make the changes to my dataStructure (section1Item, section2Items and section3Items), and call reloadData, all my UICollectionView cells disappear. A few symptoms of the issue
After the reloadData call, non of my dataSource methods get recalled. I tried putting a breakpoint in my implementation of numberOfSectionsInCollectionView and collectionView:numberOfItemsInSection but they don't get hit.
I tried debugging using RevealApp, and I found out that after reloadData call, all my UICollectionViewCell's have their hidden property set to "YES", even though I don't have any code in my code base calling .hidden = YES;
I also tried overriding UICollectionViewCell#setHidden to detect what (if any) part of the UIKit framework calls it, and again there was no breakpoint triggers.
Tools details: I'm working with XCode5-DP6 on iOS7 simulator.
UPDATE: My UICollectionView shows all the cells correctly on first render.
Ok peeps, so I was able to figure out the issue. The delegate (self) was a subclass of UIViewController. In the init, I was assigning self.view = viewFromStoryBoard where viewFromStoryBoard was passed in by the caller and which was setup in a storyboard.
Since I was not really using any of the facilities offered by subclass UIViewController, I decided to switch to subclassing NSObject and manually retaining the pointer to the UICollectionView.
This fixed my problem. However I'm not a 100% on the exact nature of the issue. I'm guessing somehow overriding a UIViewController's view isn't all that it seems.
There are lots of bugs with iOS 7 and UICollectionView... In my case reloadData doesn't work correctly, it works with delay.
UICollectionView: I'm doing it wrong. I just don't know how.
My Setup
I'm running this on an iPhone 4S with iOS 6.0.1.
My Goal
I have a table view in which one section is devoted to images:
When the user taps the "Add Image..." cell, they are prompted to either choose an image from their photo library or take a new one with the camera. That part of the app seems to be working fine.
When the user first adds an image, the "No Images" label will be removed from the second table cell, and a UICollectionView, created programmatically, is added in its place. That part also seems to be working fine.
The collection view is supposed to display the images they have added, and it's here where I'm running into trouble. (I know that I'm going to have to jump through some hoops to get the table view cell to enlarge itself as the number of images grows. I'm not that far yet.)
When I attempt to insert an item into the collection view, it throws an exception. More on that later.
My Code
I've got the UITableViewController in charge of the table view also acting as the collection view's delegate and datasource. Here is the relevant code (I have omitted the bits of the controller that I consider unrelated to this problem, including lines in methods like -viewDidLoad. I've also omitted most of the image acquisition code since I don't think it's relevant):
#define ATImageThumbnailMaxDimension 100
#interface ATAddEditActivityViewController ()
{
UICollectionView* imageDisplayView;
NSMutableArray* imageViews;
}
#property (weak, nonatomic) IBOutlet UITableViewCell *imageDisplayCell;
#property (weak, nonatomic) IBOutlet UILabel *noImagesLabel;
#end
#implementation ATAddEditActivityViewController
- (void)viewDidLoad
{
[super viewDidLoad];
UICollectionViewFlowLayout* flowLayout = [[UICollectionViewFlowLayout alloc] init];
flowLayout.scrollDirection = UICollectionViewScrollDirectionVertical;
imageDisplayView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 290, 120) collectionViewLayout:flowLayout]; // The frame rect still needs tweaking
imageDisplayView.delegate = self;
imageDisplayView.dataSource = self;
imageDisplayView.backgroundColor = [UIColor yellowColor]; // Just so I can see the actual extent of the view
imageDisplayView.opaque = YES;
[imageDisplayView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:#"Cell"];
imageViews = [NSMutableArray array];
}
#pragma mark - UIImagePickerDelegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
/* ...code defining imageToSave omitted... */
[self addImage:imageToSave toCollectionView:imageDisplayView];
[self dismissModalViewControllerAnimated:YES];
}
#pragma mark - UICollectionViewDelegate
- (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
#pragma mark - UICollectionViewDatasource
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
[[cell contentView] addSubview:imageViews[indexPath.row]];
return cell;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return [imageViews count];
}
#pragma mark - UICollectionViewDelegateFlowLayout
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return ((UIImageView*)imageViews[indexPath.item]).bounds.size;
}
#pragma mark - Image Handling
- (void)addImage:(UIImage*)image toCollectionView:(UICollectionView*)cv
{
if ([imageViews count] == 0) {
[self.noImagesLabel removeFromSuperview];
[self.imageDisplayCell.contentView addSubview:cv];
}
UIImageView* imageView = [[UIImageView alloc] initWithImage:image];
/* ...code that sets the bounds of the image view omitted... */
[imageViews addObject:imageView];
[cv insertItemsAtIndexPaths:#[[NSIndexPath indexPathForItem:[imageViews count]-1 inSection:0]]];
[cv reloadData];
}
#end
To summarize:
The collection view is instantiated and configured in the -viewDidLoad method
The UIImagePickerDelegate method that receives the chosen image calls -addImage:toCollectionView
...which creates a new image view to hold the image and adds it to the datasource array and collection view. This is the line that produces the exception.
The UICollectionView datasource methods rely on the imageViews array.
The Exception
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (1) must be equal to the number of items contained in that section before the update (1), plus or minus the number of items inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).'
If I'm parsing this right, what this is telling me is that the (brand new!) collection view thinks it was created with a single item. So, I added a log to -addImage:toCollectionView to test this theory:
NSLog(#"%d", [cv numberOfItemsInSection:0]);
With that line in there, the exception never gets thrown! The call to -numberOfItemsInSection: must force the collection view to consult its datasource and realize that it has no items. Or something. I'm conjecturing here. But, well, whatever: the collection view still doesn't display any items at this point, so I'm still doing something wrong and I don't know what.
In Conclusion
I get an odd exception when I attempt to add an item to a newly-minted-and-inserted collection view...except when I call -numberOfItemsInSection: before attempting insertion.
Even if I manage to get past the exception with a shady workaround, the items still do not show up in the collection view.
Sorry for the novel of a question. Any ideas?
Unfortunately the accepted answer is incorrect (although it's on the right track); the problem is that you were calling reloadData & insertItems when you should have just been inserting the item. So instead of:
[imageViews addObject:imageView];
[cv insertItemsAtIndexPaths:#[[NSIndexPath indexPathForItem:[imageViews count]-1 inSection:0]]];
[cv reloadData];
Just do:
[imageViews addObject:imageView];
[cv insertItemsAtIndexPaths:#[[NSIndexPath indexPathForItem:[imageViews count]-1 inSection:0]]];
Not only will this give you a nice animation, it prevents you from using the tableview inefficiently (not a big deal in a 1-cell collection view, but a huge problem for larger data sets), and avoids crashes like the one you were seeing, where two methods were both trying to modify the collection view (and one of them -- reloadData -- does not play well with others).
As an aside, reloadData is not very UICollectionView-friendly; if you do have a sizable &/or complex collection, and an insertion happens shortly before or after a call to reloadData, the insertion might finish before the reloadData finishes -- which will reliably cause an "invalid number of items" crash (same goes for deletions). Calling reloadSections instead of just reloadData seems to help avoid that problem.
Faced same issue but the reason with me was that I forgot to connect the collection view Data Source to view controller
It is because the [cell count] don't equal the [real index count] + [insert indexes].
Sometimes the dispatch_async don't include block of the array insert data and insertItemsAtIndexPaths.
I got the problem with somtimes crash. It is not cause each time crash.
Just a guess: at the time you are inserting the first image, the collection view may not yet have loaded its data. However, in the exception message, the collection view claims to "know" the number of items before the insertion (1). Therefore, it could have lazily loaded its data in insertItemsAtIndexPaths: and taken the result as "before" state. Also, you don't need to reload data after an insertion.
Long story short, move the
[cv reloadData];
up to get
if ([imageViews count] == 0) {
[self.noImagesLabel removeFromSuperview];
[self.imageDisplayCell.contentView addSubview:cv];
[cv reloadData];
}