Blog Archives

Mastering Copy and Paste in iOS – Part 2

In part 1 of this series I focused on a very simple locking down of copy and paste in iOS.  Replying NO to canPerformSelector: simply removes the ability to copy and paste from the menu that appears.  Needless to say that can be a little inelegant.  Preventing data leakage can be a good thing but making your app difficult to use is not.  In this post we’ll dig deeper into how you can interact with the copy and paste system to not only prevent leakage but also to work with more than just text.

First let’s get acquainted with some of the components of the copy and paste system.

UIPasteboard

Any time you copy or cut anything in iOS that content gets posted to an instance of UIPasteboard.  The pasteboard is a powerful and type agnostic way to move content from one application to another or between parts of a single application.  The pasteboard is capable of storing nearly any type of data, not just text.

UIMenuController

Screen Shot 2013-05-25 at 3.10.07 PM

UIMenuController is the controller that’s displayed when you highlight text.  Like many other visual controls in iOS this one is pretty locked down.  There are several system items that are added to the menu by default, but you can certainly add your own items via UIMenuItem to do whatever you like.

UIResponderStandardEditActions

This informal protocol is conformed to by descendants of UIResponder to handle the common actions provided by UIMenuController.  UIMenuController will start looking for objects in the responder chain with the first responder moving up the hierarchy until it finds a valid target for the various messages offered by the specified action, such as copy: or cut:.  As we saw in part 1, objects responding to canPerformAction: alter the options provided by the UIMenuController.

The way these three interact isn’t entirely obvious, so we’ll build a sample application that shows placing data in an application specific pasteboard, uses a custom image view that conforms to the UIResponderStandardEditActions protocol supporting cutting and pasting, and adding some custom functionality to the UIMenuController that will appear over our control.  You can download the sample project here.

Setting up the project

We’ll start with a master-detail template.  The master view controller will contain a list of table cells with an image and some text.  The detail view will show the image displayed.  This isn’t a post about table views and interface builder, so I recommend just checking out the sample code if you’re not comfortable with the basic setup.

The custom control

Our custom control will simply be a UIImageView that implements the basic methods for cut, copy, and paste.  To get the implementation correct it’s important to think about what a user’s expectation would be for these three actions:

  • copy – the image is made available to paste into other controls that can accept an image as content.
  • cut – the image is made available to paste into other controls that can accept the image and the image is removed from the current control.
  • paste – the displayed image is replaced with whatever image is in the clipboard.

Implementing these three methods from UIResponderStandardEditActions is actually quite simple:

- (void)copy:(id)sender
{
    UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
    [pasteboard setImage:self.image];
}

- (void)paste:(id)sender
{
    UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
    if (pasteboard.image) self.image = pasteboard.image;
}

- (void)thumb:(id)sender
{
    UIImage *thumb = [self thumbnailFromImage:self.image];
    [[UIPasteboard generalPasteboard] setImage:thumb];
}

Easy, right? The copy method will add the UIImageView’s image to the pasteboard via setImage. Conversely, paste will place the image from the pasteboard if it exists. Cut will first call copy to place the image to the pasteboard then clear out the image property.

Adding the UIMenuController

You can run this code now, however there’s no way to actually access the cut, copy, and paste methods.  We’ll have to do a couple of things to get the UIMenuController to appear and allow us to copy and paste the image content.

  1. Add a long press gesture recognizer to our control.
  2. On the action for gesture recognizer we’ll make our control the first responder.
  3. Lastly show the menu controller.

Adding the gesture recognizer is easy.  See the code below for creating the UIMenuController:

- (void)handleLongPress:(UILongPressGestureRecognizer *)sender
{
    NSLog(@"long press");

    UIMenuController *menu = [UIMenuController sharedMenuController];
    if (![menu isMenuVisible])
    {
        [self becomeFirstResponder];
        [menu setTargetRect:self.frame inView:self.superview];
        [menu setMenuVisible:YES animated:YES];
    }
}

You’ll notice that the menu controller is a singleton, and this is the only way to instantiate one. If the menu isn’t shown already we’ll make our image view the first responder and display the menu. The critically important step is to register ourselves as the first responder. Remember that the UIMenuController looks for the protocol methods using the responder chain. No other class in our view will respond to these methods. Without setting ourselves as the first responder the UIMenuViewController won’t display at all.

Since we implement cut, copy, and paste, those are the three buttons presented in the menu controller.  At this point you can run the app and successfully copy and paste between the image views.  You can also try jumping out to Mobile Safari, long pressing an image, selecting copy and pasting it into one of your image views if you’re still skeptical.

Adding custom actions to the menu

UIMenuController exposes the menuItems property allowing you to add custom menu items.  The menu items are added after any system items and must be instances of UIMenuItem.  We’ll add a custom menu item to copy a thumbnail of the image to the pasteboard.

Creating the thumbnail is somewhat out of scope for a detailed explanation, but here’s the code below.  It takes an image and returns one-third the size of the original (source material taken from here):

- (UIImage *)thumbnailFromImage:(UIImage *)source
{
    CGSize originalSize = source.size;
    CGSize destSize = CGSizeMake(originalSize.width / 3, originalSize.height / 3);

    UIGraphicsBeginImageContext(destSize);
    [ source drawInRect:(CGRect){CGPointZero, destSize}];
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    return newImage;
}

Next we need to create a method on our image view to place the thumbnail on the pasteboard:

- (void)thumb:(id)sender
{
    UIImage *thumb = [self thumbnailFromImage:self.image];
    [[UIPasteboard generalPasteboard] setImage:thumb];
}

Lastly we need to create the UIMenuItem to add to the UIMenuController. The UIMenuItem class is very simple and only takes a title and selector. Since we added the method thumb: to our image view that’s the selector we’ll reference in the menu item.

UIMenuItem *item = [[UIMenuItem alloc] initWithTitle:@"Copy Thumbnail" action:@selector(thumb:)];
menu.menuItems = @[item];

We’ll place this code right before showing the menu controller in the long press handler. You can now run the app, select “Copy Thumbnail” and you’ll see the image pasted is a thumbnail relative to the original.

Note: If you created the nib for the detail view yourself, it’s important the view mode to “center” to avoid the image view stretching the thumbnail when you paste it.

Creating an application specific pasteboard

Up until this point we’ve been using the standard general pasteboard but the SDK provides us other options as well. The only requirement for creating a new one is that the name must be unique. You might also mark a pasteboard as persistent so it will live on after your app terminates. This isn’t particularly useful for a single application but can be extremely powerful for preventing data leakage while still maintaining copy and paste functionality between a suite of apps.

We’ll define the custom pasteboard name in the application delegate’s header file. We can import that into any other file where I need the pasteboard name.

// App delegate header
extern NSString *const pasteboardIdentifier;

I’ll also initialize the pasteboard in applicationDidLoad method on the app delegate:

// app delegate implementation file
// make sure to define the extern
NSString *const pasteboardIdentifier = @"com.pzearfoss.customCopyPaste";

// ...
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // ...
    [UIPasteboard pasteboardWithName:pasteboardIdentifier create:YES];
    // ...
}

Lastly, in my image view I’ll replace the generalPasteboard call with pasteboardWithName:create:

- (void)copy:(id)sender
{
    UIPasteboard *pasteboard = [UIPasteboard pasteboardWithName:pasteboardIdentifier create:NO];
    [pasteboard setImage:self.image];
}

- (void)paste:(id)sender
{
    UIPasteboard *pasteboard = [UIPasteboard pasteboardWithName:pasteboardIdentifier create:NO];
    if (pasteboard.image) self.image = pasteboard.image;
}

- (void)thumb:(id)sender
{
    UIImage *thumb = [self thumbnailFromImage:self.image];
    [[UIPasteboard pasteboardWithName:pasteboardIdentifier create:NO] setImage:thumb];
}

Running the application you’ll now see that you can copy and paste between image views as you could before, but if you try to copy from another app like Mobile Safari it will not paste in our app.

That concludes part 2 for implementing copy and paste, UIMenuController, and using an app specific UIPasteboard. Check out part 3 where we’ll store more complex data types in the pasteboard.