Adoption Curve Dot Net

Working With Size Classes in Interface Builder

| Comments

Android used to be notorious amongst iOS developers for its practically infinite permutations of interface size. Viewed from the iOS world, this used to look like a problem, because iOS didn’t really provide much in the way of support for building interfaces of different sizes.

If you were building a universal app that supported both iPhone and iPad, there was a tendency to end up with a lot of if deviceType == kIpad-style code.

AutoLayout was the first part of fixing that problem, and the job’s been completed with iOS 8 and size classes. This is probably the least-sexy feature introduced in 8, but it’s definitely one of the more important.

Some quick background on size classes

There are currently two size classes – horizontal and vertical, and each one comes in two sizes – regular and compact. The current orientation of the device can be described as a combination of the sizes:

  • Horizontal regular, vertical regular: iPad in either orientation
  • Horizontal compact, vertical regular: iPhone portrait
  • Horizontal regular, vertical compact: no current device
  • Horizontal compact, vertical compact: iPhone landscape

Storyboards and nib files now support these size classes – so you can think of them as having up to four different layouts contained within the same file. At the bottom of the Interface Builder window, there’s now a control that allows you to switch between each combination:

Every control or AutoLayout constraint can exist in one, several or all of the size classes. This means that you can build interfaces that change depending on device type and/or orientation without any code. Controls can appear or disappear, change size, or change arrangements – all based on the layout that you create in Interface Builder.

The Many Forms of Swift Functions

| Comments

There are a somewhat bewildering variety of forms that a Swift function can take, depending on the permutations of parameters and return values that you want to use. Here’s a cheat summary:

Here’s a downloadable PDF version.

A Minimum Viable tableView in Swift

| Comments

This GitHub repo is a minimum viable implementation of a UITableView in Swift. Here’s a swift (badum, tish) tutorial on creating a UITableView using the new language.

The project consists consists of a single storyboard with a table view control, and a view controller written in Swift:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    
    let cellIdentifier = "cellIdentifier"
    var tableData = String[]()
    
    @IBOutlet var tableView: UITableView
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // Register the UITableViewCell class with the tableView
        self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: self.cellIdentifier)
        
        // Setup table data
        for index in 0...100 {
            self.tableData += "Item \(index)"
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    // UITableViewDataSource methods
    
    func numberOfSectionsInTableView(tableView: UITableView!) -> Int {
        return 1
    }
    
    func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int {
        return tableData.count
    }
    
    func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
        var cell = tableView.dequeueReusableCellWithIdentifier(self.cellIdentifier) as UITableViewCell
        cell.textLabel.text = self.tableData[indexPath.row]
        return cell
    }

    // UITableViewDelegate methods
    
    func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) {
        let alert = UIAlertController(title: "Item selected", message: "You selected item \(indexPath.row)", preferredStyle: UIAlertControllerStyle.Alert)
        alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Cancel, handler: nil))
        self.presentViewController(alert, animated: true, completion: nil)
    }
  
}

Creating a Draggable UICollectionViewCell

| Comments

So here’s the situation – you’re creating an interactive UICollectionView, and you want to be able to drag a cell around the screen with a touch. To provide user feedback, you want the contents of the cell to follow the user’s finger as it moves around.

The problem is that unless you’re using a completely custom collection view layout, you can’t move the cell itself. The collection view is in charge of where things are displayed, and it’s a major pain to override this – especially if you’re using a flow layout. Reimplementing UICollectionViewFlowLayout from scratch is a decidedly non-trivial undertaking.

The answer lies in a hack. Create a copy of the contents of the cell as an image, then drag this around the screen underneath your finger. Much easier.

Here’s an example – it assumes that you’ve previously created and attached a UIPanGestureRecognizer to the collection view, and tied this to a method called handlePan: in your view controller. There’s also a UIImageView property on the view controller called movingCell.

A draggable UICollectionViewCelllink
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
26
27
28
29
(void)handlePan:(UIPanGestureRecognizer *)panRecognizer {

    CGPoint locationPoint = [panRecognizer locationInView:self.collectionView];

    if (panRecognizer.state == UIGestureRecognizerStateBegan) {

        NSIndexPath indexPathOfMovingCell = [self.collectionView indexPathForItemAtPoint:locationPoint];
        UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPathOfMovingCell];

        UIGraphicsBeginImageContext(cell.bounds.size);
        [cell.layer renderInContext:UIGraphicsGetCurrentContext()];
        UIImage *cellImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();

        self.movingCell = [[UIImageView alloc] initWithImage:cellImage];
        [self.movingCell setCenter:locationPoint];
        [self.movingCell setAlpha:0.75f];
        [self.collectionView addSubview:self.movingCell];

    }

    if (panRecognizer.state == UIGestureRecognizerStateChanged) {
        [self.movingCell setCenter:locationPoint];
    }

    if (panRecognizer.state == UIGestureRecognizerStateEnded) {
        [self.movingCell removeFromSuperview];
    }
}

When the pan gesture recognizer fires, it calls the handlePan: method with itself as a parameter.

A UIPanGestureRecognizer has three states that we’re interested in – UIGestureRecognizerStateBegan (which is fired as the first touch starts), UIGestureRecognizerStateChanged(which fires as the touch moves) and UIGestureRecognizerStateEnded (which fires as the finger is lifted).

We hook into the UIGestureRecognizerStateBegan event, and get the location where the pan gesture is occurring:

CGPoint locationPoint = [panRecognizer locationInView:self.collectionView];

Then if the touches have just begun, we grab the cell in which the touch started:

NSIndexPath *indexPathOfMovingCell = [self.collectionView indexPathForItemAtPoint:locationPoint];

UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPathOfMovingCell];

and create a UIImage out of the cell’s layer:

UIGraphicsBeginImageContext(cell.bounds.size);

[cell.layer renderInContext:UIGraphicsGetCurrentContext()];

UIImage *cellImage = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

Finally, we use this UIImage to populate a UIImageView property, and update the center of the UIImageView so that it lies underneath the current location of the touch. I’ve also tweaked the image’s opacity to make it slightly translucent:

self.movingCell = [[UIImageView alloc] initWithImage:cellImage];

[self.movingCell setCenter:locationPoint];

[self.movingCell setAlpha:0.75f];

[self.collectionView addSubview:self.movingCell];

Following the touches is just a case of updating the centre of the UIImageView:

if (panRecognizer.state == UIGestureRecognizerStateChanged) {

    [self.movingCell setCenter:locationPoint];

}

And when the touches end, we remove the UIImageView from the collectionView completely:

if (panRecognizer.state == UIGestureRecognizerStateEnded) {

    [self.movingCell removeFromSuperview];

}

This implementation simply removes the pseudo-cell from the screen when the touch finishes, but there’s no reason why you can’t do something like insert it back into the collection view at the point where it was ‘dropped’. I’ll put the code for this up in another post.

Mocking UICollectionViewLayouts

| Comments

At the heart of custom UICollectionViewLayouts are lots of calculations, and creating/debugging these by hand can be painful. It’s easier in the long run to write tests to help with this – but setting up the stack of objects to make the tests run can be a bit involved.

Here’s how I’m doing it – using XCTest and OCMock, although there’s no reason why this approach won’t work with other test/mock frameworks like Kiwi etc.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-(void)testCalculateSpokeRadiusReturnsCorrectValueForTwoItems {

    UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 500, 500) collectionViewLayout:self.customLayout];

    id collectionViewMock = OCMPartialMock(collectionView);
    [[[collectionViewMock stub] andReturnValue:@(1)] numberOfItemsInSection:0];
  
    [collectionViewMock setCollectionViewLayout:self.customLayout];

    [self.customLayout setItemSize:CGSizeMake(100, 100)];
    [self.customLayout setSidePadding:10.0f];

    XCTAssertEqual([self.customLayout calculateSpokeRadius], 190.0f, @"should be 190.0f");

}

The process isn’t too gnarly. First, create a real, live UICollectionView instance and give it your custom layout (in this test, I’d previously instantiated the custom layout object in the test setup):

UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 500, 500) collectionViewLayout:self.customLayout];

Then create a partial mock:

id collectionViewMock = OCMPartialMock(collectionView);

With this you, you can then stub out the numberOfItemsInSection: method and return the number of items you want to run the calculations for – by mocking out this method, you’ve got no dependencies on your datasources.

The advantage of using a partial mock is that you only need to stub out the methods that you want to control – you can use everything else as you would with the real, live object.

[[[collectionViewMock stub] andReturnValue:@(1)] numberOfItemsInSection:0];

Now link the custom layout and the collection view together:

[collectionViewMock setCollectionViewLayout:self.customLayout];

A few custom layout settings (these will obviously depend on how you’ve implemented your layout):

[self.customLayout setItemSize:CGSizeMake(100, 100)];`
[self.customLayout setSidePadding:10.0f];`

Finally, after all this, you can actually fire the test:

XCTAssertEqual([self.customLayout calculateSpokeRadius], 190.0f, @"should be 190.0f");

Here, I’ve created a helper method inside the custom layout to calculate the radius from the centre of the collection view for various sizes of layout. That’s often an easier approach to take – calculating layout attributes like item centre often involves some fiddly maths, so by breaking it up into chunks of helper methods you can test each bit piece-by-piece.

This tends to be easier in the long run than doing everything in one fell swoop, because you can spend a long time down the rabbit hole of figuring out where the layout is going wrong. With this test, I can throw various sizes of collection view at the layout, and check that things will still work out OK.