More XCTest UT Testing: A Helper function to scroll to a cell in a UICollectionView

At Luma Touch, we continue to use (and highly recommend) fastlane (now a part of fabric.io tools) to create and upload builds, manage App Store metadata, and most importantly, create and upload screenshots to the App Store.

The most difficult part of creating screenshots is setting up the XCTest automated tests in XCode to get everything in the right ‘place’ for each screenshot.  My previous post discussed a number of hints and tips and showed a complete example of how to do this.

For our next app, a large part of the screenshot setup requires selecting effect presets from a collection view, and scrolling the collection view to the correct location.  This is made particularly difficult since each device has a different sizes for the collection views and different sizes for the cells within them.  After a number of tries at guessing the right distance to ‘swipe’ with coordinates that would work for all devices, I ended up writing a new helper function that would automate scrolling to a particular cell within a collectionView.

The scrollTo Helper Function

The function looks like this (full source at the bottom of this post):

func scrollTo(cellIdentifier: String, collectionViewElement: XCUIElement, scrollUp: Bool = false, fullyVisible: Bool = false) -> Bool

To use it in your test code, your UICollectionView and each cell should have its accessibilityIdentifier set in code.  I usually include the section and item in the accessibilityIdentifier for the cell (i.e.: “presetcell-0-20”).  So, to scroll to a cell in a collectionView with an accessibilityIdentifier of “presets”, you could use:

scrollTo("presetcell-0-20", collectionViewElement: app.collectionViews["presets"])

This will scroll downward through the collectionView until “presetcell-0-20” is hittable.  If you wanted to make sure that the cell is fully visible (for a screenshot for example), you would use:

scrollTo("presetcell-0-20", collectionViewElement: app.collectionViews["presets"], scrollUp: false, fullyVisible: true)

And, finally, if you want to scroll upwards to “presetcell-0-5” and make it fully visible, you would use:

scrollTo(“presetcell-0-5”, collectionViewElement: app.collectionViews[“presets”], scrollUp: true, fullyVisible: true)

An Important Note About Cell accessibilityIdentifiers

In my previous post, I noted how I cleared the accessibilityIdentifier in tableView:didEndDisplayingCell.  However, this has a small chance of confusing the scrollTo helper function, so I’ve changed to setting the accessibilityIdentifier uniquely in didEndDisplayingCell like this:

cell.accessibilityIdentifier = [NSString stringWithFormat:@"notvisible-%ld-%ld", (long)indexPath.section, (long)indexPath.item];

How The Helper Function Works

The helper function scrolls 1/2 the height of the collection View over and over until either the cell you’re looking for is hittable, or the scroll doesn’t actually change anything (you’ve hit the top or bottom of the collection view).

Note that the first check it does for the cell, looks like this:

collectionViewElement.cells.matchingIdentifier(cellIdentifier).count > 0

This lets you query the collectionView cells to see if the identifier is present without having the test fail by directly checking for the cell with collectionViewElement.cells[cellIdentifier], you would get a failure in the test, and it wouldn’t continue.

The code checks to see if the touch changed anything by keeping track of the ‘middle’ cell in the list of cells for the collection view (which by-the-way, might include cells that are no longer displayed), and determines if it’s the same cell id and same frame to see if anything has changed.

After the cell is found to be hittable, if you want it fully visible, there is a second loop that scrolls up or down by a smaller amount (1/2 the height of the cell) until the cell’s frame is fully contained by the collection view’s frame.

The Helper Function Source

    func scrollTo(cellIdentifier: String, collectionViewElement: XCUIElement, scrollUp: Bool = false, fullyVisible: Bool = false) -> Bool {
        var rtn = false
        var lastMidCellID = ""
        var lastMidCellRect = CGRectZero

        var currentMidCell = collectionViewElement.cells.elementBoundByIndex(collectionViewElement.cells.count / 2)

        // Scroll until the requested cell is hittable, or until we try and scroll but nothing changes

        while (lastMidCellID != currentMidCell.identifier || !CGRectEqualToRect(lastMidCellRect, currentMidCell.frame)) {            

            if (collectionViewElement.cells.matchingIdentifier(cellIdentifier).count > 0 && collectionViewElement.cells[cellIdentifier].exists && collectionViewElement.cells[cellIdentifier].hittable) {
                rtn = true
                break;
            }

            lastMidCellID = currentMidCell.identifier
            lastMidCellRect = currentMidCell.frame      // Need to capture this before the scroll

            if (scrollUp) {
                collectionViewElement.coordinateWithNormalizedOffset(CGVector(dx: 0.99, dy: 0.4)).pressForDuration(0.01, thenDragToCoordinate: collectionViewElement.coordinateWithNormalizedOffset(CGVector(dx: 0.99, dy: 0.9)))\
            }
            else {
                collectionViewElement.coordinateWithNormalizedOffset(CGVector(dx: 0.99, dy: 0.9)).pressForDuration(0.01, thenDragToCoordinate: collectionViewElement.coordinateWithNormalizedOffset(CGVector(dx: 0.99, dy: 0.4)))
            }

            currentMidCell = collectionViewElement.cells.elementBoundByIndex(collectionViewElement.cells.count / 2)
        }


        // If we want cell fully visible, do finer scrolling (1/2 height of cell relative to collection view) until cell frame fully contained by collection view frame

        if (fullyVisible) {
            let cell = collectionViewElement.cells[cellIdentifier]
            let scrollDistance = (cell.frame.height / 2) / collectionViewElement.frame.height

            while (!CGRectContainsRect(collectionViewElement.frame, cell.frame)) {   
                if (cell.frame.minY < collectionViewElement.frame.minY) {
                    collectionViewElement.coordinateWithNormalizedOffset(CGVector(dx: 0.99, dy: 0.5)).pressForDuration(0.01, thenDragToCoordinate: collectionViewElement.coordinateWithNormalizedOffset(CGVector(dx: 0.99, dy: 0.5 + scrollDistance)))
                }
                else {
                    collectionViewElement.coordinateWithNormalizedOffset(CGVector(dx: 0.99, dy: 0.5)).pressForDuration(0.01, thenDragToCoordinate: collectionViewElement.coordinateWithNormalizedOffset(CGVector(dx: 0.99, dy: 0.5 - scrollDistance)))
                }
            }
        }

        return rtn;
    }

 

XCTest UI Testing Hints and Tips

As I’m writing this, I’m watching fastlane’s snapshot tool generate 900 snapshots (3 app variants, 6 devices, 5 screenshots per device, 10 languages) slowly in the background while I work on other things.  If you haven’t looked at fastlane yet, you should (https://github.com/fastlane/fastlane), it’s an incredible timesaver in distributing beta builds, managing metadata and screenshots for the app store, and much more.

Fastlane’s snapshot tool was recently modified to use the new XCTest UI Testing (replacing the older Instruments Javascript based UI Testing).  In the long run, this is a great thing.  The new UI Testing has a lot of promise, but it it’s still not documented and has a number of issues that can make it frustrating to use. I wrote this blog based on some issues that I struggled with and resolved in creating the screenshot code for snapshot.  I’m hoping others will find this useful.

In my own research to resolve problems, I found the following sites and articles extremely helpful in learning more about the UI Testing framework:

masilotti.com – Multiple articles and basic documentation of UI Test classes

UI Testing Gotchas at Big Nerd Ranch

UI Testing in Xcode at Infinity Learning

XCode 7 UI Testing, a first look at mokacoding

I would recommend reading these sources to get a better overall understanding, since this blog focuses on some specific issues and solutions rather than documenting the basics of UI Testing.

Adding Test Files To Your App

The first thing I needed for my screenshots was to be able to add a number of test files, including photos and videos that needed to be in the Photos app on the simulator.  In the previous version of snapshot, I could copy the files as part of the Snapfile script, but that isn’t possible now.  So, for this version, I added a Run Script action in the UI Test target‘s Build Phases that copies the files into the app itself:

cp -R "${HOME}/Dropbox/LumaTouch/Product Assets/PinnacleStudio/Screenshot-Projects/SnapshotFiles/" "${BUILT_PRODUCTS_DIR}/PinnacleStudio.app/SnapshotFiles"

When the app is built for the simulator, it contains code that looks for the SnapshotFiles folder, and if found, it copies some of its data into the App’s documents folder, and for photos and videos, calls PHAssetLibrary to add the photos and videos to the Photos app.  The code looks for the SnapshotFiles folder like this:

NSURL *folderUrl = [[NSBundle mainBundle] bundleURL];
folderUrl = [folderUrl URLByAppendingPathComponent:@"SnapshotFiles"];
if ([[NSFileManager defaultManager] fileExistsAtPath:folderUrl.path])
{
  ...
}

Accessibility To Identify Elements

The best way to reference UI elements in UI Testing is through accessibility values.  Elements like StaticText (UILabel) elements can automatically be referenced by the text value of the label, which effectively becomes the accessibilityLabel value of the element like:

let labelElement = app.staticTexts["Phone Number"]

However, this is not very useful if your app is localized and the label changes based on the language.  I prefer to set the accessibilityIdentifier for each UI element in the app and use that to reference elements when testing.  The accessibilityIdentifier is an internal identifier and is not localized.  You can set this in Interface Builder, or in code, like this:

phoneLabel.accessibilityIdentifier = "phone-label"

Then, to access in your test, simply use the accessibilityIdentifier, like this:

XCTAssert(app.labels["phone-label"].title == "Phone Number")

Waiting for UI Elements to Appear or Disappear

One of the issues with adding my test files for screenshots is that it may take some time for the app to copy the files and add the media to Photos.  Originally I put a sleep(30) as a guess in my test code, but there is a better way.  I added code in the app to create a hidden button once it was finished setting up the test files.  I created a number of helper functions (code for these helper functions is at the bottom of this post) to wait for elements to exist, or be hittable, or to disappear again.  These were simply added as extensions to XCTestCase in my test code.

  • waitForHittable(element: XCUIElement, waitSeconds: Double) – Wait for an element to exist and be hittable (visible and onscreen)
  • waitForNotHittable(element: XCUIElement, waitSeconds: Double) – Wait until an element is no longer hittable (might still exist, but is either not visible or not onscreen)
  • waitForExists(element: XCUIElement, waitSeconds: Double) – Wait for an element to exist (does not need to be visible or onscreen)

All of these functions take advantage of the waitForExpectationsWithTimeout method in XCTestCase and make it easy to wait for certain things to happen within an allotted time.  So, in my case, I have the following code in my test to wait for the test files to be finished:

waitForExists(app.otherElements["snapshotReady"], waitSeconds: 60)

As soon as the “snapshotReady” view is created and added as a subview in my app’s code (it’s hidden, and created with a rect that’s offscreen, so it’s never seen on the UI), the test continues.  If the processing of the snapshot test files fails, then after 60 seconds, the test will fail.

In another point in my test code, I need to start a render process, which brings up a progress view with a cancel button.  In my old test code, I just put in a long sleep() to make sure it could finish, but with the wait helper functions, I can wait until the progress view appears, then disappears again to continue:

app.buttons["edit-render"].tap()
waitForHittable(app.buttons["export-process-cancel"], waitSeconds: 5)
waitForNotHittable(app.buttons["export-process-cancel"], waitSeconds: 320)

Debugging UI Elements When Testing

The best way to see what is going on is to set a breakpoint in your test code, then output a complete list of the UI Elements that are onscreen.  You can do this in lldb with the following command:

po print(app.debugDescription)

This assumes that app is assigned to XCUIApplication(), which is normally the case. This will print a well-formatted description of the app element and all of its descendants (if you don’t use print(), you’ll get a very ugly output).  You can use this on any XCUIElement(), for example:

po print(app.buttons["edit-render"])

Shortest Path To An Element

When you’re recording tests, XCode might record something like this when tapping on a tableView cell (assuming you’ve set accessibility identifiers or labels on the table and cells):

app.tables["address-table"].cells["address-3"].tap()

When you’re writing your own test code, if you know that the accessibilityIdentifier for the cells are unique in the UI, you can shortcut this with:

app.cells["address-3"].tap()

This is because the cells call simply queries all descendants that are Cell elements, and as long as it finds only one that matches the identifier, that will be the element returned.

Duplicate Identifiers in TableView Cells

When running one of my tests, I kept getting a failure Multiple Matches Found when I tried to tap() a cell in my table.  After a great deal of experimenting I figured out the problem. I was setting the accessibilityIdentifier in cellForIndexPath, but I was never clearing that identifier, and somehow the cached cell, was being found in the query. To solve this problem I added code to clear the accessibilityIdentifier in tableView:didEndDisplayingCell.

Failed To Find Matching Element

A number of times when recording, I got the following message added as a comment in my test code:

Failed to find matching element please file bug (bugreport.apple.com) and provide output from Console.app

I tried many different changes of accessibilityIdentifiers and different paths to the element, but nothing helped, so I filed a Radar with Apple.  It was a duplicate, so obviously I wasn’t the only one.  In playing with XCode 7.2 beta 2, it seems to be better about this, but I was still having a problem when calling tap() on cells in some tables.  It wouldn’t fail, but the cell was not being selected.  After a lot of playing around, I finally found the cause of the problem:

Tap Was A Long Press

The table that was failing had a long press gesture recognizer (0.175 seconds) attached (for drag-and-drop), and what was happening is that the tap() call was being picked up as a long press, so the cell wasn’t being selected.  To solve this problem, I changed the tap() to a pressForDuration, like this:

 app.tables["text-album-5-0"].cells["cell-0-1"].pressForDuration(0.05);

How To Do An Arbitrary Touch or Drag

Without documentation, it’s easy to miss how to do an arbitrary touch or drag in the UI (from/to a particular point on the screen).  For many situations it’s not needed, you can just use the pressForDuration:thenDragToElement call on an element, but I had some situations that weren’t that straightforward.

The solution is XCUICoordinate().  You create an XCUICoordinate() object by calling coordinateWithNormalizedOffset() on any XCUIElement (including XCUIApplication).  The CGVector is a value normalized from 0.0 to 1.0 to represent the element’s rect (you can provide values outside of the rect with negative values and values above 1.0).

The XCUICoordinate() object holds a reference to the XCUIElement() it was retrieved from. It is important to note that when you perform an action with XCUICoordinate() it will calculate the screen point at that moment, so if the element the coordinate references has moved since you first grabbed the coordinate, the tap will be relative to the element’s current location.

To perform a drag with an XCUICoordinate, you can call pressForDuration:thenDragToCoordinate.

You can get the second coordinate the same way you got the first one (by creating it from any element), or you can call coordinateWithOffset(offsetVector: CGVector) on an XCUICoordinate to return a new coordinate offset from the first coordinate (the offsetVector in this case is in absolute points rather than a normalized value).  Here is a complete example of using XCUICoordinate to do a drag:

let startCoord = app.tables["text-album-0-1"].coordinateWithNormalizedOffset(CGVector(dx: 0.5, dy: 0.01));

let endCoord = startCoord.coordinateWithOffset(CGVector(dx: 0.0, dy: -100));

startCoord.pressForDuration(0.05, thenDragToCoordinate: endCoord)

This starts the touch in the text-album-0-1 album halfway across the album, and 1% distant from top, then drags 100 points upward.

Conditional Code for iPad and iPhone

I needed some different code depending on whether the test was running on iPhone or iPad, but didn’t want to write separate tests.  You can determine the size class of the app’s window to provide this type of condition:

if (app.windows.elementBoundByIndex(0).horizontalSizeClass == .Compact || app.windows.elementBoundByIndex(0).verticalSizeClass == .Compact)

More To Come

That’s all of my hints and tips for now.  I’ll create a new blog as I find more hints and tips.  I’m re-creating a suite of hundreds of UI Tests done with OCUnit() back in 2011.  I always find examples the best way to learn something, so I’ve also included a complete example of a test doing a screenshot with fastlanes/snapshot.

Screenshot Test Source Code

func testScreenshot01() {
    let app = XCUIApplication()
    
    XCUIDevice().orientation = .LandscapeRight;

    waitForExists(app.otherElements["snapshotReady"], waitSeconds: 60)

    if (app.windows.elementBoundByIndex(0).horizontalSizeClass == .Regular && appl.windows.elementBoundByIndex(0).verticalSizeClass == .Regular)
        {
            // iPad Screenshot

            // Select and tap again to edit the project
            app.tables["proj-table"].cells["proj-cell-3"].tap();
            app.tables["proj-table"].cells["proj-cell-3"].tap();

            // Select the Albums segment, and select album 4
            app.segmentedControls["album-segments"].buttons.elementBoundByIndex(1).tap()
            sleep(1)

            app.tables["text-album-0-1"].cells["cell-0-4"].pressForDuration(0.05)

           // Render and wait for completion
            waitForHittable(app.buttons["edit-render"], waitSeconds: 5)
            app.buttons["edit-render"].tap()
            waitForHittable(app.buttons["export-process-cancel"], waitSeconds: 5)
            waitForNotHittable(app.buttons["export-process-cancel"], waitSeconds: 320)

            
            // Tap on clip 4 in the storyboard
            app.otherElements["storyClip-4"].tap()

            // Open the audio mixer
            app.buttons["comp-mixer"].tap();            
 
            // Select a video clip for the source viewer
            app.collectionViews["thumbnail-album-0-1-4"].cells["cell-0-8"].images["thumbnail"].pressForDuration(0.05)             
        }
        else
        {
            // iPhone Screenshot

            // Drag upward in the project table to make sure project is visible
            let coord = app.tables["proj-table"].coordinateWithNormalizedOffset(CGVector(dx: 0.5, dy: 0.5))

            let endCoord = coord.coordinateWithOffset(CGVector(dx: 0.0, dy: -50))

            coord.pressForDuration(0.05, thenDragToCoordinate: endCoord)

            sleep(1)
            // Select and edit the project
            app.tables["proj-table"].cells["proj-cell-3"].tap();
            app.tables["proj-table"].cells["proj-cell-3"].tap();

            // Tap breadcrumb to go back to Moments/Albums select
            app.buttons["album-breadcrumb"].tap()

            // Tap on Albums
            app.cells["cell-0-1"].pressForDuration(0.05)
            sleep(1)

            // Open album
            app.tables["text-album-0-1"].cells["cell-0-4"].pressForDuration(0.05);

            // Render and wait for completion
            waitForHittable(app.buttons["edit-render"], waitSeconds: 5)
            app.buttons["edit-render"].tap();
            waitForHittable(app.buttons["export-process-cancel"], waitSeconds: 5)
            waitForNotHittable(app.buttons["export-process-cancel"], waitSeconds: 320)

            // Tap on clips to get to clip 4
            app.otherElements["storyClip-1"].tap();
            sleep(1)
            app.otherElements["storyClip-2"].tap();
            sleep(1)
            app.otherElements["storyClip-3"].tap();
            sleep(1)
            app.otherElements["storyClip-4"].tap();
            sleep(1)

            // Switch to timeline only view
            app.buttons["storytime-button"].tap();

            // Tap in timeline ruler to de-select clip
            app.otherElements["Ruler"].coordinateWithNormalizedOffset(CGVector(dx: 0.1, dy: 0.05)).tap()

            // Open toolbox, tap audio mixer, close toolbox
            app.buttons["toolbox-button"].tap()
            app.buttons["comp-mixer"].tap()
            app.buttons["toolbox-button"].tap()
        }

        // Take the fastlane/snapshot 
        snapshot("Shot01")        

        // Get back to projects page
        app.buttons["audiomix-close"].tap()
        app.buttons["album-projects"].tap()
    }

Helper Functions Source Code

extension XCTestCase {
    func waitForHittable(element: XCUIElement, waitSeconds: Double, file: String = __FILE__, line: UInt = __LINE__) {
        let existsPredicate = NSPredicate(format: "hittable == true")
        expectationForPredicate(existsPredicate, evaluatedWithObject: element, handler: nil)        

        waitForExpectationsWithTimeout(waitSeconds) { (error) -> Void in
            if (error != nil) {
                let message = "Failed to find \(element) after \(waitSeconds) seconds."
                self.recordFailureWithDescription(message,
                        inFile: file, atLine: line, expected: true)
            }
        }
    }

    func waitForNotHittable(element: XCUIElement, waitSeconds: Double, file: String = __FILE__, line: UInt = __LINE__) {
        let existsPredicate = NSPredicate(format: "hittable == false")
        expectationForPredicate(existsPredicate, evaluatedWithObject: element, handler: nil)

        waitForExpectationsWithTimeout(waitSeconds) { (error) -> Void in
            if (error != nil) {
                let message = "Failed to find \(element) after \(waitSeconds) seconds."
                self.recordFailureWithDescription(message,
                    inFile: file, atLine: line, expected: true)
            }
        }
    }




    func waitForExists(element: XCUIElement, waitSeconds: Double, file: String = __FILE__, line: UInt = __LINE__) {
        let existsPredicate = NSPredicate(format: "exists == true")
        expectationForPredicate(existsPredicate, evaluatedWithObject: element, handler: nil)

        waitForExpectationsWithTimeout(waitSeconds) { (error) -> Void in
            if (error != nil) {
                let message = "Failed to find \(element) after \(waitSeconds) seconds."
                self.recordFailureWithDescription(message,
                    inFile: file, atLine: line, expected: true)
            }
        }
    }

}

The pros and cons of Apple’s TestFlight for me so far

I was an avid TestFlight user since 2011. TestFlight made it easy to distribute beta builds to testers. It helped make provisioning with device ids relatively painless, especially with its ability to re-provision a beta with new devices. It was still a bit of a pain though, when users got new devices, there was a bit of a dance that had to be performed, getting the device id, adding that in developer.apple.com, then adding the new device to a provisioning profile, and regenerating the profile. And of course there was the yearly 100 device limitation with no ability to remove old devices during the year.

When TestFlight was purchased by Apple and stopped accepting new apps, I switched to Crashlytics Beta (I was already using Crashlytics for crash tracking). It’s been a good replacement for TestFlight.

With the re-release of TestFlight from Apple, I had some hope of simplifying the process, but as always with developer tools from Apple, there are real tradeoffs in what you get. So here is a list of pros and cons with Apple’s TestFlight that I’ve found so far:

Pros:

– No more worrying about devices! This is the biggest advantage of TestFlight. When you add a user (either an Internal or External tester), you simply add the email address of their Apple ID, and they will be able to install beta builds on their devices (up to 10 per tester).

– It is nice to be able to do testing with Internal testers first (a new submitted build shows up automatically for internal testers), then send out the build to external testers.

– Beta builds appear with an orange dot (and will automatically expire after 30 days), so it’s easy to tell that it’s a beta

– With the addition of Groups, it’s easier to manage external testers across multiple products (though this could still be improved)

– Once you’ve submitted an app for initial review for external testing, subsequent betas can be readied for external review immediately after entering build information.

Cons:

– Users can only install the latest beta build (they can’t install old builds from the history of builds). This is another big problem if you end up sending out a beta that causes bigger problems than it fixes (not uncommon during betas).

– You can only send out 2 beta builds per day for external beta testing! This is a big limitation when you’re trying to send out quick changes to fix an odd bug that can’t be reproduced in-house

– You must submit the app for review for external beta testing. For the initial submission, it took a few days to get it approved.

– After you submit the build, you then need to go to itunesconnect and submit it for external beta testing, filling in the ‘what to test’, and answering a few questions about changes to the build. I found the process a little cumbersome – would be nice if that info could be filled in during submission of the build (would be nice to have a separate Submit Beta button in XCode)

– Still waiting on any kind of good crash reporting (I won’t be switching from Crashlytics anytime soon)

TestFlight is definitely a major step forward for beta testing of apps for iOS, but there are some difficult tradeoffs right now.  I’ll probably stick with it so I don’t have to worry about managing devices and provisioning, but I’m certainly hoping for continued improvement.

Subclassing UICollectionViewLayoutAttributes Correctly

I had the need to create a custom UICollectionViewLayout and I also needed to subclass UICollectionViewLayoutAttributes to add a representation attribute so I could ‘tell’ each cell to draw itself differently depending on the users’ selection of a view type.

In looking at examples others had provided for subclassing UICollectionViewLayoutAttributes I found a major confusion.  Many developers are trying to use the UICollectionViewLayout’s layoutAttributesForItemAtIndexPath method as a call to create the attributes object, but that method is meant to be called by the UICollectionView to return attributes for a specific item in the collection.

The correct way to create a new instance of custom attributes is to call UICollectionViewLayoutAttributes’ layoutAttributesForCellWithIndexPath class method.  You can call this method with any subclass of UICollectionViewLayoutAttributes to create a instance of your custom attributes (you’ll notice that it has instancetype as its return value, so it returns an instance of whatever class calls the method).

So, to create an instance of my custom layout attributes, I would use:


MyCustomLayoutAttributes *layoutAttributes = [MyCustomLayoutAttributes layoutAttributesForCellWithIndexPath:someIndexPath];

However, if you want to allow your custom layout class to be subclassed and allow the layerAttributesClass method overridden, then you should ask for the current layoutAttributeClass as shown here:


UICollectionViewLayoutAttributes *layoutAttributes = [[[self class] layoutAttributesClass] layoutAttributesForCellWithIndexPath:someIndexPath];
// setup standard attribute parameters in UICollectionViewLayoutAttributes
if ([layoutAttributes isKindOfClass:[MyCustomLayoutAttributes class])
{
    MyCustomLayoutAttributes *myAttributes = (MyCustomLayoutAttributes*)layoutAttributes;
    // setup custom attributes
}

 

UITableView Color Changes in iOS 7

Before iOS 7, the default background color for a UITableView was clearColor, and UITableViewCells by default took on the UITableView’s background color, so there was virtually no work setting up our tables in iOS 6.

When you switch to Xcode 5, your tables will default to the new iOS 7 behavior.  UITableView’s have a white background color by default, so you must change it to clearColor either in Interface Builder or programmatically.

More importantly, UITableViewCell’s no longer pick up the background color of the UITableView and are white by default. Setting the backgroundColor in InterfaceBuilder for a UITableViewCell has no effect (to my surprise I guess this has always been the case).

So, the “correct” way (based on Apple’s documentation in UITableViewCell) to set the UITableViewCell’s backgroundColor is to do it in the UITableViewDelegate willDisplayCell method, as shown here:

 

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    cell.backgroundColor = [UIColor clearColor];
}

Also, be sure your UITableViewCell’s contentView.backgroundColor is also set to clearColor (this can be done in Interface Builder).

 

I have found though that I can set the backgroundColor when I deque the cells in cellForRowAtIndexPath, but I’m not 100% sure this will work in all cases.

 


					

Instantiate Storyboard ViewControllers Manually

Quote

I’ve started using storyboards in XCode/iOS more frequently, but I really needed something different from the normal segues and connections that Apple provides, and while Apple briefly documents how to do this, I thought I’d share my example.  Here is the view in Interface Builder of the test app that I created:

My initial controller is on the left, and the blue area is simply a UIView that I’ve connected to  a member variable outlet named _parentView.  This is where I want my yellow and pink sub views to appear, each handled by their own controller.  Notice that there is no connection between the storyboards in this case.  The key here is to set the Storyboard ID for yellow and pink View Controllers so I can instantiate them in my code.  If I don’t set a storyboard id for the yellow and pink View Controllers, I’ll get a warning from XCode that they are unreachable. You set the Storyboard ID in the Identity inspector (see the inspector panel on the right in the picture above – make sure ‘Use Storyboard ID’ is checked).

Once I have storyboard IDs set for each of these view controllers, the code is quite simple to instantiate the view controllers and embed them using the parentView as a container.

- (IBAction)showView1:(id)sender 
{
  [self switchViews:@"yellow"];
}

- (IBAction)showView2:(id)sender 
{
  [self switchViews:@"pink"];
}

- (void)switchViews:(NSString*)storyboardId
{
  __block UIViewController *lastController = _childController;

  _childController = (UIViewController*)[self.storyboard instantiateViewControllerWithIdentifier:storyboardId];
  [self addChildViewController:_childController];
  [_parentView addSubview:_childController.view];

  CGRect parentRect = _parentView.bounds;
  parentRect.origin.x += parentRect.size.width;
  _childController.view.frame = parentRect;
  _childController.view.alpha = 0;
  [UIView animateWithDuration:0.5 animations:^{
    _childController.view.frame = _parentView.bounds;
    _childController.view.alpha = 1.0;
  } completion:^(BOOL finished) {
    if (lastController)
    {
      [lastController.view removeFromSuperview];
      [lastController removeFromParentViewController];
    }
  }];
}
The basics in the code above:
– Keep a pointer to the last view controller child
– Use instantiateViewControllerWithIdentifier using the storyboard id you set in interface builder to create and return the view controller.
– Add the new controller as a child controller to the parent
– Add the new controllers view as a subView to the parent
– Then for fun I animate the new controller onscreen
– When the new view is fully animated, then I remove the old view
With this basic flow, you can customize the user interface in just about any way you can imagine.