I want to have a UICollectionView that scrolls from right to left, i.e. when it appears on screen after being loaded the collection view should should show the rightmost cells/items first and then add the rest at the left. I've tried the workaround presented here however if I call this in viewWillAppear: I get:
*** Assertion failure in -[UICollectionViewData layoutAttributesForItemAtIndexPath:], /SourceCache/UIKit/UIKit-2372/UICollectionViewData.m:485
If I scroll to the last item in viewDidAppear: it works fine, except now user first sees the left items and then the scrolling to the last item. Also tried using the contentOffset property on UICollectionView (as it's a subclass of UIScrollView) but this parameter is also only set sometime in-between viewWillAppear: and viewDidAppear:
Any alternatives? I guess I could try subclassing the UICollectionViewFlowLayout and layout the cells from right to left, but I'm a little anxious to go into that territory.
This is what I do:
- (void)_scrollToTheRight
{
NSIndexPath *lastIndexPath = [self.fetchedResultsController indexPathForObject:self.fetchedResultsController.fetchedObjects.lastObject];
if (lastIndexPath)
{
[self.collectionView scrollToItemAtIndexPath:lastIndexPath atScrollPosition:UICollectionViewScrollPositionRight animated:NO];
}
}
Each value is:
lastIndexPath
(NSIndexPath *) $0 = 0x1f1754a0 <NSIndexPath 0x1f1754a0> 2 indexes [0, 21]
(NSInteger)[self.fetchedResultsController.fetchedObjects count]
(NSInteger) $3 = 22 [no Objective-C description available]
(NSInteger)[self.collectionView numberOfItemsInSection:0]
(NSInteger) $2 = 22 [no Objective-C description available]
Last note: the collection view controller is loaded from a container view (new in iOS 6 storyboards) of a view controller that is put on screen by a UIPageViewController.
Thanks.
EDIT 1:
So I think the problem is that using my storyboard layout (UICollectionViewController's embedded using the new Container Views) is causing the problem.
Because elsewhere in the app where I also embed view controllers (a normal UITableViewController) viewDidAppear: gets called before the view actually appears. I'm suspecting that this setup is not notifying each child view controller properly that they should load and layout their views.
Since iOS 9 one can use
collectionView.semanticContentAttribute = .forceRightToLeft
for right to left scrolling when scrollDirection = .horizontal and the itemSize fills the entire height (i.e. only one horizontal line of items)
Related
I'm trying to create an interface similar to the Home scene in the meetup app. You can see it in action below. I want to recreate the [All, Going, ...] menu behavior. I want it to start in the middle of the list and scroll up until it reaches the top of the list and stick there. Very similar to how section headers work in a UITableView.
Creating the menu is not the issue. My problem is creating the sticky behavior and have it work well with the rest of the list.
I've tried using a UITableView but I couldn't get the menu cell to stick. I can't put the menu in a section header because I want to use section headers for the data below the menu and UITableView's behavior is to push a section header up when the next section reaches the top of the list. I can't put the menu in the UITableView.tableHeader because the menu starts below some other data in the list.
UITableView
- UITableViewCell -> Label
- UITableViewCell -> UICollectionView of UIImageViews
- UITableViewCell -> Label
- UITableViewCell -> MyMenu (Sticky)
- UITableViewHeaderFooterView - Section 1
- UITableViewCell -> Data
- UITableViewCell -> Data
- UITableViewHeaderFooterView - Section 1
- UITableViewCell -> Data
- UITableViewCell -> Data
I've tried using a UIScrollView containing the menu and a UITableView below it but using a UITableView (which is a UIScrollView) inside a UIScrollView is painful. I couldn't get the scrolling behavior to feel natural.
UIScrollView
- UIView -> (Container)
- Label
- UICollectionView of UIImageViews
- Label
- MyMenu (Sticky)
- UITableView - Data
I'm about to try and write a UICollectionViewLayout to do what I want but I feel like I will have to recreate functionality that I get for free with UITableView.
Any idea how to approach this? Perhaps there is a reliable method to make a UITableViewCell stick and for subsequent section headers to stick under it?
One way to implement something like this is with a view hierarchy like this:
UIView
- UITableView
- UIView -> (Container)
- Label
- UICollectionView of UIImageViews
- Label
- MyMenu (Sticky)
Your container with your menu is a sibling of the table view, but it overlaps it.
In the scroll view delegate method scrollViewDidScroll(_:) you can reposition your menu container view so the menu is positioned above the table content. Then you need to tell the table view to reserve some space between the top and the first table cell. For this you can configure the contentInset of the table view.
I would use a table view.
Add an empty cell that will be where your control will be placed while it's visible, and to avoid your control covering any content.
Add your control as a subview of your table view.
Then override scrollViewDidScroll (UITableView is a subclass of UIScrollView so they share delegate methods).
In scrollViewDidScroll, which gets called at least every frame while the scroll view is scrolling, update the position of the content, like this:
let controlFrame = tableView.rectForRow(at: indexPathOfYourBlankCell)
controlFrame.origin.y = max(0, tableView.contentOffset.y - controlFrame.y)
control.frame = controlFrame
tableView.bringSubviewToFront(control)
Keep in mind that you will have to tweak the second line if your table view has a top inset, for example, if it's under a transparent navigation bar, or you're using an iPhone with a notch.
I suggest implementing it first o an notch-less iPhone simulator, with no navigation bar, and once it works you can tweak the way the y property is calculated by adding the inset.
I think something like this would work, but I'm not sure.
controlFrame.origin.y = max(0, tableView.contentOffset.y + tableView.contentInsets.top - controlFrame.y)
I implemented #EmilioPelaez's suggestion of using a separate menu view, positioning it over an empty cell and moving it as the table scrolls. To make it work I had to do the following things:
Find the frame of the empty cell so I can position the menu over it
As the empty view moves outside the visible area of the table view move the menu so it stays inside the visible area of the table view. It should look like it is docked to the top of the table view.
When the empty cell reaches the top adjust the tableView.contentInsets.top so the section headers below look like they stick to the bottom of the menu
When the table scrolls in the other direction reset the tableView.contentInsets.top
Support dynamic type and rotation changes
I ended up doing everything in viewDidLayoutSubviews because I need to handle rotation and dynamic text changes and scrollViewDidScroll isn't called on every rotation and dynamic text change. viewDidLayoutSubviews is called after almost every scrollViewDidScroll.
let menuCellPath = IndexPath(row: 1, section: 1)
var tableViewInsetCached = false
var cachedTableViewInsetTop: CGFloat = 0.0
override func viewDidLayoutSubviews() {
// Cache the starting tableView.contentInset.top because I need to change it later
// when the menu is docked to the top of the table
if !tableViewInsetCached {
cachedTableViewInsetTop = tableView.contentInset.top
tableViewInsetCached = true
}
// Get the frame of the empty cell. Use rectForRow instead of cellForIndexPath so it
// works even if the cell has been reused.
let menuCellFrame = tableView.rectForRow(at: menuCellPath)
// Calculate how far the menu must move to continue to be within the
// visible area of the scroll view. If the delta is a negative number
// the cell is within the visible area so clamp it at 0, i.e., don't move it.
// Use `tableView.safeAreaInsets.top` to take into account the notch, translucent
// UINavigationBar, and status bar.
let menuFrameDeltaY = max(0, tableView.safeAreaInsets.top + tableView.contentOffset.y - menuCellFrame.origin.y)
// Add the delta to the menu's frame
var newMenuFrame = menuCellFrame
newMenuFrame.origin.y = menuCellFrame.origin.y + menuFrameDeltaY
menuView.frame = newMenuFrame
if menuFrameDeltaY > 0 {
print("cell outside visible area -> change contentInset")
// Change the contentInset so subsequent section headers
// stick to the bottom of the menu
tableView.contentInset.top = menuCellFrame.size.height
} else {
print("cell inside visible area -> reset contentInset")
// The empty cell is inside the visible area so we should
// reset the contentInset
tableView.contentInset.top = cachedTableViewInsetTop
}
}
It's important to remember that we are dealing with a UIScrollView under the hood. The frames of its subviews don't change as the table is scrolled. Only the contentOffset changes which means that max(0, tableView.safeAreaInsets.top + tableView.contentOffset.y - menuCellFrame.origin.y) calculates the amount the menu must move to continue to be within the visible area of the table view. If the delta is less than zero the empty cell is within the visible area of the table view and I don't have to move the menu, just give it the same frame as the empty cell which is why I use max(0, x) to clamp it at zero. If the delta is greater than zero the empty cell is no longer within the visible area of the table view and the menu must be moved to continue to be within the visible area.
I have a collection view, and I am trying to get the index of the cell that I am peeking and poping from.
Issue
Currently I am using indexPathForItemAtPoint: however this always returns 0 no mater where I tap on the screen.
Code
collection view controller:
- (void)viewDidLoad {
[super viewDidLoad];
[self registerForPreviewingWithDelegate:self sourceView:self.collectionView];
}
- (UIViewController *) previewingContext:(id <UIViewControllerPreviewing>)previewingContext viewControllerForLocation:(CGPoint)location {
CellEditViewController *CEVC = [self.storyboard instantiateViewControllerWithIdentifier:#"detail"]; //The view I want peek/pop to
NSLog(#"Location: %f,%f", location.x, location.y);
NSLog(#"Index of: %lu", [[self.collectionView indexPathForItemAtPoint:location] row]);
[CEVC setPreferredContentSize:CGSizeMake(0.0, 320.0)];
return CEVC;
}
- (void)previewingContext:(id <UIViewControllerPreviewing>)previewingContext commitViewController:(UIViewController *)viewControllerToCommit {
[self showViewController:viewControllerToCommit sender:self];
}
What I have tried
Creating a new location to identify the index cell.
Moving registerForPreviewingWithDelegate:sourceView: to where I create each cell.
Moving previewingContext:viewControllerForLocation: and previewingContext:commitViewController: to the cell view method, this did not work for other reasons.
I do not think this is an issue with previewing, because when I implemented the same thing with a UITapGestureRecognizer, I got a similar output:
Tap recognizer:
- (void) processDoubleTap:(UITapGestureRecognizer *)sender {
NSLog(#"Got tapped twice");
if (sender.state == UIGestureRecognizerStateEnded) {
CGPoint point = [sender locationInView:self.collectionView];
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:point];
NSLog(#"Index was: %lu", [indexPath row]);
if (indexPath) {
NSLog(#"Index was double tapped");
}
}
}
Output:
2017-12-25 10:48:13.990523-0800 quickthings[3052:356150] Got tapped twice
2017-12-25 10:48:13.990843-0800 quickthings[3052:356150] Index was: 0
Source
Github Repository
Screenshot
Here is what does happen, this is exactly what I want. The only other thing I would like to do is when the cell is tapped also be able to get the index of the tapped cell.
Collection View In Story Board
The (blue) UIView is "linked" to the Collection View Controller (the top view controller in the second screenshot below).
The Cause:
The main cause of the problem is how the CollectionViewController is setup in the storyboard. In summary these are the main issues with it:
The CollectionViewController was added as a UIViewController and then the class changed to CollectionViewController but the problem is that is a subclass of a UICollectionViewController. So there is a miss match between what the storyboard thinks it is and what it actually is. You should have added it as a UICollectionViewController to begin with.
The CollectionViewControllers top view has had it's class changed from UIView to UICollectionView which I assume was to match how the UICollectionViewController is setup. So again there is a miss match here meaning you can't see any of the correct properties in the Interface Builder.
There is then an additional UICollectionView added as a sub view of the main CollectionViewController main view. This has its data source and delegate linked into the CollectionViewController class and is the one that is actually being displayed. It's all setup correctly with a prototype cell. However it's not linked into the class as an IBOutlet so you can't reference it.
So why is this causing the problem you are seeing. In the CollectionViewController when you refer to self.collectionView you are referring to the top level one which is not the one that is displaying the cells. That's not the one displaying the cells and in fact has no displayed cells so when you ask for the cell at a particular point it will always return nil and hence you get a row value of zero. If you were able to get access to the UICollectionView actually displaying the cells you could get the correct value.
Other than that there will be other issues that you haven't come across yet.
The Solution:
There are basically two main solutions to this problem...
Remove the CollectionViewController from the storyboard and add it again as a UICollectionViewController. This of course is the ideal way to resolve it as it will all be setup correctly however it will require re-creating the entire controller in the storyboard again and is not absolutely necessary as you can do method 2...
You can modify your storyboard and setup to correctly reflect the setup you have and get it working.
I have tried method 2 and this is what I had to do to get it working correctly:
Change the CollectionViewController from subclassing UICollectionViewController to subclassing UIViewController as it will just become a holder for the UICollectionView. You also need to make it support UICollectionViewDataSource and UICollectionViewDelegate directly as the UICollectionViewController already does this but the UIViewController doesn't. So change this line in CollectionViewController.m:
#interface CollectionViewController : UICollectionViewController
to this:
#interface CollectionViewController : UIViewController <UICollectionViewDelegate, UICollectionViewDataSource>
Next in the storyboard change the top level view of the CollectionViewController back to a UIView (i.e. delete the UICollectionView in the class property). It's now just a holder for the real UICollectionView.
Next link the remaining UICollectionView that is setup in the storyboard to an IBOutlet in CollectionViewController.h called 'collectionView' so that all the references to self.collectionView are now referring to the correct thing.
One extra thing I had to do but I'm not sure why is change the cell identifier for the UITableViewController and UICollectionViewController so that they are not both 'Cell'. I just used 'TableCell' and 'CollectionCell'. If you don't do this they both get confused for some reason.
(I think that is all I did but if not I'm sure you will be able to handle any other issues)
Once all that has been done then the self.collectionView will be referring to the correct thing and you will get the correct index path in the - (UIViewController *) previewingContext:(id <UIViewControllerPreviewing>)previewingContext viewControllerForLocation:(CGPoint)location method.
Bonus Round:
Now some extra bonus comments. You appear to also have the same kind of issue with the TableViewController although it doesn't appear to be causing any issues at the moment. You may want to fix it though.
When I first ran the project I could not see anyway to add anything to the table because the text field and 'add' button were missing. This turned out to be because I was using an iPhone 8 simulator not an iPhone X simulator and once I switched everything was ok and I could see the text field and 'add' button. What this means is your interface is not adapting to the different device sizes correctly (at all really) and you should really address that.
Phew that ended up longer than I thought but hopefully will clear up the problem. Also as I had access to the git repository I could clone it and make my changes. I obviously haven't committed them as it's your repository but if you have any trouble understanding what I have done to get it to work I can commit them ether into your main branch or as a new branch.
I am creating a paginated UICollectionView that scrolls horizontally and whose cells are as big as the collection view so that only one cell is shown at a time. I also want to display the last item first when the collection view first appears, so that the other items are revealed from the left instead of the default where the next items come in from the right.
To do that, I call scrollToItemAtIndexPath: in my view controller's viewDidLayoutSubviews as suggested by this answer. If I put the call to scrollToItemAtIndexPath in viewWillAppear or viewDidLoad, the collection view does not at all scroll to the last item.
However, this call destroys the layout of my collection view cells. For example, if I don't call scrollToItemAtIndexPath:, the collection view (the white area below) looks like the left one--correctly laid out but showing the first item. If I call scrollToItemAtIndexPath:, the collection view does initially display the last item, but the layout is messed up like in the right (the date isn't even showing anymore).
What am I doing wrong?
More info:
I see this error both in iOS 7 and iOS 8.
I use size classes in Xcode 6.1.
The code for viewDidLayoutSubviews:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
NSIndexPath *lastIndexPath = [NSIndexPath indexPathForItem:self.unit.readings.count - 1 inSection:0];
[self.readingCollectionView scrollToItemAtIndexPath:lastIndexPath atScrollPosition:UICollectionViewScrollPositionLeft animated:NO];
}
Whenever I've had this issue and I have a constant size cell I've worked around it by just setting the contentOffset manually and ignoring the collectionView methods.
self.collectionView.contentOffset = (CGPoint){
.x = self.collectionView.contentSize.width - cell.width,
.y = 0,
};
I put the following in viewWillLayoutSubviews (also works in viewDidLayoutSubviews):
[self.readingCollectionView scrollToItemAtIndexPath:lastIndexPath atScrollPosition:UICollectionViewScrollPositionLeft animated:NO];
[self.readingCollectionView reloadData];
A UI issue persists, however: when the view controller appears, it displays the first item, THEN immediately refreshes to display the last item instead.
To get around this seemingly unresolvable problem, I hacked the UI instead: display a "Loading" label before the view controller appears, and show the collection view in viewDidAppear.
Consider an standard, vertically scrolling flow layout populated with enough cells to cause scrolling. When scrolled to the bottom, if you delete an item such that the content size of the collection view must shrink to accommodate the new number of items (i.e. delete the last item on the bottom row), the row of cells that scroll in from the top are hidden. At the end of the deletion animation, the top row appears without animation - it's a very unpleasant effect.
In slow motion:
It's really simple to reproduce:
Create a new single view project and change the default ViewController to be a subclass of UICollectionViewController
Add a UICollectionViewController to the storyboard that uses a standard flow layout, and change its class to ViewController. Give the cell prototype the identifier "Cell" and a size of 200x200.
Add the following code to ViewController.m:
#interface ViewController ()
#property(nonatomic, assign) NSInteger numberOfItems;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.numberOfItems = 19;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return self.numberOfItems;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
return [collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
self.numberOfItems--;
[collectionView deleteItemsAtIndexPaths:#[indexPath]];
}
#end
Additional Info
I've seen other manifestations of this problem when dealing with collection views, it's just that the above example seems the simplest to demonstrate the issue. UICollectionView seems to go into some kind of paralysed state of panic during the default animations, and refuses to unhide certain cells until after the animation completes. It even prevents manual calls to cell.hidden = NO on hidden cells from having an effect (hidden is still YES afterwards). Dropping down to the underlying layer and setting hidden there works, provided you can get a reference to the cell you want to unhide, which is non-trivial when dealing with cells that haven't been displayed yet.
-initialLayoutAttributesForAppearingItemAtIndexPath is being called for every item visible at the time of the call to deleteItemsAtIndexPaths:, but not for the ones that are scrolled into view. It is possible work around the issue by calling reloadData inside a batch update block immediately afterwards, which appears to make the collection view realise that the top row is about to appear:
[collectionView deleteItemsAtIndexPaths:#[indexPath]];
[collectionView performBatchUpdates:^{
[collectionView reloadData];
} completion:nil];
But unfortunately this is not an option for me. I am trying to implement some custom animation timing by manipulating the cell layers & animations, and calling reloadData really throws things out of whack by causing unnecessary layout callbacks.
Update: A bit of investigation
I added log statements to a lot of layout methods and looked through some stack frames to try and find out what's going wrong. Crucially, I'm checking when layoutSubviews is called, when the collection view asks for layout attributes from the layout object (layoutAttributesForElementsInRect:) and when applyLayoutAttributes: is called on the cells.
I would expect to see a sequence of methods like this:
// user taps cell (to delete it)
-deleteItemsAtIndexPaths:
-layoutAttributesForElementsInRect:
-finalLayoutAttributes...: // Called for the item being deleted
-finalLayoutAttributes...: // \__ Called for each index path visible
-initialLayoutAttributes...: // / when deletion started
-applyLayoutAttributes: // Called for the item being deleted, to apply final layout attributes
// collection view begins scrolling up
-layoutSubviews: // Called multiple times as the
-layoutAttributesForElementsInRect: // collection view scrolls
// ... for any new set of
// ... attributes returned:
-collectionView:cellForItemAtIndexPath:
-applyLayoutAttributes: // Sets the standard attributes for the new cell
// collection view finishes scrolling
Most of this is happening; layout is correctly triggered as the view scrolls, and the collection view properly queries the layout for the attributes of cells to be displayed. However, collectionView:cellForItemAtIndexPath: and the corresponding applyLayoutAttributes: methods are not being called until after the deletion, when layout is invoked one last time causing the hidden cells to be assigned their layout attributes (sets hidden = NO).
So it seems that despite receiving all the correct responses from the layout object, the collection view has some kind of flag set to not update the cells during the update. There is a private method on UICollectionView called from within layoutSubviews that seems responsible for refreshing the cells' appearance: _updateVisibleCellsNow:. This is from where the data source eventually gets asked for a new cell before applying the cells starting attributes, and it seems this is the point of failure, as it is not being called when it should be.
Additionally, this does seem to be related to the update animation, or at least cells are not updated for the duration of the insertion/deletion. For example the following works without glitches:
- (void)addCell
{
NSIndexPath *indexPathToInsert = [NSIndexPath indexPathForItem:self.numberOfItems
inSection:0];
self.numberOfItems++;
[self.collectionView insertItemsAtIndexPaths:#[indexPathToInsert]];
[self.collectionView scrollToItemAtIndexPath:indexPathToInsert
atScrollPosition:UICollectionViewScrollPositionCenteredVertically
animated:YES];
}
If the above method is called to insert a cell while the inserted cell is outside the current visible bounds, the item is inserted without animation and the collection view scrolls to it, properly dequeuing and displaying cells on the way.
Problem occurs in iOS 7 & iOS 8 beta 5.
Adjust your content insets so that they go beyond the bounds of the device's screen size slightly.
collectionView.contentInsets = UIEdgeInsetsMake(-5,0,0,0); //Adjust this value until it looks ok
I'm currently trying both KIF and Subliminal for iOS integration testing. But I still cannot figure out how to simulate scrolling on table view or collection view using both frameworks. Like how to scroll to the bottom of table view or collection view.
EDIT 1:
I made simple single collection view app here https://github.com/nicnocquee/TestSubliminal
Basically I want to test the last cell's label. I cannot do
SLElement *lastLabel = [SLElement elementWithAccessibilityLabel:#"Label of the last cell"];
[lastLabel scrollToVisible];
because the label doesn't exist yet until the collection view is scrolled to the bottom.
EDIT 2:
I marked Aaron's answer as the answer. But Jeffrey's also works :)
You could also simulate the user scrolling through the collection looking for the cell, by dragging the collection view until the cell becomes visible:
while (!SLWaitUntilTrue([UIAElement(lastLabel) isValidAndVisible], 1.0)) {
[[SLWindow mainWindow] dragWithStartOffset:CGPointMake(0.5, 0.75) endOffset:CGPointMake(0.5, 0.25)];
}
Those offsets translate to dragging straight up along the middle of the collection view, from 75% down the view to 25% down the view. -isValidAndVisible lets you check for the cell's visibility without worrying about whether it exists yet (whereas -isVisible would throw an exception if the cell didn't exist). And I wrap -isValidAndVisible in SLWaitUntilTrue so that we let the collection view finish scrolling before dragging again.
In contrast to #AaronGolden's app hook solution, this approach requires you be able to identify a particular cell to scroll to. So I'd frame this approach as "scroll to a cell", whereas the app hook lets you "scroll to a position".
This is probably more invasive but so simple too -
Go to SLUIAElement.m and add the following methods:
- (void)scrollDown {
[self waitUntilTappable:NO thenSendMessage:#"scrollDown()"];
}
- (void)scrollUp {
[self waitUntilTappable:NO thenSendMessage:#"scrollUp()"];
}
You will also have to declare those method signature in the SLUIAElement.h file to make those new methods visible to the test suite.
Then what you can do is add an accessibility identifier to a collection view, call that identifier and scroll on it. EXAMPLE:
SLElement *scrollView = [SLElement elementWithAccessibilityIdentifier:#"scrollView"];
[scrollView scrollDown];
The issue is that the cell you're trying to find in your test case, the one with label "This is cell 19", does not exist until the collection view has already been scrolled. So we need to make the view scroll first and then look for the cell. The easiest way to make the collection view scroll with Subliminal is through an application hook. In (for example) your view controller's viewDidLoad method, you could register the view controller to respond to a particular message from any Subliminal test case, like so:
[[SLTestController sharedTestController] registerTarget:self forAction:#selector(scrollToBottom)];
and the view controller could implement that method as:
- (void)scrollToBottom {
[self.collectionView setContentOffset:CGPointMake(0.0, 1774.0)];
}
That 1774 is just the offset that happens to scroll the collection view in your test app all the way to the bottom. In a real application the app hook would probably be more sophisticated. (And in a real application you would want to make sure to call [[SLTestController sharedTestController] deregisterTarget:self] in your view controller's dealloc method.)
To trigger the scrollToBottom method from a Subliminal test case you can use:
[[SLTestController sharedTestController] sendAction:#selector(scrollToBottom)];
or the convenience macro:
SLAskApp(scrollToBottom);
The shared SLTestController will send the scrollToBottom message to the object that registered to receive it (your view controller).
When the sendAction or SLAskApp macro returns your cell 19 will already be visible, so you don't need to bother with the [lastLabel scrollToVisible] call anymore. Your complete test case could look like this:
- (void)testScrollingCase {
SLElement *label1 = [SLElement elementWithAccessibilityLabel:#"This is cell 0"];
SLAssertTrue([UIAElement(label1) isVisible], #"Cell 0 should be visible at this point");
SLElement *label5 = [SLElement elementWithAccessibilityLabel:#"This is cell 5"];
SLAssertFalse([UIAElement(label5) isValid], #"Cell 5 should not be visible at this point");
// Cause the collection view to scroll to the bottom.
SLAskApp(scrollToBottom);
SLElement *lastLabel = [SLElement elementWithAccessibilityLabel:#"This is cell 19"];
SLAssertTrue([UIAElement(lastLabel) isVisible], #"Last cell should be visible at this point");
}