Compiled Chronicles

A software development blog by Angelo Villegas

iOS: UIPasteboard, UIMenuController & UIMenuItem

There are times that you need to copy and paste items on your device, few words won’t be a problem but several paragraphs will. UIPasteboard is a built-in class that allows you to copy items to the pasteboard in iOS and paste it somewhere else. A user then selects an item (e.g., words, images, etc…) from any field where text and images are allowed to be copied and paste the contents currently in the pasteboard. There’s a high chance your app will have data or information that users will need to copy and paste to save it somewhere else such as the Notes app. Flight plans, PDF, images, or a full paragraph, you name it, users are forced to solve problems creatively for lack of a provided solution.

Class API

Let’s look at a simple implementation, and then dive into some specifics about the APIs.

[[UIPasteboard generalPasteboard] setString:self.text];

gerenalPasteboard returns the general pasteboard, which is used for general copy-paste operations. You may use the general pasteboard for copying and pasting text, images, URLs, colors, and other data within an app or between apps. The general pasteboard is persistent across device restarts and app uninstalls. setString: is a setter of the string property that sets the string value of the first pasteboard item. The value stored in this property is an NSString object. The associated array of representation types is UIPasteboardTypeListString, which includes type kUTTypeUTF8PlainText. Setting this property replaces all current items in the pasteboard with the new item. If the first item has no value of the indicated type, nil is returned. There are three ways to initialise an instance of UIPasteboard:

+ (UIPasteboard *)generalPasteboard;
+ (UIPasteboard *)pasteboardWithName:(NSString *)pasteboardName create:(BOOL)create;
+ (UIPasteboard *)pasteboardWithUniqueName;

The general pasteboard is persistent across device restarts and app uninstalls.

The first, generalPasteboard was explained above. The second method returns a pasteboard identified by name using the parameter pasteboardName, optionally creating it if it doesn’t exist. The third returns an app pasteboard identified by a unique system-generated name.

Coding in Style

There are different ways of implementing a copy and paste process, subclass or not, it will be your own decision what suits your app best. You can create a button with a method that will copy a certain text from a label or a textview:

- (void)copyButtonPressed:(id)sender
{
    UIPasteboard *generalPasteboard = [UIPasteboard generalPasteboard];
    generalPasteboard.string = self.label.text;
}

This is not applicable many objects in a view though, so it’s best to subclass certain objects.

Subclassing

The code is a little more involved if you want to use custom menu options, but offers a lot of flexibility. It’s your responsibility to detect the long press and show the custom menu, and the easiest way to do this is using UILongPressGestureRecognizer.

UILabel must be subclassed to implement canBecomeFirstResponder and canPerformAction:withSender:

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. This feature is present since iOS 3 but is rarely used by application developers even though it’s very easy to implement.

PasteboardLabel.{h,m}

@interface PasteboardLabel : UILabel

@end

@implementation PasteboardLabel

- (BOOL)canBecomeFirstResponder
{
    return YES;
}

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
    return (action == @selector(copy:));
}

#pragma mark - UIResponderStandardEditActions
- (void)copy:(id)sender
{
    UIPasteboard *generalPasteboard = [UIPasteboard generalPasteboard];
    generalPasteboard.string = self.text;
}

@end

ViewController.m

- (void)viewDidLoad
{
    PasteboardLabel *label = [[PasteboardLabel alloc] initWithRect:...];
    label.userInteractionEnabled = YES;
    [self.view addSubview:label];
    
    UIGestureRecognizer *gestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressGestureRecognizer:)];
    [label addGestureRecognizer:gestureRecognizer];
}

#pragma mark - Gestures
- (void)longPressGestureRecognizer:(UIGestureRecognizer *)recognizer
{
    UIMenuController *menuController = [UIMenuController sharedMenuController];
    [menuController setTargetRect:recognizer.view.frame inView:recognizer.view.superview];
    [menuController setMenuVisible:YES animated:YES];
    [recognizer.view becomeFirstResponder];
}

The most important part of the codes above is: UIMenuController *menuController = ...

This small bit of code instantiate a UIMenuController object that displays a menu with a long press. There are, however, limitations for labels such as the select: and selectAll: methods does not work.

You’ll notice that the menu controller is a singleton, and this is the only way to instantiate one.

To control which actions to include, you should modify the method canPerformAction:withSender: accordingly. The default implementation of this method returns YES if the responder class implements the requested action and calls the next responder if it does not. Subclasses may override this method to enable menu commands based on the current state; for example, you would enable the Copy command if there is a selection or disable the Paste command if the pasteboard did not contain data with the correct pasteboard representation type. If no responder in the responder chain returns YES, the menu command is disabled. Note that if your class returns NO for a command, another responder further up the responder chain may still return YES, enabling the command.

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
    return (action == @selector(copy:));
}

Pasting to UIImageView

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

The copy: method will add the UIImageView’s image to the pasteboard via the image property while paste: will place the image from the pasteboard if it exists.

UIResponderStandardEditActions

Note: These methods are not implemented in NSObject

Standard Edit Actions
NameiOSDescription
increaseSize:7.0The system calls this action method in response to the user pressing Command-plus (+) on an attached hardware keyboard. Typical responses for this type of event are to increase the font size of text or to change the zoom level of scroll views.
decreaseSize:7.0The system calls this action method in response to the user pressing Command-minus (-) on an attached hardware keyboard. Typical responses for this type of event are to decrease the font size of text or to change the zoom level of scroll views.

Custom Menu Item

There are situations you’ll want to add custom items in the menu, you can do so by initialising a UIMenuItem object and passing it to the UIMenuController object. UIMenuController has a menuItems property, which is an NSArray of UIMenuItem objects. You can create a method such as:

ViewController.h
- (void)longPressGestureRecognizer:(UIGestureRecognizer *)recognizer
{
    UIMenuItem *removeItem = [self.label menuItemRemove];
    UIMenuController *menuController = ...
    ...
    menuController.menuItems = @[ removeItem ];
}
PasteboardLabel.{h,m}
@interface PasteboardLabel : UILabel

- (UIMenuItem *)menuItemRemove;

@end

@implementation PasteboardLabel

- (UIMenuItem *)menuItemRemove
{
    return [[UIMenuItem alloc] initWithTitle:@"Remove" action:@selector(remove:)];
}

- (void)remove:(id)sender
{
    self.text = @"";
}

@end

By the help of UIMenuItem, you can make a powerful menu designed for rich text editing and the likes. One thing to remember though with UIMenuItem, if the specified action is not implemented, that item will not appear in the menu.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *