Adoption Curve Dot Net

Testing for Cowards Part 2: Testing User Interfaces

| Comments

Introduction to part 2

This is the second of three posts (part 1 | part 3) that works thorough the presentation I gave at September’s iOSDevUK conference in Aberystwyth. In the first, I covered the background to test-driven development of the simple traffic lights project I’m using as an example; and looked at building the app’s model layer using a test-driven approach.

The code and tests can be cloned or downloaded from GitHub: https://github.com/timd/TrafficLightTests.

Setting up the user interface for testing

Once the model works, I switch attention to the user interface (specifically, the user interactions). This is where the perception of iOS testing as difficult often arises – how do you test something that relies on a real live user touching something?

The answer is to think of the interaction as involving two layers – there’s the view layer, which the user touches; and the view controller which reacts to the touches with code. So you’re not interested in testing the touch itself – what you’re actually testing is the IBAction method behind the scenes

If you’re happy to assume that the UI controls are actually linked to the underlying method (and you can test that the connections are made correctly if you want to) then the testing process is simply a case of making sure that your didTapSomeButton method does what it should do when the someButton gets tapped.

The other UI testing question that crops up is how you can check the status of interface controls – what colour is the background of a UIView for example?

The reason this becomes an issue is that IBOutlets are normally declared in the view controller’s implementation file; and are encapsulated away from the view of the test.

You could declare them in the header file, but that would break encapsulation and just feels a bit wrong – working code shouldn’t have to change for the sake of tests. The workaround is to declare all the private properties and methods that your test will need access to in a category on your view controller at the top of the test. So the top of the UITests file looks like:

#import "Kiwi.h"
#import "ViewController.h"
#import "AppDelegate.h"
#import "LightEngine.h"

@interface ViewController (UITests)
@property (nonatomic, weak) id delegate;
@property (weak, nonatomic) IBOutlet UIView *upRed;
@property (weak, nonatomic) IBOutlet UIView *upAmber;
@property (weak, nonatomic) IBOutlet UIView *upGreen;
@property (weak, nonatomic) IBOutlet UIView *downRed;
@property (weak, nonatomic) IBOutlet UIView *downAmber;
@property (weak, nonatomic) IBOutlet UIView *downGreen;
@property (weak, nonatomic) IBOutlet UIImageView *stopImageview;
@property (weak, nonatomic) IBOutlet UIButton *startButton;
@property (weak, nonatomic) IBOutlet UIButton *tickButton;
@property (weak, nonatomic) IBOutlet UIButton *stopButton;
@property (nonatomic, strong) NSArray *upLights;
@property (nonatomic, strong) NSArray *downLights;
- (IBAction)didTapStartButton:(id)sender;
- (IBAction)didTapTickButton:(id)sender;
- (IBAction)didTapStopButton:(id)sender;
@end

SPEC_BEGIN(UITests)

// Tests start here

SPEC_END

Having gained access to all the outlets and methods for testing without needing to expose them to other classes, there’s one other step to make before the UI can be accessed by your tests.

A live view controller uses the initWithNibName:bundle: method to load the xib file, and loadView to trigger the three view creation methods – viewDidLoad, viewWillAppear: and viewDidAppear:

The loadView method won’t get triggered in a test – but you can force it by accessing the view property of the viewController with:

[myViewController view];

As soon as that property is accessed, the view lifecycle methods are triggered and all the outlets will be connected. The full code at the top of the test looks like:

 __block ViewController *vc = nil;

    beforeEach(^{
        vc = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];
        [vc view];
    });

Testing delegates

The LightEngine object is connected to the view controller as a delegate, so testing that this is correctly wired up is an important step:

context(@"when instantiated", ^{

    it(@"should have a delegate property", ^{
        [[vc should] respondToSelector:@selector(delegate)];
    });

    it(@"should have the delegate set", ^{
        AppDelegate *appDelegate = [[AppDelegate alloc] init];
        [appDelegate application:nil didFinishLaunchingWithOptions:nil];
        [[(NSObject *)appDelegate.viewController.delegate should] conformToProtocol:@protocol(LightEngineProtocol)];
    });

});

This is pretty straight-forward – we instantiate an instance of appDelegate then fire the application:didFinishLaunchingWithOptions: method which is where the LightEngine is instantiated along with the view controller.

There’s one bit of Kiwi-specific wierdness – it’s necessary to get cast the appDelegate to an NSObject to get the test to compile for some reason that I’ve yet to fathom.

Testing the user interface

Assuming that you’ve got the user interface into a state where the tests can get at it, you can then start testing it. My approach is to start with the initial default state and test that everything is where I expect it to be – this can be useful if you want the UI to load with certain controls disabled, for example. In this case, I want the lights to be black rather than the default gray, and the buttons in the correct state:

context(@"when in the default state", ^{

    it(@"should show all lights as black", ^{
        [[vc.upRed.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.upAmber.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.upGreen.backgroundColor should] equal:[UIColor blackColor]];

        [[vc.downRed.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.downAmber.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.downGreen.backgroundColor should] equal:[UIColor blackColor]];
    });

    it(@"should show the tick and stop buttons as greyed out", ^{
        [[theValue(vc.tickButton.alpha) should] equal:theValue(0.5f)];
        [[theValue(vc.stopButton.alpha) should] equal:theValue(0.5f)];
    });

    it(@"should show the start button as active", ^{
        [[theValue(vc.startButton.alpha) should] equal:theValue(1.0f)];
    });

});

As you can see from the tests above, the view controller exposes the UIView properties and buttons, which makes testing their states very easy.

Once the default state is tested and working, then it’s a case of doing the same for each of the user interactions. First, the start button:

describe(@"and handling the start button", ^{

    it(@"should turn both red lights on in response to the start button", ^{
        [vc didTapStartButton:nil];
        [[vc.upRed.backgroundColor should] equal:[UIColor redColor]];
        [[vc.upAmber.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.upGreen.backgroundColor should] equal:[UIColor blackColor]];

        [[vc.downRed.backgroundColor should] equal:[UIColor redColor]];
        [[vc.downAmber.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.downGreen.backgroundColor should] equal:[UIColor blackColor]];
    });

    it(@"should lock the 'tick' button until the start button has been tapped", ^{
        [[theValue(vc.tickButton.enabled) should] equal:theValue(NO)];
        [vc didTapStartButton:nil];
        [[theValue(vc.tickButton.enabled) should] equal:theValue(YES)];
    });

    it(@"should unlock the 'stop' button after the start button has been tapped", ^{
        [[theValue(vc.stopButton.enabled) should] equal:theValue(NO)];
        [vc didTapStartButton:nil];
        [[theValue(vc.stopButton.enabled) should] equal:theValue(YES)];
    });

    it(@"should show the stop and tick buttons as active after the start button has been tapped", ^{
        [vc didTapStartButton:nil];
        [[theValue(vc.stopButton.alpha) should] equal:theValue(1.0f)];
        [[theValue(vc.tickButton.alpha) should] equal:theValue(1.0f)];
    });

    it(@"should show the start button as inactive after the start button has been tapped", ^{
        [vc didTapStartButton:nil];
        [[theValue(vc.startButton.alpha) should] equal:theValue(0.5f)];
    });

});

When things get to the tick button, we can change approach slightly. The button causes a tick method to be sent to the delegate, which is an ideal place to use a mock object.

All we’re testing here is that the correct message is sent by the view controller – we’ve already tested what the delegate (in this case the LightEngine) will do. So there’s not really any point in going to the effort of creating a real, live LightEngine instance when we can use a mock object instead.

The mock will stand in for the LightEngine much as a stunt double will dive through the plate glass window instead of the movie star with the main billing. So long as the mock looks and behaves like a LightEngine, the view controller will happily accept it as such. This is often referred to as ‘duck typing’ – if it looks like a duck, walks like a duck and quacks like a duck, for our purposes it probably is a duck.

Here’s the code to do this:

describe(@"and handling the tick button", ^{

    it(@"should send a 'tick' message to the delegate when the Tick button is tapped", ^{
        id delegateMock = [KWMock mockForProtocol:@protocol(LightEngineProtocol)];
        [[delegateMock should] conformToProtocol:@protocol(LightEngineProtocol)];
        [vc setDelegate:delegateMock];
        [[delegateMock should] receive:@selector(tick) andReturn:@164];
        [vc didTapTickButton:nil];
    });

    it(@"should show the tick button as active after each tick", ^{
        [vc didTapStartButton:nil];
        [[theValue(vc.tickButton.alpha) should] equal:theValue(1.0f)];
        [vc didTapTickButton:nil];
        [[theValue(vc.tickButton.alpha) should] equal:theValue(1.0f)];
    });

});

Here we’re created an instance of the KWMock class and telling it to pretend to conform to the LightEngineProtocol. There’s a quick test to make sure that the mock has been created properly, and then the mock is set as the delegate of the view controller.

Then we set an expectation – that the delegateMock will receive a tick message, and we tell it to return @164 when it does. That expectation set, we can then call the didTapTickButton method. If the test gets to the end and the mock hasn’t received the tick message, the test will fail.

At this point, the combination of LightEngine and UI tests will have verified that all aspects of the tick process are working correctly. Next is handling the stop button:

describe(@"and handling the stop button", ^{

    beforeEach(^{
        [vc didTapStartButton:nil];
        [[theValue(vc.tickButton.enabled) should] beTrue];
        [[theValue(vc.stopButton.enabled) should] beTrue];

        [vc didTapStopButton:nil];
    });

    it(@"should send the stopSequence message to the delegate when tapping the stop button", ^{
        id delegateMock = [KWMock mockForProtocol:@protocol(LightEngineProtocol)];
        [[delegateMock should] conformToProtocol:@protocol(LightEngineProtocol)];
        [vc setDelegate:delegateMock];
        [[delegateMock should] receive:@selector(stopSequence)];
        [vc didTapStopButton:nil];
    });

    it(@"should turn all lights black in response to the stop button", ^{
        [[vc.upRed.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.upAmber.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.upGreen.backgroundColor should] equal:[UIColor blackColor]];

        [[vc.downRed.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.downAmber.backgroundColor should] equal:[UIColor blackColor]];
        [[vc.downGreen.backgroundColor should] equal:[UIColor blackColor]];
    });

    it(@"should lock the tick and stop buttons after tapping the stop button", ^{
        [[theValue(vc.tickButton.enabled) should] beFalse];
        [[theValue(vc.stopButton.enabled) should] beFalse];
    });

    it(@"should unlock the start button after tapping the stop button", ^{
        [[theValue(vc.startButton.enabled) should] beTrue];
    });

    it(@"should show the tick and stop buttons as inactive", ^{
        [[theValue(vc.tickButton.alpha) should] equal:theValue(0.5f)];
        [[theValue(vc.stopButton.alpha) should] equal:theValue(0.5f)];
    });

    it(@"should show the start button as active", ^{
        [[theValue(vc.startButton.alpha) should] equal:theValue(1.0f)];
    });

});

This set of tests is very similar to those which came before – the beforeEach block is where the stop button is tapped, and the tests are checking that the correct messages are sent; the lights change colour correctly, and the buttons get updated.

Testing the user interface

In the next post, I’ll cover testing the methods that update the user interface in response to the LightEngine codes.

http://adoptioncurve.net/archives/2013/09/testing-for-cowards-part-3-testing-the-full-interface/

A Cowardly Test-o-phobe’s Presentation From iOSDevUK

| Comments

Last week I gave a presentation at iOSDevUK in Aberystwyth on test-driven development for iOS. This is the first of three posts that go through the presentation itself and some of the background, together with links to other resources. ( Part 2 | Part 3 )

It sometimes seems to me that unit testing on iOS has been a poor relation ever since the framework was released and having talked to some ex-Apple people one evening, it seems that unit testing hasn’t been an approach that Apple have used much internally. This might go some way to explaining the relatively poor state of the tools compared to say, the Rails world.

That said, with a combination of the Apple tools that are available and those created by the iOS community, unit testing and test-driven development are both possible and feasible.

It’s my contention that test-driven development delivers better-quality code, so the purpose of the talk was to try to show that building apps with this approach is something that can and should be done.

Having said that, I don’t have a lot of time for the quasi-religious waffle that tends to characterise discussion of test-driven development. Like any technical topic, there are those who lose sight of the end goal in the search for the “one true way” of doing things. I get paid by the delivered project, so I’m more pragmatic – if it helps me build bug-free code quicker, then I’ll live with the lack of ideological purity.

Bearing that in mind, what follows is here is my take on things and other opinions are available. This is the first of three posts which walks through the presentation code that can be downloaded or cloned from GitHub: https://github.com/timd/TrafficLightTests It’s not a word-for-word transcript of what I said, but this in combination with the test code itself should be clear enough.

The project

The demo project is an iPhone project that runs a simulation of a set of traffic lights running the UK sequence – red; red and amber; green; amber; and back to red. There are two sets of lights – one controlling upstream traffic; and the other controlling downstream.

The app launches with all lights off; tapping the start button launches the sequence with two red lights. From there, tapping the tick button toggles each set of lights through the sequence in turn before repeating. Tapping the stop button at any time reset the sequence back to two reds.

Before the sequence is started by tapping start, the tick and stop buttons are disabled. Once the sequence is in progress, the start button is disabled, and tick and stop are enabled.

The app structure

The app has a model-view-controller structure, and uses a state machine to control the state of the lights as they run through the sequence. The LightEngine class acts as a delegate to the ViewController and has a single tick method that returns an NSNumber light code. That’s a decimal representation of the state of the lights at each stage of the sequence, together with a flag to show whether the traffic is flowing upstream or downstream.

The ViewController class uses the code to control the state of the lights, by making a bitwise comparison of the code’s binary value and toggling each light in turn. It also responds to the start and stop buttons by resetting the lights, LightEngine and button enablement.

The source code

All the source code (including the tests) can be downloaded or cloned from GitHub: https://github.com/timd/TrafficLightTests

The testing approach

I split the talk into two sections – the first was a quick demo of how to add a test target to a project; and then install the Kiwi framework to replace the default SenTest. I’ll cover those steps in a separate, later post.

The second section looked at building the app from scratch using a test-driven approach. To do to this in 40 minutes while typing frantically was a bit of a tall order, so I cheated slightly by using Xcode snippets to store the code rather than typing; and Git to flip between the project in various states.

The structure of the testing covered three areas:

  • the LightEngine model, which returns the code for the lights at each step in the sequence
  • the user interface – wiring up the buttons to call the right methods, and control the initial state of the lights
  • the lights themselves – taking the code returned from the LightEngine and updating the display accordingly.

That’s by no means the only approach that could be used; but it makes logical sense to me to start from the inside-out by building the model; getting the model to respond to user input; then fully-updating the interface.

Testing the model

The model is a standalone NSObject subclass that has one role in life – to act as a state machine that steps through each permutation of lights in turn, and respond with a code to a tick method. It’s linked to the view controller as the delegate – this isn’t necessarily an architecture that you’d use in real life, but it was a handy way of demonstrating testing of delegates.

“Classical” TDD starts with a blinking cursor in an empty text file, and works from there. That’s not possible with an Xcode project, because the compiler will complain about missing classes – so the first step is to test that the object can be instantiated. Also included in that batch are tests for the existence of the tick method, and that an NSNumber is returned:

context(@"when instantiated", ^{

    it(@"should exist", ^{
        [[engine shouldNot] beNil];
    });

    it(@"should respond to the 'tick' message", ^{
        [[engine should] respondToSelector:@selector(tick)];
    });

    it(@"should return an NSNumber in response to a 'tick' message", ^{
        id result = [engine tick];
        [[result should] beKindOfClass:[NSNumber class]];
    });

});

Next, the tick method is tested to ensure that it’ll handle keeping track of the position in the state machine sequence:

describe(@"and handling the tick count", ^{

    it(@"should have a tickCount property", ^{
        [[engine should] respondToSelector:@selector(tickCount)];
    });

    it(@"should increment the tickCount property in response to a tick", ^{
        [[theValue(engine.tickCount) should] equal:theValue(0)];
        [engine tick];
        [[theValue(engine.tickCount) should] equal:theValue(1)];
        [engine tick];
        [[theValue(engine.tickCount) should] equal:theValue(2)];
    });

    it(@"should roll the tickCount over to 0 when it reaches 7", ^{
        [[theValue(engine.tickCount) should] equal:theValue(0)];
        for(int count=0; count<7; count++) {
            [engine tick];
        }
        [[theValue(engine.tickCount) should] equal:theValue(7)];
        [engine tick];
        [[theValue(engine.tickCount) should] equal:theValue(0)];
    });

});

And finally that the tick method will return the right values at the right point in the sequence:

describe(@"and handling tick responses", ^{

    it(@"should return @164 in response to the first tick", ^{
        NSNumber *result = [engine tick];
        [[result should] equal:@164];
    });

    it(@"should return @180 in response to the second tick", ^{
        [engine tick];
        NSNumber *secondResult = [engine tick];
        [[secondResult should] equal:@180];
    });

    NSArray *expectedValues = @[@164,@180,@140,@148,@100,@102,@97,@98,
                                @164,@180,@140,@148,@100,@102,@97,@98,
                                @164,@180,@140,@148,@100,@102,@97,@98];

    it(@"should return the correct values when ticking repeatedly", ^{

        for (NSNumber *value in expectedValues) {
            NSNumber *returnValue = [engine tick];
            [[returnValue should] equal:value];
        }

    });

});

The final set of tests is to check that the model will handle being brought to a halt:

context(@"when stopping", ^{

    it(@"should respond to the stop message", ^{
        [[engine should] respondToSelector:@selector(stopSequence)];
    });

    it(@"should reset the tickCount back to zero in response to a stop message", ^{
        [[theValue([engine tickCount]) should] equal:theValue(0)];
        [engine tick];
        [[theValue([engine tickCount]) should] equal:theValue(1)];
        [engine stopSequence];
        [[theValue([engine tickCount]) should] equal:theValue(0)];
    });

});

Testing the user interface

In the next post, I’ll cover setting up the user interface for testing; and step through how to test user interaction.

http://adoptioncurve.net/archives/2013/09/testing-for-cowards-part-2-testing-user-interfaces/

Changing the Airport MAC Address With OS X

| Comments

If you walk past a fancy-looking bin in the City of London, there’s a chance it may have recorded details of any wifi-enabled devices you happen to be carrying.

Tom Taylor figured out the way that this was being done – the bins are recording the MAC address of the wifi interface, and pointed out that it’s possible to change this if you’re running OS X.

The chances of a bin sniffing the address of a Macbook are fairly slim unless you happen to be using it within wifi range – but there are situations where it’s actually useful to change the address. Tom’s example was reconnecting to time-limited public wifi, but there are plenty of others.

It’s not tricky to do, just fiddly – so here’s a quick-and-dirty script that changes the MAC address of a Macbook’s built-in Airport adaptor to a random value (it assumes that the Airport adaptor is known by the system as eth0, but I’ve never come across a Macbook where that wasn’t the case.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash

mac=`openssl rand -hex 6 | sed 's/\(..\)/\1:/g; s/.$//'`
echo "Changing Wifi MAC address to $mac"

ifconfig en0 ether $mac

echo "Turning Airport off..."
/usr/sbin/networksetup -setairportpower en0 off

sleep 5

echo "Turning Airport on..."
/usr/sbin/networksetup -setairportpower en0 on

sleep 5
newmac=`ifconfig en0 ether | grep '[a-f0-9][a-f0-9]:[a-f0-9][a-f0-9]:[a-f0-9][a-f0-9]:[a-f0-9][a-f0-9]:[a-f0-9][a-f0-9]:[a-f0-9][a-f0-9]'`
newip=`ifconfig en0 inet`

echo "Airport MAC address: $newmac"
echo "Airport IP address: $newip"

To use this, create a text file called macChange.sh or similar (the .sh extension is the important bit) and then make it executable with chmod +x macChange.sh

You can then run it with sudo ./macChange.sh any time you want to alter the MAC address – the script changes it, then bounces the Airport adaptor to make sure the change takes effect.

Batch Resizing of Images With Mogrify

| Comments

An outboard brain post.

To resize all images in a set of subdirectories:

1
find ../ -name "*.jpg" -exec mogrify -resize 192x192 {} \;

To check which files this will impact, you can run it non-destructively by placing echo in front of the mogrify command:

1
find ../ -name "*.jpg" -exec mogrify echo -resize 192x192 {} \;

An Automater workflow to append text onto files in a set of subdirectories:

Building a Infinitely-Scrolling Gallery With a UICollectionView

| Comments

A common requirement for a photo gallery or similar is endless scrolling – once you’ve reached the end of the list of items, the list “wraps around” and begins again from the first item. UICollectionViews very easily lend themselves to creating galleries, but there’s no in-built mechanism for endless scrolling.

This is a technique that can create the illusion of endless scrolling with relatively little effort. It uses UICollectionView’s scrollToItemAtIndexPath:atScrollPosition:animated: method to move the collection view back around to the beginning when it’s scrolled to the end of a list of items – by setting the animation property to NO, the move takes place without the user being aware of it.

By implementing the same effect in reverse when the collection view reaches the beginning, it appears that it can be infinitely scrolled in either direction.

Three assumptions first – the collection view is scrolling horizontally; the cells fill the full width of the collection view’s frame; and the collection view has its paging property set to YES so that cells will ‘snap’ to the edges of the frame as they scroll.

Setting up the data

The technique relies on manipulating the list of items to add an extra item at each end. Say for example you had an array of three items, and wanted the collection view to endlessly scroll. You’d need to add an additional two items onto the array – a copy of item 3 at the beginning of the array, and a copy of item 1 at the beginning:

That’s a simple operation – assuming you’ve got an array of data called originalArray, this method will create an NSMutableArray called workingArray, and pass an immutable copy of that into the UICollectionView’s data source.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-(void)setupDataForCollectionView {

    // Create the original set of data
    NSArray *originalArray = @[@"itemOne", @"itemTwo", "itemThree"];

    // Grab references to the first and last items
    // They're typed as id so you don't need to worry about what kind
    // of objects the originalArray is holding
    id firstItem = originalArray[0];
    id lastItem = [originalArray lastObject];

    NSMutableArray *workingArray = [originalArray mutableCopy];

    // Add the copy of the last item to the beginning
    [workingArray insertObject:lastItem atIndex:0];

    // Add the copy of the first item to the end
    [workingArray addObject:firstItem];

    // Update the collection view's data source property
    self.dataArray = [NSArray arrayWithArray:workingArray];

}

The new self.dataArray will now contain:

1
@"itemThree", @"itemOne", @"itemTwo", "itemThree", @"itemOne"

Creating the effect

The process relies on the ability to move a UICollectionView to a specific indexPath without animation – that’s possible with the scrollToIndexPath:atScrollPosition:animated: method.

This takes three parameters:

  • the indexPath that the collection view should scroll to
  • a UICollectionViewScrollPosition to control where the collection view should scroll to
  • a BOOL to control whether the scrolling should be animated or not

The UICollectionViewScrollPosition parameter controls exactly where the collection view should be scrolled to. Assuming that the collection view is paged, you’d want it to scroll to UICollectionViewScrollPositionLeft which will align the item being displayed with the left-hand edge of the UICollectionView’s frame. If you were working with a vertically-scrolling collection view, you’d want the UICollectionViewScrollPositionTop value.

Here’s how that works in practice when scrolling to the end of the collection view:

Scrolling to the beginning of the collection view is very similar, but works in the opposite direction:

Implementing the process

The key to making this happen is being able to detect when the user has scrolled the collection view to the end, and using that to trigger the scrollToIndexPath:atScrollPosition:animated: method.

To do this, you can exploit the fact that UICollectionView inherits from UIScrollView and so calls the scrollViewDidEndDecelerating: method from the UIScrollViewDelegate protocol when the scrolling stops.

By checking the contentOffset property, you can check where the collectionView has landed, and react accordingly. In this method, the local variable scrollview is a reference to the UICollectionView that’s calling it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
-(void)scrollViewDidEndDecelerating:(UIScrollview *)scrollview {

    // Calculate where the collection view should be at the right-hand end item
    float contentOffsetWhenFullyScrolledRight = self.collectionView.frame.size.width * ([self.dataArray count] -1);

    if (scrollView.contentOffset.x == contentOffsetWhenFullyScrolledRight) {

        // user is scrolling to the right from the last item to the 'fake' item 1.
        // reposition offset to show the 'real' item 1 at the left-hand end of the collection view

        NSIndexPath *newIndexPath = [NSIndexPath indexPathForItem:1 inSection:0];

        [self.collectionView scrollToItemAtIndexPath:newIndexPath atScrollPosition:UICollectionViewScrollPositionLeft animated:NO];

    } else if (scrollView.contentOffset.x == 0)  {

        // user is scrolling to the left from the first item to the fake 'item N'.
        // reposition offset to show the 'real' item N at the right end end of the collection view

        NSIndexPath *newIndexPath = [NSIndexPath indexPathForItem:([self.dataArray count] -2) inSection:0];

        [self.collectionView scrollToItemAtIndexPath:newIndexPath atScrollPosition:UICollectionViewScrollPositionLeft animated:NO];

    }
}

First, this calculates the content offset that the collection view has when it’s scrolled fully to the right (so the right-hand-end copy of item 1 is showing). The offset is actually calculated relative to the left edge of the collection view, so it’s the collection view’s width multiplied by the number of items in the collection view minus the width of one item.

Then you can check if the collection view has landed there. If it has, you need to scroll it back so that it shows the ‘real’ item 1 – that’s the second item in the data array, so the corresponding indexPath will be item 1, section 0 (remember that index paths are zero-indexed, so the left-most item will have index path [0,0], the second item will have index path [1,0] and so on).

Having created the index path to scroll to, it’s just a case of calling scrollToItemAtIndexPath:atScrollPosition:animated on the collection view – remembering to set the animated property to NO so that you don’t see the collection view scroll.

If the collection view has been scrolled to the left-most item, it’ll be showing the extra copy of item 3 that was inserted at the start of the workingArray – and the content offset will be 0. This time, you need to scroll the content view to the last-but-one item in the workingArray – which is the ‘real’ item 3.

Its index path is [3,0] – in other words, [workingArray count] - 2. It’s minus 2 – firstly because arrays and index paths are zero-indexed, so the index of the last item in an array is the number of items minus 1; and then you need to display the ‘real’ item 3 which is one place to the left from the end of the array.

Summary

Creating a circular UICollectionView isn’t difficult, and has five steps:

  • update the data to pad it with ‘fake’ items at the beginning the end
  • check the content offset of the collection view when it stops scolling
  • move to the second item in the data if the collection view has stopped at the end
  • move to the last-but-one item in the data if the collection view has stopped at the beginning
  • ensure the animation property of the scrollToItemAtIndexPath: method is NO to hide the movement from the user.