kiwi-bdd / Kiwi

Simple BDD for iOS
BSD 3-Clause "New" or "Revised" License
4.14k stars 512 forks source link

Testing something like a UICollectionViewLayout with Kiwi? #261

Closed Goles closed 11 years ago

Goles commented 11 years ago

I need to test a UICollectionViewFlowLayout subclass with Kiwi,

I have correctly mocked up the delegate and the dataSource for a UICollectionView, but I'm still having some issues.

With the specified item size of CGSize(200.0f, 200.0f), I should be getting 5 items on the screen, but for some reason the attributes array that is returned at the last line does return 10 attributes, so that means that there are 10 visible cells.

What could be going on here? If my layout works as expected there's always 5 elements on the screen:

This is what I have so far (read the comments), and it mostly works.

    describe(@"LineLayout", ^{
        __block UICollectionView *collectionView;
        __block HICollectionViewLineLayout *layout;
        __block const CGRect windowFrame = CGRectMake(0.0f, 0.0f, 1024.0f, 768.0f);
        __block const CGSize itemSize = CGSizeMake(200.0f, 200.0f);

        // Create a Collection View that uses a LineLayout and set the datasource delegate
        // before each test
        beforeEach(^{
            layout = [[HICollectionViewLineLayout alloc] init];
            collectionView = [[UICollectionView alloc] initWithFrame:windowFrame collectionViewLayout:layout];

            // Mock the UILineLayout data source
            id delegateMock = [KWMock mockForProtocol:@protocol(UICollectionViewDelegateFlowLayout)];
            [[delegateMock should] conformToProtocol:@protocol(UICollectionViewDelegateFlowLayout)];
            [delegateMock stub:@selector(collectionView:layout:sizeForItemAtIndexPath:) andReturn:theValue(itemSize)];
            [delegateMock stub:@selector(collectionView:layout:insetForItemAtIndex:) andReturn:theValue(UIEdgeInsetsZero)];

            // Mock the UICollection View dataSource
            id dataSourceMock = [KWMock mockForProtocol:@protocol(UICollectionViewDataSource)];
            [[dataSourceMock should] conformToProtocol:@protocol(UICollectionViewDataSource)];
            [dataSourceMock stub:@selector(numberOfSectionsInCollectionView:) andReturn:theValue(1)];
            [dataSourceMock stub:@selector(collectionView:numberOfItemsInSection:) andReturn:theValue(10)];

            // Set the delegate and the data source
            collectionView.delegate = delegateMock;
            collectionView.dataSource = dataSourceMock;

            // Reload the collectionView Data
            [collectionView reloadData];
        });

        it(@"Should properly identify central element when cell number is not even", ^{
            NSArray *attributes = [layout layoutAttributesForElementsInRect:windowFrame];

            // test that [attributes count] == 5
        });

This is what I see when I just run the app with no tests (5 visible cells)

enter image description here

supermarin commented 11 years ago

If you change theValue(10) to theValue(11), does the counter stay the same? :)

[dataSourceMock stub:@selector(collectionView:numberOfItemsInSection:) andReturn:theValue(10)];
Goles commented 11 years ago

HILayoutAttributes is my own sub-class of UICollectionViewLayoutAttributes, I tested setting theValue(10) to 15 and the number remained the same... so I went to the extreme and did theValue(1500). At that point I did:

    NSArray *attributes = [layout layoutAttributesForElementsInRect:windowFrame];
    NSLog(@"%@", attributes);

And got the following output (of 18 items)

    "<HILayoutAttributes: 0x1753820> index path: (<NSIndexPath 0x1751aa0> 2 indexes [0, 0]); frame = (1.43053e-35 6.16571e-44; 200 200); ",
    "<HILayoutAttributes: 0x1753d90> index path: (<NSIndexPath 0x1752450> 2 indexes [0, 1]); frame = (1.43053e-35 285; 200 200); ",
    "<HILayoutAttributes: 0x1761d60> index path: (<NSIndexPath 0x1755a40> 2 indexes [0, 2]); frame = (1.43053e-35 569.999; 200 200); ",
    "<HILayoutAttributes: 0x1761c00> index path: (<NSIndexPath 0x1750bc0> 2 indexes [0, 3]); frame = (200 6.16571e-44; 200 200); ",
    "<HILayoutAttributes: 0x177f5d0> index path: (<NSIndexPath 0x17621f0> 2 indexes [0, 4]); frame = (200 285; 200 200); ",
    "<HILayoutAttributes: 0x1132b930> index path: (<NSIndexPath 0x176f9c0> 2 indexes [0, 5]); frame = (200 569.999; 200 200); ",
    "<HILayoutAttributes: 0x1132b9b0> index path: (<NSIndexPath 0x1776530> 2 indexes [0, 6]); frame = (371.8 -28.2; 256.4 256.4); transform = [1.282, 0, 0, 1.282, 0, 0]; zIndex = 1; ",
    "<HILayoutAttributes: 0x1132ba30> index path: (<NSIndexPath 0x1763740> 2 indexes [0, 7]); frame = (371.8 256.8; 256.4 256.4); transform = [1.282, 0, 0, 1.282, 0, 0]; zIndex = 1; ",
    "<HILayoutAttributes: 0x1132bab0> index path: (<NSIndexPath 0x1753910> 2 indexes [0, 8]); frame = (371.8 541.799; 256.4 256.4); transform = [1.282, 0, 0, 1.282, 0, 0]; zIndex = 1; ",
    "<HILayoutAttributes: 0x1132bb30> index path: (<NSIndexPath 0x1761920> 2 indexes [0, 9]); frame = (598.2 -1.8; 203.6 203.6); transform = [1.018, 0, 0, 1.018, 0, 0]; zIndex = 1; ",
    "<HILayoutAttributes: 0x1132b6d0> index path: (<NSIndexPath 0x1753470> 2 indexes [0, 10]); frame = (598.2 283.2; 203.6 203.6); transform = [1.018, 0, 0, 1.018, 0, 0]; zIndex = 1; ",
    "<HILayoutAttributes: 0x1132b650> index path: (<NSIndexPath 0x17623a0> 2 indexes [0, 11]); frame = (598.2 568.199; 203.6 203.6); transform = [1.018, 0, 0, 1.018, 0, 0]; zIndex = 1; ",
    "<HILayoutAttributes: 0x1132b5d0> index path: (<NSIndexPath 0x1788b40> 2 indexes [0, 12]); frame = (800 6.16571e-44; 200 200); ",
    "<HILayoutAttributes: 0x1132b550> index path: (<NSIndexPath 0x17553e0> 2 indexes [0, 13]); frame = (800 285; 200 200); ",
    "<HILayoutAttributes: 0x1132b4d0> index path: (<NSIndexPath 0x1753a90> 2 indexes [0, 14]); frame = (800 569.999; 200 200); ",
    "<HILayoutAttributes: 0x1132b450> index path: (<NSIndexPath 0x1753770> 2 indexes [0, 15]); frame = (1000 6.16571e-44; 200 200); ",
    "<HILayoutAttributes: 0x1132b3d0> index path: (<NSIndexPath 0x1754de0> 2 indexes [0, 16]); frame = (1000 285; 200 200); ",
    "<HILayoutAttributes: 0x1132b350> index path: (<NSIndexPath 0x1752da0> 2 indexes [0, 17]); frame = (1000 569.999; 200 200); "

It's very strange though, don't know what could be going on, there should be 5 items on the screen (like the above picture shows...) maybe I'm missing some method call in my CollectionView? I tried to do [collectionView reloadData] thinking that in that way my layout could somehow be applied, but no luck.

I could even send you the project if that could help you to see the issue :)

supermarin commented 11 years ago

you can just expose your sublclass here as a comment, maybe that could tell us something more helpful

Goles commented 11 years ago

@mneorr Here's my UICollectionViewFlowLayout SubClass

#import "HICollectionViewLineLayout.h"
#import "HILayoutAttributes.h"

static const NSInteger ITEM_SIZE = 200;
static const NSInteger INSET_HEIGHT_SIZE = 200;
static const CGFloat ACTIVE_DISTANCE = 200.0f;
static const CGFloat ZOOM_FACTOR = 0.3f;

@implementation HICollectionViewLineLayout

- (id)init
{
    if(self = [super init]) {
        self.itemSize = CGSizeMake(ITEM_SIZE, ITEM_SIZE);
        self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
        self.sectionInset = UIEdgeInsetsMake(200.0f, 0.0f, 200.0f, 0.0f);
        self.minimumLineSpacing = 50.0f;
    }

    return self;
}

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
    return YES;
}

+ (Class)layoutAttributesClass
{
    return [HILayoutAttributes class];
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
    CGRect visibleRect;
    visibleRect.origin = self.collectionView.contentOffset;
    visibleRect.size = self.collectionView.bounds.size;

    for (UICollectionViewLayoutAttributes *attribute in attributes) {
        HILayoutAttributes *atr = (HILayoutAttributes *) attribute;
        atr.isCenterCell = NO;
        if (CGRectIntersectsRect(attribute.frame, rect)) {
            [self setLineAttributes:attribute visibleRect:visibleRect];
        }
    }

    NSIndexPath *path = [self indexPathForCenterCellInRect:visibleRect];

    NSInteger centerCellIndex = [attributes indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
        HILayoutAttributes *atr = (HILayoutAttributes *)obj;
        if ([atr.indexPath isEqual:path]) {
            return YES;
        }

        return NO;
    }];

    if (centerCellIndex != NSNotFound) {
        ((HILayoutAttributes *)[attributes objectAtIndex:centerCellIndex]).isCenterCell = YES;
    }

    return attributes;
}

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{
    CGFloat offsetAdjustment = MAXFLOAT;
    CGFloat horizontalCenter = proposedContentOffset.x + (CGRectGetWidth(self.collectionView.bounds) / 2.0f);
    CGRect targetRect = CGRectMake(proposedContentOffset.x, 0.0f, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);

    // Find the element which is closest to the center and calculate the offset
    // To move that element to the center.
    NSArray *attributes = [super layoutAttributesForElementsInRect:targetRect];

    for (UICollectionViewLayoutAttributes *attribute in attributes) {
        if (attribute.representedElementCategory != UICollectionElementCategoryCell)
            continue; // skip headers

        CGFloat itemHorizontalCenter = attribute.center.x;
        CGFloat distanceToCenter = itemHorizontalCenter - horizontalCenter;

        if (ABS(distanceToCenter) < ABS(offsetAdjustment)) {
            offsetAdjustment = distanceToCenter;
        }
    }

    return CGPointMake(proposedContentOffset.x + offsetAdjustment, proposedContentOffset.y);
}

#pragma mark - Private

- (void)setLineAttributes:(UICollectionViewLayoutAttributes *)attributes visibleRect:(CGRect)visibleRect
{
    CGFloat distance = CGRectGetMidX(visibleRect) - attributes.center.x;
    CGFloat normalizedDistance = distance / ACTIVE_DISTANCE;

    // Apply a zoom factor to cells within ACTIVE_DISTANCE
    if (ABS(distance) < ACTIVE_DISTANCE) {
        CGFloat zoom = 1.0f + ZOOM_FACTOR * (1.0f - ABS(normalizedDistance));
        attributes.transform3D = CATransform3DMakeScale(zoom, zoom, 1.0);
        attributes.zIndex = round(zoom);
    }
}

- (NSIndexPath *)indexPathForCenterCellInRect:(CGRect)visibleRect
{
    CGFloat horizontalCenter = visibleRect.origin.x + (CGRectGetWidth(self.collectionView.bounds) / 2.0f);
    CGFloat offsetAdjustment = MAXFLOAT;

    UICollectionViewLayoutAttributes *previousAttribute = nil;
    NSArray *attributes = [super layoutAttributesForElementsInRect:visibleRect];

    for (UICollectionViewLayoutAttributes *atr in attributes) {
        CGFloat itemHorizontalCenter = atr.center.x;
        CGFloat distanceToCenter = itemHorizontalCenter - horizontalCenter;

        if (ABS(distanceToCenter) < ABS(offsetAdjustment)) {
            offsetAdjustment = distanceToCenter;
        } else {
            // This means that the previous index belongs to the center cell.
            NSIndexPath *path = (previousAttribute) ? previousAttribute.indexPath : atr.indexPath;
            return path;
        }

        previousAttribute = atr;
    }

    return nil;
}

- (void) setVisibleCellsCenterAttributes:(NSArray *)visibleAttributes withCenterIndex:(NSInteger)centralIndex
{
    NSInteger currentIndex = 0;
    for (UICollectionViewLayoutAttributes *atr in visibleAttributes) {
        HILayoutAttributes *lineAttributes = (HILayoutAttributes *) atr;
        if (currentIndex == centralIndex) {
            lineAttributes.isCenterCell = YES;
        } else {
            lineAttributes.isCenterCell = NO;
        }
        ++currentIndex;
    }
}

@end
supermarin commented 11 years ago

omg, i've totally forgot on this one. have you solved the issue?

Goles commented 11 years ago

not really!

I'm on vacations now though! Will resume my efforts around June 15th!!

cheers!

Sent from my iPhone

On 11 mai 2013, at 01:35, Marin Usalj notifications@github.com wrote:

omg, i've totally forgot on this one. have you solved the issue?

— Reply to this email directly or view it on GitHub.

supermarin commented 11 years ago

@Goles awesome, see you then :)

yas375 commented 11 years ago

I was curious about the problem and have created a dummy project :)

Here are some of my thoughts.

  1. Seem like you have to use nullMockForProtocol: instead of mockForProtocol. Especially for mock for delegate. Because without it I was getting errors like mock received unexpected message -collectionView:<UICollectionView: 0x9848000; frame = (0 0; 1024 768); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x916dc80>; layer = <CALayer: 0x9173680>; contentOffset: {0, 0}> collection view layout: <HICollectionViewLineLayout: 0x9187940> layout:<HICollectionViewLineLayout: 0x9187940> insetForSectionAtIndex:0.
  2. there is no need in [[delegateMock should] conformToProtocol:@protocol(UICollectionViewDelegateFlowLayout)]; and [[dataSourceMock should] conformToProtocol:@protocol(UICollectionViewDataSource)]; line. No need to test if KWMock class works. It's part of library and it's tested in Kiwi's tests. And the second mistake here is that you did test in beforeEach block. Tests should be inside tests (it).
  3. I added breakpoint to layoutAttributesForElementsInRect: and noticed that attributes have wrong frames. Seems like theirs origins were not set. Or probably I missed something in HILayoutAttributes? I don't know what you have implemented in that class.

2013-05-11_1227

In case you have the same problem in your project as well you can try to override setFrame: method of HILayoutAttributes and to see when it's called and with what values when you run the app. Probably you also need to mock some other methods from dataSource and/or delegate protocols, so attributes frame will be set correctly.

I didn't work with UICollectionViews yet, so maybe I missed something. But I hope something of that will help :)