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.
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 would like to subclass a UICollectionView (not a UICollectionViewController), and I would like to know how I can set it up so that when the user highlights (or selects) a cell, the collection view can be notified, so I can perform a little animation on the cell. You may ask why I can't do that in a view controller. I chose to subclass UICollectionView so that it could be reusable. I am relatively new to iOS programming, and I would welcome any suggestions or ideas.
You can use a block ^{}
Create a class with a .xib file. The .xib file will be used for each cell.
In your .xib file, add a clear UIButton, so that is on top of all your subviews. So the user can click on it.
In your .h file add
#property (copy, nonatomic) void (^actionBlock)(void);
In your .m file add and link it to your UIButton in the .xib file
- (IBAction)showAnimation:(id)sender
{
if (self.actionBlock) {
self.actionBlock();
}
}
Now in UICollectionViewController, cellForItemAtIndexPath, call the block
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
MyCellClass *cell =
[collectionView dequeueReusableCellWithReuseIdentifier:#"cell"
forIndexPath:indexPath];
cell.actionBlock = ^{
//Here you have access to indexPath.section and indexPath.row
NSLog(#"Going to animate the cell %# x %#", indexPath.section, indexPath.row);
//do any other code for this specific cell
};
return cell;
}
Using action blocks, is like opening a portal into each cell, happy coding
UICollectionView is the view and UICollectionViewController is the viewController.
The UICollectionView you are subclassing is use for updating user interface with viewController's core logic to be triggered while highlighting (or selecting) the cell.
So you should set up the selected logic in your viewController.
If you understood Delegate Pattern, the normal way to update your UICollectionView's cell while selected is using a delegate. When something triggered, call viewcontroller to do that for you.
Check out these links about
Cocoa MVC Design Pattern and Designing Your Data Source and Delegate for CollectionView.
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!
As soon as the cell is no longer visible on the screen I need to be notified.
UITableView already has a delegate method called tableView:didEndDisplayingCell:forRowAtIndexPath: but this delegate method never gets called. And yes I do have the delegate for my UITableView set.
Any other ways to detect a cell being removed? I need to be able to save the content (inputs) of this cell before it's being reused by another item.
EDIT:
According to the documentation tableView:didEndDisplayingCell:forRowAtIndexPath: is iOS 6 and higher API. Is there a way to achieve this on iOS 5?
On versions of iOS older than 6.0, the table view doesn't send the tableView:didEndDisplayingCell:forRowAtIndexPath: message.
If you are using a subclass of UITableViewCell, you can get the same effect on older versions of iOS by overriding didMoveToWindow:
- (void)didMoveToWindow {
if (self.window == nil) {
// I have been removed from the table view.
}
}
You may need to give your cell a (weak or unsafe_unretained) reference back to your table view delegate so you can send the delegate a message.
However, you can't rely only on didMoveToWindow for all versions of iOS. Before iOS 6, a table view always removed a table view cell as a subview before reusing it, so the cell would always receive didMoveToWindow before being reused. However, starting in iOS 6, a table view can reuse a cell without removing it as a subview. The table view will simply change the cell's frame to move it to its new location. This means that starting in iOS 6, a cell does not always receive didMoveToWindow before being reused.
So you should implement both didMoveToWindow in your cell subclass, and tableView:didEndDisplayingCell:forRowAtIndexPath: in your delegate, and make sure it works if both are called, or if just one is called.
I ended up using the combination of below to make sure that the logic applies to both iOS 5.0 and 6.0
CELL LOGIC
#protocol MyCellDelegate
- (void)myCellDidEndDisplaying:(MyCell *)cell;
#end
#implementation MyCell
// Does not work on iOS 6.0
- (void)removeFromSuperview
{
[super removeFromSuperview];
[self.delegate myCellDidEndDisplaying:(MyCell *)self];
}
#end
VIEWCONTROLLER LOGIC
#implementation MyViewcontroller
- (void)myCellDidEndDisplaying:(MyCell *)cell
{
IndexPath *indexPath = [self.tableView indexPatForCell:cell];
// do stuff
}
// Does not work on iOS below 6.0
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
}
#end
tableView:didEndDisplayingCell:forRowAtIndexPath:
is only available in iOS6 and later.
One (albeit slow) way to accomplish what you are after would be to use the scrollView delegate methods to monitor when the tableview scrolls. From there, call:
NSArray *visiblePaths = [tableView indexPathsForVisibleRows];
and check for any changes to the array of visible paths.
I used IB to create a UITableViewController in Storyboard. And I set the UITableview's delegate to be its controller. The UITableView has static cells. Just 2 sections. First section has 3 non-selectable rows. And last section has 1 row which is selectable. I set this all in IB only.
Then I implemented this method in the UITableViewController.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"SELECTED");
if (indexPath.section == 1 && indexPath.row == 0) {
//login and report the result
[self login];
}
}
When I select the only row in the 2nd section, this above method is not getting called. What could have gone wrong. I have double checked the delegate setups in outlets inspector of UITableView as well. Everything is fine!
Do you see the NSLog statement in the console? How about in viewDidAppear, NSLog(#"Delegate: %#",tableView.delegate)
See if you're getting NULL in the console.
You know that the dataSource is connected right, or else you wouldn't have any cells visible. It's got to be your delegate connection =)
Just do this:
Control drag you tableView from Storyboard to viewcontroller .h file, create #property for ex. tableView. Add again to .h file. Then in .m file in
viewDidLoad write this:
-(void)viewDidLoad{
//your other code
self.tableView.delegate = self;
self.tableView.dataSource = self;
//other code ...
}
I prefer to use tblView for property name, because you will see warning that tableView hides instance. Hope this help.