Taking screenshots and getting a reference to the keyboard in iOS 4.x

If you want to transition between two views, here's a neat trick: take a screenshot of the screen you're transitioning from and do the transition between that bitmap and a bitmap representation of the screen you're transitioning to. That way, instead of animating between two possibly complex views with lots of subviews, etc., you are simply animating between two bitmaps. This, of course, will be far less of a strain on the CPU and thus perform better. This little nugget isn't just for iPhone development either. I first started using it back in the day while making Flash apps.

Taking screenshots programmatically

The issue with using this method when doing iPhone development is how to take a screenshot of your app. In the past, it was easy: you simply used the undocumented-but-tolerated UIGetScreenImage() function and it spat out a screenshot for you. Since the release of 4.0, however, Apple no longer accepts apps that use UIScreenImage(). Instead, they've helpfully provided a sample method you can use that utilizes CALayer/-renderInContext:. I've altered this method very slightly to compensate for the presence of the status bar and made it a class method in a category on UIScreen.

Keyboard blues

The screenshot method will return a screenshot of your app, including the keyboard if it's displaying. But what if you want to just get a reference to the keyboard or if you want to take a snapshot of just the keyboard.

In 3.2 and lower, the method I blogged back in April will get you a reference to the keyboard view. That, however, will not work in 4.0 since the class name of the keyboard has changed (since it's a private class, this is wholly expected and may change again at any time). While 4.0 gives you the handy .inputView and .inputAccessoryView properties on text field and view objects that you can use to create custom keyboards and custom accessory views, it doesn't provide a way to get a reference to the system keyboard (for doing things like taking a screenshot of it or possibly overlaying something on it for those times that you don't want to create a completely new keyboard). So, using Apple's code as a base, I added two more class methods to my UIScreen category. One returns a reference to the keyboard on 4.x and the other takes a screenshot of just the keyboard.

It's a niche need (one that I have in Feathers) but I hope it helps you since I couldn't find any information for getting a reference to the system keyboard under 4.x anywhere else on the web.

Here's the full category (I'm afraid it could use some refactoring but I don't have the time at the moment to clean it up).

Download

Download the source (UIScreen+Screenshot.zip; 4KB) or view it at the end of this post.

How to use

Simply import the category in your class and then call one of the three factory methods. e.g.,

#import "UIScreen+Screenshot.h"

…

  UIImage *myScreenshot = [UIScreen screenshot];
  UIImage *myKeyboardScreenshot = [UIScreen keyboardScreenshot];
  UIWindow *keyboardRef = [UIScreen keyboardRef];

…

Browse source code

(I'm aware that the horizontal scrolling is a pain – sorry. This blog needs a facelift and a wider content area. Cobbler's children's shoes, and all that…)

/*
   Copyright (c) 2010 Aral Balkan. Released under the open source MIT license.

   Based on the sample Apple code at
   http://developer.apple.com/library/ios/#qa/qa2010/qa1703.html
*/

@interface UIScreen(Screenshot)

+ (UIImage*)screenshot;
+ (UIImage *)keyboardScreenshot;
+ (UIWindow*)keyboardRef;
@end


@implementation UIScreen(Screenshot)

+ (UIImage *)screenshot
{
  // Also checking for version directly for 3.2(.x) since UIGraphicsBeginImageContextWithOptions appears to exist
  // but can't be used.
  float systemVersion = [[[UIDevice currentDevice] systemVersion] floatValue];

    // Create a graphics context with the target size
    // On iOS 4 and later, use UIGraphicsBeginImageContextWithOptions to take the scale into consideration
    // On iOS prior to 4, fall back to use UIGraphicsBeginImageContext
  CGSize imageSize = [[UIScreen mainScreen] bounds].size;
       if (systemVersion >= 4.0f)
  {
        UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0);

  } else {
    UIGraphicsBeginImageContext(imageSize);
  }

      CGContextRef context = UIGraphicsGetCurrentContext();

      // Iterate over every window from back to front
      //NSInteger count = 0;
      for (UIWindow *window in [[UIApplication sharedApplication] windows])
      {
        if (![window respondsToSelector:@selector(screen)] || [window screen] == [UIScreen mainScreen])
        {
          // -renderInContext: renders in the coordinate space of the layer,
          // so we must first apply the layer's geometry to the graphics context
          CGContextSaveGState(context);
          // Center the context around the window's anchor point
          CGContextTranslateCTM(context, [window center].x, [window center].y);
          // Apply the window's transform about the anchor point
          CGContextConcatCTM(context, [window transform]);

          // Y-offset for the status bar (if it's showing)
          NSInteger yOffset = [UIApplication sharedApplication].statusBarHidden ? 0 : -20;

          // Offset by the portion of the bounds left of and above the anchor point
          CGContextTranslateCTM(context,
                      -[window bounds].size.width * [[window layer] anchorPoint].x,
                      -[window bounds].size.height * [[window layer] anchorPoint].y + yOffset);

          // Render the layer hierarchy to the current context
          [[window layer] renderInContext:context];

          // Restore the context
          CGContextRestoreGState(context);
        }
      }

    // Retrieve the screenshot image
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return image;
}

+ (UIWindow*)keyboardRef
{
  UIWindow *keyboard = nil;
  for (UIWindow *window in [[UIApplication sharedApplication] windows])
  {
    if ([[window description] hasPrefix:@"<UITextEffectsWin"])
    {
      keyboard = window;
      break;
    }
  }
  return keyboard;
}

+ (UIImage *)keyboardScreenshot
{
  // Also checking for version directly for 3.2(.x) since UIGraphicsBeginImageContextWithOptions appears to exist
  // but can't be used.
  float systemVersion = [[[UIDevice currentDevice] systemVersion] floatValue];

    // Create a graphics context with the target size
    // On iOS 4 and later, use UIGraphicsBeginImageContextWithOptions to take the scale into consideration
    // On iOS prior to 4, fall back to use UIGraphicsBeginImageContext
  CGSize imageSize = [[UIScreen mainScreen] bounds].size;
    if (systemVersion >= 4.0f)
        UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0);
  else
    UIGraphicsBeginImageContext(imageSize);

  CGContextRef context = UIGraphicsGetCurrentContext();

  // Iterate over every window from back to front
  //NSInteger count = 0;
  UIWindow *window = [UIScreen keyboardRef];

  // -renderInContext: renders in the coordinate space of the layer,
  // so we must first apply the layer's geometry to the graphics context
  CGContextSaveGState(context);
  // Center the context around the window's anchor point
  CGContextTranslateCTM(context, [window center].x, [window center].y);
  // Apply the window's transform about the anchor point
  CGContextConcatCTM(context, [window transform]);

  // Y-offset for the status bar (if it's showing)
  NSInteger yOffset = [UIApplication sharedApplication].statusBarHidden ? 0 : -20;

  // Offset by the portion of the bounds left of and above the anchor point
  CGContextTranslateCTM(context,
              -[window bounds].size.width * [[window layer] anchorPoint].x,
              -[window bounds].size.height * [[window layer] anchorPoint].y + yOffset );

  // Render the layer hierarchy to the current context
  [[window layer] renderInContext:context];

  // Restore the context
  CGContextRestoreGState(context);

    // Retrieve the screenshot image
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return image;
}

@end

Update: For some reason the weak linking doesn't appear to work on the iPad (simulator or device) on iOS < 4.0 (tested on 3.2.2/iPad and 3.1/iPhone). The check for the existence (!= NULL) of UIGraphicsBeginImageContextWithOptions succeeds even though it should fail, and then causes a crash when the function is actually accessed. To work around this, I've reverted to using a version check and updated the code both in this post and in the download with the changes.

Comments