How to implement UICollectionViewCells showing "stateful" contents? - ios

I am currently refactoring my Newsstand app.
Previously, I was showing several custom UIViews ("CoverView") in a self-calculated grid layout (3 items per row, as many rows as necessary) embedded in a scroll view, scrolling vertically.
Each "CoverView" consists of
a label (showing the issue title)
an image (showing the issue cover)
a button (showing "DOWNLOAD" or - if already downloaded - "READ")
a progress bar (initially hidden, showing the download progress)
Each CoverView has it's own UITapGestureRecognizer and UILongPressGestureRecognizer.
If a user taps once, the button is hidden, the download progress bar is shown and constantly updated with the download progress:
[issue addObserver:cover forKeyPath:#"downloadProgress" options:NSKeyValueObservingOptionNew context:NULL];
(The long press recognizer is used to let the user delete the issue).
All CoverViews are stored in a NSMutableDictionary, and are always kept in memory. Although the unscaled images are rather big (1024 pixels high), I never had performance problems while scrolling, probably since there are not that many issues (and therefore CoverViews) to store.
Since the self-implemented grid-layout is complex and difficult to maintain, I wanted to use a UICollectionView instead. I added a collection view and made my CoverView class inherit from UICollectionViewCell (instead of UIView).
The display and scrolling works, but I'm having these problems:
The scrolling is lagging when a new row of cells needs to be shown and collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath is called for each.
I tried to not reuse cells (to improve performance, strangely enough), but this failed due to each cell having to have it's reuse identifier set (and the "reuseIdentifier" property is readonly, so I cannot set it in code).
I don't know where to unset the observers for KVO for a cell. Is there a way to be notified once a cell gets queued for reuse?
In general, I'm unsure whether I'm misusing UICollectionView with my specific use case here and should stick to my previous approach, or whether it's just my inability to efficiently use and tweak UICollectionView.

To set 'reuseIdentifier', the proper design is to use UICollectionView methods :
registerClass:forCellWithReuseIdentifier:
or registerNib:forCellWithReuseIdentifier:
You typically call these methods once, depending on how you want to provide your cell's instantiation (inflated from NIB, or instantiated through standard [[ViewClass alloc] init]
Once views are registered, you can use them from UiCollectionView like this :
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"yourReuseId" forIndexPath:indexPath];
// Collection view handle ALL the work :
// 1 - retrieves a reusable view if views have already been recycled
// or 2- instantiate a new one, using the class/nib you registered
// So, cell SHOULDN'T be nil from here :)
// Note : from iOS 6, UITableView has a similar auto-instantiation mechanism
}
3 - to unset KVO, override UICollectionViewCell's -(void)prepareForReuse method (inherited from UICollectionReusableView)

Related

UICollectionViewCell not displaying in UICollectionView

I am new to iOS and just trying to build a template view for future use.
I designed a view in Main.Storyboard with a UICollectionView - with several (hardcoded, I guess?) UICollectionViewCells. The UICollectionView displays when I test the app (background of that portion changes color), but I am not sure how to get the UICollectionViewCells to display.
All I have done so far is connect the view to the appropriate view controller, but I am very confused as to where to go from here.
My Main.Storyboard : http://imgur.com/a/aXy3O
If possible - is there a way to quickly add a function to trigger an alert when any UICollectionViewCell is clicked?
Thanks.
UICollectionView have very similar structure like UITableView.
So if you check its delegate methods you will find what you need.In this case you need to implement this method:
- (void)collectionView:(UICollectionView *)collectionView
didSelectItemAtIndexPath:(NSIndexPath *)indexPath

Determine if a UITableViewCell of a UICollectionView contained in a UITableView is visible

I have a UITableView which contains a UICollectionView and I need to determine if the UITableViewCell of the UICollectionView are visible or not.
I tried to use these methods :
indexPathsForVisibleItems of UICollectionView objects, but it returns to me an array with all my UICollectionViewCells (the count on the array returns to me 799 when only 30 cells are visible), and I get the same result when I used the method visibleCells of UICollectionView objects which returns to me the same array.
Is there any way to solve this ?
Well there are a few ways to define visible. Perhaps if the edge of a cell is visible, then the cell may itself be considered as visible. If you wish to check it the entire cell is visible, you may try something like checking that the cell's frame is entirely contained within the UICollectionView's bounds.
You do not provide enough context for us to have a good guess, however, I can demonstrate a case where UICollectionView inside a UITableView reports the correct number of visible items using -indexPathsForVisibleItems:
Download the AFTabledCollectionView project from Github.
Open the project in Xcode and go to AFViewController.m.
Paste the following somewhere in the implementation:
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
NSArray *items = [collectionView indexPathsForVisibleItems];
NSLog(#"%lu", (unsigned long)items.count);
}
Run the project in the simulator and tap on an item in any UICollectionView. You will see in the console that the correct number of visible items for the collection view is being displayed.

UITableViewCell is redrawing on dequeue

My setup
I have a UITableViewCell that is in my main storyboard in a UITableViewController. It gets populated with some JSON data pulled from a REST API that will cause each cell to be a variable height. There are UIImageViews, UILabels all of different heights and styles, think Instagram-esque.
My problem
When I scroll to maybe the 5th or 6th cell, then go back up, they start redrawing and overlapping, so text gets mixed, lines get redrawn, etc.
What I've tried
This seems like a common problem on SO, so I've tried several posted solutions. It seems like my issue is probably the same problem as others face, which is, I am calling addSubview on my cell every time it dequeues, but I've tried checking to see if the cell already exists. I came across another post somewhere (sorry, I can't remember where), that suggests that because I am creating this in the storyboard, it is already initialized and if ( !cell ) will already return false, so I don't know how to prevent it from redrawing.
When I try removing the cell from the storyboard, and creating it programmatically, I get an error saying it can't find a cell with my identifier #"Cell".
I've also tried someone's solution of removing all subviews when I dequeue, so I used:
for ( UIView *view in cell.contentView.subviews ) {
if ([view isKindOfClass:[UIView class]]) {
[view removeFromSuperview];
}
}
and it doesn't find anything.
#rdelmar's comment is correct. You shouldn't do what you're doing. Might work, but it's bad form and you don't want to get into bad habits.
First, take advantage of object oriented programming. A cell should be able to configure itself based on the data you ask it to display. The table view shouldn't be designing the cell.
UITableViewCells need to be optimized for speed. Creating and adding subviews is a slow process. It's OK to do it once, but the cell will be reused (a system optimization) and you should just reuse the existing views that were added the first time the cell was created.
For example, you can hide subviews if they're not needed. You might want to do this in -prepareForReuse. You can move them around in -layoutSubviews. Or change the position of subviews in -updateConstraints.
Typically you just want to pass the data to display to the table view cell subclass from the data source (often the view controller). Let the cell do the display work.
When you add your subview after dequeueing uour cell, give a tag to your subview. This way, when you dequeue a cell, you can first check for the presence of a subview with your tag, and if it exists, remove it before adding your new view:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
...
// try to dequeue a cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:<yourCellIdentifier>];
if( !cell )
{
// create a new cell if necessary
}
static int _myViewTag = 1000987 // give it a high int : low value are used by the system in cells
UIView *v = cell.contentView viewWithTag:_myViewTag];
if( v ) // subview with such tag already exists, so remove it.
[v removeFromSuperview];
// now add your new subview
[cell.contentView addSubview:<yourView>];
// adjust height of cell to your view.
...
}
Try to add a new method in your cell class to reset cell to its default style and call this method after dequeueCell.
The most efficient way to manage this is to subclass UITableViewCell and adding all your required Views as properties. So now when a cell comes up for "recycling", you know where to find the old views, like :
[cell.myTextLabel setText:#""];
aaaand you're done.
UPDATE creating a subclass makes sense if you have only a small number of "TYPES" of cells. create a subclass for each. How much complicated it gets depends on your specific scenario. But i've done it and found it to be the most effective method.
UPDATE 2 or you could make multiple cells in the storyboard, and dequeue the appropriate one based on the data source, save all the coding.

UICollectionView inside UICollectionViewCell trouble in getting scroll touch

I have implemented UICollectionView inside a UICollectionViewCell (both from left to right:Horizontal scrolling) everything works fine. The trouble is that when the last cell in the innercollection is scrolled the outside UICollectionView gets the touches and gets scrolled accordingly to left or right (happens for first and last cell).
How can I avoid this scroll that happens by default.
if your custom UICollectionViewCell XIB file looks something like this at the the root (see the icon next to the Data Entry Matrix View, which is the name for my UICollectionViewCell subclass), then you will have the same problem:
The solution is to add a proper UICollectionViewCell class to the XIB, then move all the subviews under the existing Data Entry Matrix View into it, delete the old root, then reconnecting the Outlets. The end result looks like this:
You can see the different icon used - that's what you want. Once you would change your XIB like this the events were passed through.
In the raw XIB file (view as Source), the non-working version introduced the root element as
<view ...>
whereas the working version changed that root element to
<collectionViewCell ...>
Try making sure that the innercollection first react to pan gesture. This is easily done in code main collection code:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
id cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"cell" forIndexPath:indexPath];
[collectionView.panGestureRecognizer requireGestureRecognizerToFail:cell.innercollection.panGestureRecognizer];
return cell;
}

Can I "take out" or "tear off" a cell from a UICollectionView?

Note - it's possible UIViewControllerAnimatedTransitioning
https://developer.apple.com/library/ios/documentation/uikit/reference/UIViewControllerAnimatedTransitioning_Protocol/Reference/Reference.html
is relevant here.
Note - I've just realised the new iOS7 "swipe-away" gesture is this concept
there are some answers HERE:
The UICollectionView "swipe-away" in iOS7 app manager?
Imagine your iPad has a UICollectionView ("bigView"), vertical, which contains 10 cells, each about 400x400 say. Imagine the bigView is on the right half of the screen, say.
I want to do this:
Cell number 3 is perhaps clicked.
That cell moves away from the collection view (say, to the left of the screen). TBC it moves outside the boundary of the UICollectionView. Ideally it becomes a normal UIView (say).
In fact, I want to:
(1) destroy the other 9 cells
(2) destroy entirely the UICollectionView
(3) in fact, convert the one cell, #3, to (I guess) a UIView
I'll take it from there!
Can this be done??
(a) can you "convert" a UICollectionViewCell to a UIView (does it have a UIView guts you can "remove" ??)
(b) can you (perhaps) destroy all of the other cells, leaving the UICollectionView as, indeed, really being only that one cell (and perhaps move the "whole thing") ...
(c) is there any other sort of "tear off" mechanism from a UICollectionView?
Having worked on this many, many times now, here's pretty much the simplest basic solution.
You'll have a class that is literally the collection view,
#interface BouncyItems : UICollectionViewController
in that class you'll have a didSelectItemAtIndexPath: or similar:
in there, you can easily get at the cell in question.
-(void)collectionView:(UICollectionView *)cv
didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger thisRow = indexPath.row;
NSString *thatName = yourData.stuff[thisRow][#"nameField"];
YourCellClass *cell = [cv cellForItemAtIndexPath:indexPath];
NSLog(#"Yo, peeling off ....... %# %#", theName);
YourScene *parent = (YourScene *)self.parentViewController;
[parent peelOff:cell.holder];
}
But more importantly, you're going to have an actual scene, that has a container view, which is the collection view.
So in the example, "YourScene" is the UIViewController, which indeed has a few container views. One of those container views is indeed the collection view in question.
#interface YourScene:UIViewController
#property (nonatomic, strong) IBOutlet UIView *bouncyItemsContainer;
-(void)peelOff:(UIView *)peelMe;
#end
Note that in storyboard, looking at your BouncyItems, you would have dragged back the connection from it to "bouncyItemsContainer"
(No different than how you'd hook up buttons in "YourScene" to the relevant IBOutlet items.)
So, looking at the code in didSelectItemAtIndexPath, it's this easy,
YourScene *parent = (YourScene *)self.parentViewController;
[parent peelOff:cell.holder];
What is ".holder" .. explained below.
Now what is the routine peelOff ? Amazingly it's this simple.
The secret sauce is the convertPoint: line of code...
-(void)peelOff:(UIView *)peelMe
{
CGPoint oldCenter = [self.view
convertPoint:peel.center
fromView:peel.superview];
[self.view addSubview:peelMe];
peelMe.center = oldCenter;
[self.bouncyItemsContainer exitLeft];
}
{Recall that addSubview: conveniently removes the view from it's old superview, so it does all that for you in one line of code.}
So, that's it - the first three lines of code, you've removed the one cell, peelMe, from the container view. It is now just part of the normal scene, YourScene.
Next, simply "get rid of" the collection view. In my example, exitLeft simply animates it off the screen to the left.
So, it's perfect - you click on one cell; all the rest of the collection view animates off the screen (you could do whatever you want there, make them "fly off", face out, tumble - whatever), and the one chosen cell remains behind. Due to the "magic" convertPoint line of code, everything stays correctly in place.
Finally all cells must have a ".holder" which is.....
a UIView that holds everything in the cell; that's what you peel off. (This is the only practical solution, don't take out the underlying .contentView of the cell.) You can carefully arrange white/clear etc backgrounds so that everything "works" properly if you're flying stuff around in the scene.
So, that's all there is to it.
You now have (1) got rid of the collection view, and (2) your YourScene owns the thing that was a cell.
So, in peelOff: you would pass through the .holder UIVIew, and, you would probably also pass through some of your data, as in "the user has selected this house to see the interior" or whatever your context is.
Finally in the original question, it mentions then moving the peeled view, to some position on the YourScene. Of course, just animate it whether you want, probably in peelOff:.
Hope it helps.
I've done something along these lines. I've implemented dragging of cells around the collection view and between sections. I also needed an 'external target' to the collection view to drag cells onto. I simply turned off masksToBounds on the collectionView's layer, and I was able to drag my fake floating cell (representing the real, hidden cell) beyond the bounds of the collection view.
Regardless, I think it's much easier to basically hide the cells that are not "focused" in your example, and animate out the changes for the focussed cell as needed.
Consider implementing - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect and - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath on your collection view layout/s. You can both relayout your collectionView to move/resize the focussed cell, as well as setting the frame of the "destroyed" cells to off screen, smaller, or otherwise modified.
You can also animate these layout changes, or even move between layouts, which can make for very nifty transitions.
There is no reason one cell in the collectionView cannot be very large and have a very different frame than all the other cells. If I were to do this, I might keep the normal cells in one section, and the focussed cell as the only cell in another (as it's trivial to move cells between sections). Then, using the above methods, I'd have an easier time theming the layout for the cells en masse, as I could refer the the items by section.

Resources