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

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

Related

iOS 7 - Layout advice

I have the following layout in my view controller. I want to be able to scroll vertically with the header scrolling off the view and the UISegmentedControl sticking to the top of the view, beyond that the remaining scroll should be handled by the Collection View.
However I'm a bit confused as to what is the best approach to implemented this layout.
I tried a few implementations with mixed results:
UIScrollView with UICollectionView as subviews: UIScrollView as the parent view with the header, segmented control and collection views as child controls. The problem with this approach is that the nested scrolling does not seem to work correctly. To be able to scroll the UIScrollView the tap needs to be outside the CollectionView area otherwise only the CollectionView scrolls and the header and segmented control don't move.
Header and Segmented Control in Header cell: I tried another approach by using a single CollectionView. I added the header and Segmented Control as subviews of a single Header cell of the collection view. When the segmented control value was changed, I switch the data source property of the CollectionView to achieve the 3 views required for the collection view. Visually everything works perfectly. The only problem here is the race condition when switching quickly between first,second and third tabs. I load the data from a web service, if the web service takes time and is still loading the data and I quickly switch the tabs then I run into bugs where the data returned is for a different collection view than what is currently selected, a lot of out of order sync issues.
Update constant value for Autolayout Constraint: Another approach I tried is to change the constant value of the auto layout constraint applied to "Header" view. Then I added a gesture to the view controller's view to track the scroll, as the user scrolls vertically I adjust the constant of the auto layout constraint so that the "header" cell pops out of view. Again this doesn't seem to work that smoothly, but I suppose I can tweak it, but it seems sort of a hack.
Is there a better way to implement this layout?
#2 seems like a good solution — the scrolling gestures will be most consistent with what users expect, since it's all a single scroll view. (I agree that #3 sounds like a hack.) You can make the header "sticky" with some custom layout attributes.
The only problem here is the race condition when switching quickly between first, second and third tabs.
This is a common problem with asynchronous loading when views are being switched out (especially when you are loading data into individual cells, which are being reused as you scroll). It is important that upon receiving the data you always check whether the receiver is still expecting it; i.e., you should check the segmented control value before changing the backing data source. You could also:
Use separate data source objects for the different segments, having each one manage its own data fetching so they can't get mixed up.
Cancel the outstanding requests, if you can, when quickly switching tabs, to avoid unnecessary network requests.
Cache data to avoid re-fetching every time you switch tabs.
I think you want the same functionality that pinterest profile page have. To implement such functionality at easy way, you need to do following things.
Step 1 : Add UIView as tableHeaderView those who showing off while scrolling up.
self.tableHeaderView = yourView
Step 2 : Add UISegmentControl in section header view.
- (UITableViewHeaderFooterView *)headerViewForSection:(NSInteger)section{
return your_segmentcontrolView;
}
Step 3 : Add UICollectionView into first row of first section.
By implementing following way, you can got your desire functionality.
Hope this help you.
An alternative approach you could consider:
Use a UITableView to contain your UI
Create a UITableView, and set your header as the UITableView's headerView.
Use a sectionHeader to contain the segmentedControl.
Place your collectionView inside of a single UITableViewCell. Or alternatively, you may be able to use the UITableView's footerView to contain the gridView.
By using the sectionHeader, this should allow the header to scroll out of view, but then the sectionHeader will stick below the navigationBar or top of the contentView until another section comes into view (and in your case you will only have one section.)
Add Header View, Body View (Holding Segment View & Collection View) into scroll view.
Initially set userInteractionEnabled property to "NO" for collection view.
Track the insect of scroll view always.
If the y-coordinate of the scrolled insect is more than the height of header view, then set userInteractionEnabled property to "YES" so that thereafter collection view can be scrolled.
If user scroll outside the scroll view and try to bring the header view down, i.e Scroll view y-coordinate insect is less than the height of header view, then immediately change the user iteration mode of collection view and allow user to scroll the scroll view till the top.
Rather than implementing this by hand, you could use a library/cocoapod to set this up for you. This one looks like a pretty good fit: https://github.com/iosengineer/BMFloatingHeaderCollectionViewLayout
Plus, the code is open-source, so you can always modify as needed.
All I can say is that you need to subclass UIView and make it a delegate of UIGestureRecognizerDelegate and UICollectionViewDelegate, then in your UIView subclass, do the following, I can't give out anymore information on this because the code, although owned by myself, is proprietary to the point of probably enraging quite a few organizations that I've used this for, so here's the secret sauce:
CGPoint contentOffset = [scrollView contentOffset];
CGFloat newHeight = [_headerView maxHeight] - contentOffset.y;
CGRect frame = [_headerView frame];
if (newHeight > [_headerView maxHeight]) {
frame.origin.y = contentOffset.y;
frame.size.height = [_headerView maxHeight];
[_headerView setFrame:frame];
} else if (newHeight < [_headerView minHeight]) {
frame.origin.y = contentOffset.y;
frame.size.height = [_headerView minHeight];
[_headerView setFrame:frame];
} else {
frame.origin.y = contentOffset.y;
frame.size.height = newHeight;
[_headerView setFrame:frame];
}
if ([_delegate respondsToSelector:#selector(scrollViewDidScroll:)]) {
return [_delegate scrollViewDidScroll:scrollView];
}
You must subclass another UIView that is defined as the header for this custom UiCollectionView. Then, you must declare the UIView custom header view inside the custom subview of the UIView/UICollectionView delegate, and then set the header of that custom subview inside the UICollctionViewdelegate. You should then pull in this compounded subclass of UIView/UIcollectionView into your UIViewController. Oh yes, and in your layoutSubViews, make sure you do the height calculations that are passed through a double layered subclass. So, you will have the following files:
UIVew this is the delegate of UICollectionView and what I mentioned before
UIView this is a UISCrollViewDelegate and this is the header view
UIViewController that pulls in the subclassed UIView in number 1
UIView subclass of number 1 that pulls in number 2 and sets it as its header
In the number 4 part, make sure you do something like this:
- (CGFloat)maxHeight
{
if (SCREEN_WIDTH == 414)
{
return 260;
}else if (SCREEN_WIDTH == 375)
{
return 325;
}else
{
return 290;
}
}
- (CGFloat)minHeight
{
if (SCREEN_WIDTH == 414)
{
return 90;
}else if (SCREEN_WIDTH == 375)
{
return 325;
}else
{
return 290;
}
}
This will then pass through to the UIView subclass that is a compounded subclass as I already explained. The idea is to capture the maxHeight of you header in the subclass of this header UIView (number 2 above), and then pass this into the main UIView subclass that intercepts these values in the scrollViewDidScroll.
Last tidbit of information, make sure you set up your layoutSubviews in all methods to intercept scroll events. For example in number 1 above, the layoutsubviews method is this:
- (void)layoutSubviews
{
CGRect frame = [_headerView frame];
frame.size.width = [self frame].size.width;
[_headerView setFrame:frame];
[super layoutSubviews];
}
This is all I can give you, I wish I could post more, but this should give you an idea of how it's done in production environments for the big time apps you see out in the wild.
One more thing to note. When you start going down the road of intense implementations like this, don't be surprised to learn that, for example, a single view controller in an app that works with methods like I've explained will have anywhere from 30-40 custom subclasses that are either subclasses in their own right or compounded subclasses or subclasses of my own subclasses or my own subclasses. I'm telling you this so you get an idea of how much code is required to get this right, not to scare you, but to let you know that it might take a while to get right, and to not kick yourself in the butt if it takes awhile to make work. Good luck!!

How can I detect when a UITableView has been scroll past the very bottom/top?

I've got a view that has 3 tableView's. One is the "Main Table View", and then I have an 'Answers Table View' and 'Percentage Table View'.
When the screens loads, the Main Table View occupies the top 95% of the screen. The bottom of the screen is a UIView containing 2 buttons. "Answers" and "Percentage".
The way it works, is if I click "Percentage" it changes the height of the Main Table View to 0, and gives that height to the Answers Table View. This animates the "Answers/Percentage" View to the top, and reveals either the Answers or Percentage TableView below it.
Here's an example:
As you can see, I click on "Percentage" which animates it up. If you click on "Percentage" again it animates it back down.
However, what I want to do is if the "Answers/Percentage" view is at the bottom of the View, and the user scrolls the Main Feed UP reaches the very end of the tableView's contents (not just the end, but the end and a little bit more), I want to animate it up like in the .gif.
Similarly, if the "Answers/Percentage" is at the top, and the user scrolls the lower "Answers Table View" down past a certain point where there is no more data above, it will completely animate.
Also, I do not ever want the "Answers/Percentage" view to be in the middle, and showing a tableview both top and bottom. All one, or the other, but not a bit of both. Which I have right now.
What I need to know is... how can I detect if the user has scrolled past the very top or very bottom of the table view +30 pixels for example, to initiate my animation?
You can use the contentOffset property of the table view.
if(tableView.contentOffset.y >= (tableView.contentSize.height - tableView.frame.size.height)) {
// Start the animation
}
I haven't tested this, but let me know if it works.
Making another answer based on sublimepremise's approach because it's easier to format and post code that way. The base idea is to check the table view's .contentOffset.y, e.g. by implementing scrollViewDidScroll in its delegate, and triggering your animations accordingly.
It may have a bit of a QnD feel to it, but if you need to also abort the user's dragging action when triggering the animation, an easy way to do so would be by "resetting" the table view's gesture recognizer. In code that could look something like this:
static const CGFloat kChrisTableViewAnimationThreshold = 30.0f;
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == self.topTableView) {
if([self scrolledPastBottomThresholdInTableView:self.topTableView]) {
// Start the animation
// ...
// Toggling the table view's pangGestureRecognizer off and on cancels the gesture
self.topTableView.panGestureRecognizer.enabled = NO;
self.topTableView.panGestureRecognizer.enabled = YES;
}
}
}
- (BOOL)scrolledPastBottomThresholdInTableView:(UITableView *)tableView {
return (tableView.contentOffset.y - kChrisTableViewAnimationThreshold >= (tableView.contentSize.height - tableView.frame.size.height));
}
Note that this illustration obviously only covers recognizing when the top table view is scrolled past its bottom end, but adapting it for handling other scenarios should be pretty straightforward.

Scroll second UITableView in line with second UITableView

I have two table views set up side by side, and I need them to scroll at exactly the same time. So, when you scroll one, the other one will scroll at the same time.
I did some searching and I couldn't find any information, but I assume it must be possible somehow.
My table views are both connected to the same class and I differentiate between them like this:
if tableView == tableView1 {
//
} else if tableView = tableView2 {
//
}
You can get set the scrollView delegate to self on both of your tableView's scrollViews. And in -scrollViewDidScroll, take the contentOffset and set the other scrollView's contentOffset to the same value.
Like Schemetrical said you should use scrollViewDidScroll.
see the first answer of this:
Scrolling two UITableViews together
- (void)scrollViewDidScroll:(UIScrollView *)scrollView;
{
UITableView *slaveTable = nil;
if (self.table1 == scrollView) {
slaveTable = self.table2;
} else if (self.table2 == scrollView) {
slaveTable = self.table1;
}
[slaveTable setContentOffset:scrollView.contentOffset];
}
If you want them to scroll in prefect lock-step then this isn't a trivial problem. UITableView is a subclass of UIScrollview, so you could probably create a custom subclass of UITableView that overrode various UIScrollView methods and when something caused the table view to scroll, it would do the same thing to the other table view.
Edit: #Schemetrical's suggestion of using the scroll view delegate is cleaner than creating subclasses. You might have to monitor quite a few of the scroll view delegate methods and use them to match the behavior in the other scroll view.
EDIT #2:
Apparently I'm wrong and scrollViewDidScroll is called for every change in the scroll view, so it's simpler than I thought to keep them synced. I'm going to leave my answer for context even though I was wrong.

scrollToItemAtIndexPath destroys UICollectionViewCell layout

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.

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)

Resources