scrollToItemAtIndexPath destroys UICollectionViewCell layout - ios

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.

Related

Getting collection view cells to update after a constraint change

I'm having a problem with auto layout in a collection view cell in iOS 9.
During the collection view's :cellForItemAtIndexPath: I change a constraint:
[[self rootStack] setDistribution:UIStackViewDistributionFillEqually];
or
[[self rootStack] setDistribution:UIStackViewDistributionFillProportionally];
During the cell's preferredLayoutAttributesFittingAttributes: I try to force an additional round of layout:
- (UICollectionViewLayoutAttributes*) preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
[self setNeedsLayout];
[self layoutIfNeeded];
return layoutAttributes;
}
Still, when cells are displayed, some show the effect of the constraint change and some do not. If I scroll a cell that does not display the change offscreen and then scroll back to it, it then displays the change. Moving the constraint changes to the cell's updateConstraints resulted in none of the cells showing the change on first display.
I can fix this for the first cells that are displayed by calling reloadData on the collection view during the collection view controller's viewWillAppear:. But once I start scrolling some of the cells that come into view are not formatted correctly (unless I scroll past them and then scroll back to them).
Is there a way I can make sure that all cells are formatted correctly the first time they are displayed?

Is it possible to scroll to a particular subview inside a container view

I have a container view created in a xib, I have added various textfields to that container view, In a case when the user reaches that view controller I want the container view to automatically scroll to a particular textfield and focus on it.
Note that I am not using scroll view as of and I am trying to know is there a way to do it without using UIScrollView
There is no ability to make a UIView scroll in of itself. I would recommend using either a scroll view or a table view. If you decide to use a table view, you would have your textfields in individual cells, then you could do something like this to scroll:
//Scroll to 4th text field
NSIndexPath *indexToScrollTo = [NSIndexPath indexPathForRow:3 inSection:0];
[self.tableView scrollToRowAtIndexPath:indexToScrollTo
atScrollPosition:UITableViewScrollPositionTop
animated:YES];
In a scroll view, you would set the content offset to scroll to a particular y position. Here's an examle:
//Scroll down by 50
self.scrollView.contentOffset = CGPointMake(0,50);
yes,you can do it by using TableViewController. it automatically focus on the text fields and automatically scroll up when keyboard appears.

How to simulate table view or collection view scrolling using Subliminal?

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");
}

UICollectionView scroll right to left (reverse)

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)

Dynamically sized table in the cell of another table not resizing when reshowing view

I have a UITableView of cells where one cell contains a UITableView. Selecting that row that contains the table pushes a new view onto the screen with a list of items. The user selects a list of items and then presses 'back' to pop that view. The parent view looks at the number of items selected and the height of the cell is supposed to adjust to show all items selected.
What happens is that when the view is reshown, the listed items extends into the cell below. If I scroll that cell off the screen, then back to it, the cell is then the right height showing all items correctly.
I've tried several things such as putting code into viewWillAppear of the main table like [self.view setNeedsDisplay] and [self.view setNeedsLayout], but it doesn't work.
I can't seem to find a way to make that cell redraw to it's right size without scrolling the table once the view appears.
Is there some other method to force the redraw before it actually appears?
try:
NSIndexPath *theIndexPath = [NSIndexPath indexPathForRow:cellRow inSection:cellSection];
NSArray *theIndexPaths = [NSArray arrayWithObject:theIndexPath];
[table reloadRowsAtIndexPaths:theIndexPaths withRowAnimation:NO];
What are you currently using to redraw the cell? Just waiting for it to redraw itself when it goes off screen and back on? Or are you using something like [table reloadData];?

Resources