1

We've been tracking an issue on our project where we would have intermittently failing snapshot test cases. The gist of our approach is to render a view controller's view and compare that image to a reference image to see if they're different. There are several layers to our approach here:

Our issue is that sometimes the image created by the rendered view is empty. Just a large (correct size) transparent image.

I've tested each one in isolation and determined that none of those is the problem. Instead, I've been able to reproduce this in a standalone, plain Xcode project.

By using the same approach that FBSnapshotTestCases uses to render a view, I've created a simple test. To reproduce, create a new project of the "Master-Detail" template and give the detail view controller a Storyboard ID of "Detail". Then create this simple unit test.

func testExample1() {
    let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
    let sut = storyboard.instantiateViewControllerWithIdentifier("Detail") as UIViewController

    sut.beginAppearanceTransition(true, animated: false)
    sut.endAppearanceTransition()

    UIGraphicsBeginImageContextWithOptions(sut.view.frame.size, false, 0)
    sut.view.drawViewHierarchyInRect(sut.view.bounds, afterScreenUpdates: true)
    let image = UIGraphicsGetImageFromCurrentImageContext()!
    UIGraphicsEndImageContext()

    let data = UIImagePNGRepresentation(image)

    println("byte length: \(data.length)")
}

Nothing too fancy, and it will most likely pass. But, if you duplicate the code a few more times:

func testExample1() { ... }
func testExample2() { ... }
func testExample3() { ... }

The output is very strange (truncated):

Test Suite 'All tests' started at 2014-10-02 07:46:52 +0000
byte length: 27760
byte length: 17645
byte length: 27760
Test Suite 'All tests' passed at 2014-10-02 07:55:29 +0000.
     Executed 3 tests, with 0 failures (0 unexpected) in 517.778 (517.781) seconds

The byte lengths should be identical, but they're not. The second test (and sometimes the third) will have an empty view, just like our problem.

Empty image in quick look

A sample project demonstrating the problem is available here.

I was able to reproduce the issue using an Objective-C test project, so it's unlikely that it's a Swift problem. In past projects, we haven't used Storyboards for our view controller UI, so it's possible that there is an extra step necessary in order to "force" the view to load. It's also possible that this is an Xcode 6.x or iOS 8 issue (I've reproduced the problem with Xcode 6.0.1).

Has anyone experienced an issue like this, where rendered images of views from controllers loaded from Storyboards have been transparent?

Ash Furrow
  • 12,391
  • 3
  • 57
  • 92
  • In case anyone is interested, using the [old way](https://github.com/facebook/ios-snapshot-test-case/blob/1bf1d5f76b2585101f736bba4f558c192e5ff5c4/FBSnapshotTestController.m#L358-L379) of rendering a view does *not* produce the problem. I might have to open a radar and an issue on Facebook's library. – Ash Furrow Oct 02 '14 at 08:26
  • This is almost certainly a bug in UIKit. I'm using a fork of FBSnapshotTestCase (details [here](https://github.com/artsy/eidolon/pull/80/files)) in the meantime. I'll file a radar and post a link to openradar. – Ash Furrow Oct 02 '14 at 09:37
  • For anyone following along at home, I've opened a [radar](http://openradar.appspot.com/radar?id=6412304434331648) describing the issue. If I hear back, I'll update it there. – Ash Furrow Oct 02 '14 at 12:35

3 Answers3

2

Seems to do the trick...

    let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
    let sut = storyboard.instantiateViewControllerWithIdentifier("Detail") as UIViewController

    sut.beginAppearanceTransition(true, animated: false)
    sut.endAppearanceTransition()

    UIGraphicsBeginImageContextWithOptions(sut.view.frame.size, false, 0)
    let context = UIGraphicsGetCurrentContext
    sut.view.layer.renderInContext(context())
    let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    let data = UIImagePNGRepresentation(image)
    println("byte length: \(data.length)")
Claes
  • 236
  • 3
  • 5
  • Yeah definitely. That's actually the [old way](https://github.com/facebook/ios-snapshot-test-case/blob/1bf1d5f76b2585101f736bba4f558c192e5ff5c4/FBSnapshotTestController.m#L358-L379) that the Facebook lib used to do it. There's an [open PR](https://github.com/facebook/ios-snapshot-test-case/pull/46) to provide the option to use it. Hope it gets merged soon. – Ash Furrow Oct 02 '14 at 09:10
1

Taking the storyboards out of the equation by switching to a generated UIView:

let view = UIView(frame: CGRectMake(0, 0, 300, 300))
view.backgroundColor = UIColor.blueColor()

UIGraphicsBeginImageContextWithOptions(view.frame.size, false, 0)
view.drawViewHierarchyInRect(view.bounds, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()

let data = UIImagePNGRepresentation(image)

println("byte length: \(data.length)")

Gives similar results.

Test Case '-[TestTestTests.TestTestTests testExample1]' started.
byte length: 9663
Test Case '-[TestTestTests.TestTestTests testExample1]' passed (1.000 seconds).
Test Case '-[TestTestTests.TestTestTests testExample2]' started.
byte length: 9663
Test Case '-[TestTestTests.TestTestTests testExample2]' passed (0.112 seconds).
Test Case '-[TestTestTests.TestTestTests testExample3]' started.
byte length: 6469
orta
  • 4,225
  • 1
  • 26
  • 34
0

As this topic suggests you try using "[self.view.layer renderInContext:UIGraphicsGetCurrentContext()]" instead:

drawViewHierarchyInRect:afterScreenUpdates: delays other animations

Community
  • 1
  • 1
  • Yeah, this is what Facebook's library used to do. I've reverted to using that old behaviour in the meantime. – Ash Furrow Oct 02 '14 at 12:34