拷贝、剪切、和粘贴操作
在iPhone OS 3.0之后,用户可以在一个应用程序上拷贝文本、图像、或其它数据,然后粘贴到当前或其它应用程序的不同位置上。比如,您可以从某个电子邮件中拷贝一个地址,然后粘贴到Contacts程序的地址域中。目前,UIKit框架在UITextView
、UITextField
、和UIWebView
类中实现了拷贝-剪切-粘贴支持。如果您希望在自己的应用程序中得到这个行为,可以使用这些类的对象,或者自行实现。
本文的下面部分将描述UIKit中用于拷贝、剪切、和粘贴操作的编程接口,并解释其用法。
请注意:与拷贝和粘贴操作相关的使用指南,请参见iPhone人机界面指南文档中的“支持拷贝和粘贴”部分。
UIKit中支持拷贝-粘贴操作的设施
UIKit框架提供几个类和一个非正式协议,用于为应用程序中的拷贝、剪切、和粘贴操作提供方法和机制。具体如下:
-
UIPasteboard
类提供了粘贴板的接口。粘贴板是用于在一个应用程序内或不同应用程序间进行数据共享的受保护区域。该类提供了读写剪贴板上数据项目的方法。 -
UIMenuController
类可以在选定的拷贝、剪切、和粘贴对象的上下方显示一个编辑菜单。编辑菜单上的命令可以有拷贝、剪切、粘贴、选定、和全部选定。 -
UIResponder
类声明了canPerformAction:withSender:
方法。响应者类可以实现这个方法,以根据当前的上下文显示或移除编辑菜单上的命令。 -
UIResponderStandardEditA
非正式协议声明了处理拷贝、剪切、粘贴、选定、和全部选定命令的接口。当用户触碰编辑菜单上的某个命令时,相应的ctions UIResponderStandardEditA
方法就会被调用。ctions
粘贴板的概念
粘贴板是同一应用程序内或不同应用程序间交换数据的标准化机制。粘贴板最常见的的用途是处理拷贝、剪贴、和粘贴操作:
-
当用户在一个应用程序中选定数据并选择拷贝(或剪切)菜单命令时,被选择的数据就会被放置在粘贴板上。
-
当用户选择粘贴命令时(可以在同一或不同应用程序中),粘贴板上的数据就会被拷贝到当前应用程序上。
在iPhone OS中,粘贴板也用于支持查找(Find)操作。此外,还可以用于在不同应用程序间通过定制的URL类型传输数据(而不是通过拷贝、剪切、和粘贴命令,关于这个技巧的信息请参见“和其它应用程序间的通讯”部分。
无论是哪种操作,您通过粘贴板执行的基本任务是读写粘贴板数据。虽然这些任务在概念上很简单,但是它们屏蔽了很多重要的细节。复杂的原因主要在于数据的表现方式可能有很多种,而这个复杂性又引入了效率的考虑。本文的下面部分将对这些以及其它的问题进行讨论。
命名粘贴板
粘贴板可能是公共的,也可能是私有的。公共粘贴板被称为系统粘贴板;私有粘贴板则由应用程序自行创建,因此被称为应用程序粘贴板。粘贴板必须有唯一的名字。UIPasteboard
定义了两个系统粘贴板,每个都有自己的名字和用途:
-
UIPasteboardNameGeneral
用于剪切、拷贝、和粘贴操作,涉及到广泛的数据类型。您可以通过该类的generalPasteboard
类方法来取得代表通用(General)粘贴板的单件对象。 -
UIPasteboardNameFind
用于检索操作。当前用户在检索条(UISearchBar
)键入的字符串会被写入到这个粘贴板中,因此可以在不同的应用程序中共享。您可以通过调用pasteboardWithName:create:
类方法,并在名字参数中传入UIPasteboardNameFind
值来取得代表检索粘贴板的对象。
典型情况下,您只需使用系统定义的粘贴板就够了。但在必要时,您也可以通过pasteboardWithName:create:
方法来创建自己的应用程序粘贴板。如果您调用pasteboardWithUniqueName
方法,UIPasteboard
会为您提供一个具有唯一名称的应用程序粘贴板。您可以通过其name
属性声明来取得这个名称。
粘贴板的持久保留
您可以将粘贴板标识为持久保留,使其内容在当前使用的应用程序终止后继续存在。不持久保留的粘贴板在其创建应用程序退出后就会被移除。系统粘贴板是持久保留的,而应用程序粘贴板在缺省情况下是不持久保留的。将其应用程序粘贴板的persistent
属性设置为YES
可以使其持久保留。当持久粘贴板的拥有者程序被用户卸载时,其自身也会被移除。
粘贴板的拥有者和数据项
最后将数据放到粘贴板的对象被称为该粘贴板的拥有者。放到粘贴板上的每一片数据都称为一个粘贴板数据项。粘贴板可以保有一个或多个数据项。应用程序可以放入或取得期望数量的数据项。举例来说,假定用户在视图中选择的内容包含一些文本和一个图像,粘贴板允许您将文本和图像作为不同的数据项进行拷贝。从粘贴板读取多个数据项的应用程序可以选择只读取被支持的数据项(比如只是文本,而不支持图像)。
重要提示:当一个应用程序将数据写入粘贴板时,即使只是单一的数据项,该数据也会取代粘贴板的当前内容。虽然您可能使用UIPasteboard
的addItems:
方法来添加项目,但是该写入方法并不会将那些项目加入到粘贴板当前内容之后。
数据的表示和UTI
粘贴板操作经常在不同的应用程序间执行。系统并不要求应用程序了解对方的信息,包括对方可以处理的数据种类。为了最大化潜在的数据分享能力,粘贴板可以保留同一个数据项的多种表示。例如,一个富文本编辑器可以提供被拷贝数据的HTML、PDF、和纯文本表示。粘贴板上的一个数据项包括应用程序可为该数据提供的所有表示。
粘贴板数据项的每种表示通常都有一个唯一类型标识符(Unique Type Identifier,缩写为UTI)。UTI简单定义为一个唯一标识特定数据类型的字符串。UTI提供了一个标识数据类型的常用手段。如果您希望支持一个定制的数据类型,就必须为其创建一个唯一的标识符。为此,您可以用反向DNS表示法来定义类型标识字符串,以确保其唯一性。例如,您可以用com.myCompany.myApp.myType
来表示一个定制的类型标识。更多有关UTI的信息请参见统一类型标识符概述。
作为例子,假定一个应用程序支持富文本和图像的选择,它可能希望将富文本和Unicode版本的选定文本,以及选定图像的不同表示放到粘贴板上。在这样的场景下,每个数据项的每种表示都和它自己的数据一起保存,如图3-3所示。
一般情况下,为了最大化潜在的共享可能性,粘贴板数据项应该包括尽可能多的表示。
粘贴板的读取程序必须找到最适合自身能力(如果有的话)的数据类型。通常情况下,这意味着选择内涵最丰富的可用类型。举例来说,一个文本编辑器可能为被拷贝的数据提供HTML(富文本)和纯文本表示,支持富文本的应用程序应该选择HTML表示,而只支持纯文本的应用程序则应该选择纯文本的表示。
变化记数
变化记数是每个粘贴板都有的变量,它随着每次粘贴板内容的变化而递增—特别是发生增加、修改、或移除数据项的时候。应用程序可以通过考察变化记数(通过changeCount
属性)来确定粘贴板的当前数据是否和最后一次取得的数据相同。每次变化记数递增时,粘贴板都会向对此感兴趣的观察者发送通告。
选择和菜单管理
在拷贝或剪切视图中的某些内容之前,必须首先选择“某些内容”。它可能是一些文本、一个图像、一个URL、一种颜色、或者其它类型的数据,包括定制对象。为了在定制视图中实现拷贝-和-粘贴行为,您必须自行管理该视图中对象的选择。如果用户通过特定的触摸手势(比如双击)来选择视图中的对象,您就必须处理该事件,即在程序内部记录该选择(同时取消之前的选择),可能还要在视图中指示新的选择。如果用户可以在视图中选择多个对象,然后进行拷贝-剪切-粘贴操作,您就必须实现多选的行为。
请注意:触摸事件及其处理技巧在“触摸事件”部分进行讨论。
当应用程序确定用户请求了编辑菜单时—可能就是一个选择的动作—您应该执行下面的步骤来显示菜单:
-
调用
UIMenuController
的sharedMenuController
类方法来取得全局的,即菜单控制器实例。 -
计算选定内容的边界,并用得到的边界矩形调用
setTargetRect:inView:
方法。系统会根据选定内容与屏幕顶部和底部的距离,将编辑菜单显示在该矩形的上方或下方。 -
调用
setMenuVisible:animated:
方法(两个参数都传入YES
),在选定内容的上方或下方动画显示编辑菜单。
程序清单3-4演示了如何在touchesEnded:withEvent:
方法的实现中显示编辑菜单(注意,例子中省略了处理选择的代码)。在这个代码片段中,定制视图还向自己发送一个becomeFirstResponder
消息,确保自己在随后的拷贝、剪切、和粘贴操作中是第一响应者。
程序清单3-4
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { |
UITouch *theTouch = [touches anyObject]; |
|
if ([theTouch tapCount] == 2 && [self becomeFirstResponder]) { |
|
// selection management code goes here... |
|
// bring up editing menu. |
UIMenuController *theMenu = [UIMenuController sharedMenuController]; |
CGRect selectionRect = CGRectMake(currentSelection.x, currentSelection.y, SIDE, SIDE); |
[theMenu setTargetRect:selectionRect inView:self]; |
[theMenu setMenuVisible:YES animated:YES]; |
|
} |
} |
初始的菜单包含所有的命令,因此第一响应者提供了相应的UIResponderStandardEditA
方法的实现(copy:
、paste:
等)。但是在菜单被显示之前,系统会向第一响应者发送一个canPerformAction:withSender:
消息。在很多情况下,第一响应者就是定制视图的本身。在该方法的实现中,响应者考察给定的命令(由第一个参数传入的选择器表示)是否适合当前的上下文。举例来说,如果该选择器是paste:
,而粘贴板上没有该视图可以处理的数据,则响应者应该返回NO
,以便禁止粘贴命令。如果第一响应者没有实现canPerformAction:withSender:
方法,或者没有处理给定的命令,该消息就会进入响应者链。
程序清单3-5展示了canPerformAction:withSender:
方法的一个实现。该实现首先寻找和copy:
、copy:
、及paste:
选择器相匹配的消息,并根据当前选择的上下文激活或禁用拷贝、剪切、和粘贴菜单命令。对于粘贴命令,还考虑了粘贴板的内容。
程序清单3-5
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender { |
BOOL retValue = NO; |
ColorTile *theTile = [self colorTileForOrigin:currentSelection]; |
|
if (action == @selector(paste:) ) |
retValue = (theTile == nil) && |
[[UIPasteboard generalPasteboard] containsPasteboardTypes: |
[NSArray arrayWithObject:ColorTileUTI]]; |
else if ( action == @selector(cut:) || action == @selector(copy:) ) |
retValue = (theTile != nil); |
else |
retValue = [super canPerformAction:action withSender:sender]; |
return retValue; |
} |
请注意,这个方法的最后一个else
子句调用了超类的实现,使超类有机会处理子类忽略的命令。
还要注意,操作一个菜单命令可能会改变其它菜单命令的上下文。比如,当用户选择视图中的所有对象时,拷贝和剪切命令就应该被包含在菜单中。在这种情况下,虽然菜单仍然可见,但是响应者可以调用菜单控制器的update
方法,使第一响应者的canPerformAction:withSender:
再次被调用。
拷贝和剪切选定的内容
当用户触碰编辑菜单上的拷贝或剪切命令时,系统会分别调用响应者对象的copy:
或cut:
方法。通常情况下,第一响应者—也就是您的定制视图—会实现这些方法,但如果没有实现的话,该消息会按正常的方式进入响应者链。请注意,UIResponderStandardEditA
非正式协议声明了这些方法。
请注意:由于UIResponderStandardEditA
是非正式协议,应用程序中的任何类都可以实现它的方法。但是,为了使命令可以按缺省的方式在响应者链上传递,实现这些方法的类应该继承自UIResponder
类,且应该被安装到响应者链中。
在copy:
或cut:
消息的响应代码中,您需要把和选定内容相对应的对象或数据以尽可能多的表示形式写入到粘贴板上。这个操作涉及到如下这些步骤(假定只有一个的粘贴板数据项):
-
标识或取得和选定内容相对应的对象或二进制数据。
二进制数据必须封装在
NSData
对象中。其它可以写入到粘贴板的对象必须是属性列表对象—也就是说,必须是下面这些类的对象:NSString
、NSArray
、NSDictionary
、NSDate
、NSNumber
、或者NSURL
(有关属性列表对象的更多信息,请参见属性列表编程指南)。 -
可能的话,请为对象或数据生成一或多个其它的表示。
举例来说,在之前提到的为选定图像创建
UIImage
对象的步骤中,您可以通过UIImageJPEGRepresentatio
或n UIImagePNGRepresentation
函数将图像转换为不同的表示。 -
取得粘贴板对象。
在很多情况下,使用通用粘贴板就可以了。您可以通过
generalPasteboard
类方法来取得该对象。 -
为写入到粘贴板数据项的每个数据表示分配一个合适的UTI。
这个主题的讨论请参见“粘贴板的概念”部分。
-
将每种表示类型的数据写入到第一个粘贴板数据项中:
-
向粘贴板对象发送
setData:forPasteboardType:
消息可以写入数据对象。 -
向粘贴板对象发送
setValue:forPasteboardType:
消息可以写入属性列表对象。
-
- 对于剪切(
cut:
方法)命令,需要从应用程序的数据模型中移除选定内容所代表的对象,并更新视图。
程序清单3-6展示了copy:
和cut:
方法的一个实现。cut:
方法调用了copy:
方法,然后从视图和数据模型中移除选定的对象。注意,copy:
方法对定制对象进行归档,目的是得到一个NSData
对象,以便作为参数传递给粘贴板的setData:forPasteboardType:
方法。
程序清单3-6
- (void)copy:(id)sender { |
UIPasteboard *gpBoard = [UIPasteboard generalPasteboard]; |
ColorTile *theTile = [self colorTileForOrigin:currentSelection]; |
if (theTile) { |
NSData *tileData = [NSKeyedArchiver archivedDataWithRootObje |
if (tileData) |
[gpBoard setData:tileData forPasteboardType:ColorTileUTI]; |
} |
} |
|
- (void)cut:(id)sender { |
[self copy:sender]; |
ColorTile *theTile = [self colorTileForOrigin:currentSelection]; |
|
if (theTile) { |
CGPoint tilePoint = theTile.tileOrigin; |
[tiles removeObject:theTile]; |
CGRect tileRect = [self rectFromOrigin:tilePoint inset:TILE_INSET]; |
[self setNeedsDisplayInRect:tileRect]; |
} |
} |
粘贴选定内容
当用户触碰编辑菜单上的粘贴命令时,系统会调用响应者对象的paste:
方法。通常情况下,第一响应者—也就是您的定制视图—会实现这些方法,但如果没有实现的话,该消息会按正常的方式进入响应者链。paste:
方法在UIResponderStandardEditA
非正式协议中声明。
在paste:
-
取得粘贴板对象。
在很多情况下,使用通用粘贴板就可以了,您可以通过
generalPasteboard
类方法来取得该对象。 -
确认第一个粘贴板数据项是否包含应用程序可以处理的表示,这可以通过调用
containsPasteboardTypes:
方法,或者调用pasteboardTypes
方法并考察其返回的类型数组来实现。请注意,您在
canPerformAction:withSender:
方法的实现中应该已经执行过这个步骤。 -
如果粘贴板的第一个数据项包含应用程序可以处理的数据,则可以调用下面的方法来读取:
-
dataForPasteboardType:
,如果要读取的数据被封装为NSData
对象,就可以使用这个方法。 -
valueForPasteboardType:
,如果要读取的数据被封装为属性列表对象,请使用这个方法(请参见“拷贝和剪切选定的内容”部分)。
-
-
将对象加入到应用程序的数据模型中。
- 将对象的表示显示在用户界面中用户指定的位置上。
程序清单3-7是paste:
方法的一个实现实例,该方法执行与cut:
及copy:
方法相反的操作。示例中的视图首先确认粘贴板是否包含自身支持的定制表示数据,如果是的话,就读取该数据并将它加入到应用程序的数据模型中,然后将视图的一部分—当前选定区域—标识为需要重画。
程序清单3-7
- (void)paste:(id)sender { |
UIPasteboard *gpBoard = [UIPasteboard generalPasteboard]; |
NSArray *pbType = [NSArray arrayWithObject:ColorTileUTI]; |
ColorTile *theTile = [self colorTileForOrigin:currentSelection]; |
if (theTile == nil && [gpBoard containsPasteboardTypes:pbType]) { |
|
NSData *tileData = [gpBoard dataForPasteboardType:ColorTileUTI]; |
ColorTile *theTile = (ColorTile *)[NSKeyedUnarchiver unarchiveObjectWithData:tileData]; |
if (theTile) { |
theTile.tileOrigin = self.currentSelection; |
[tiles addObject:theTile]; |
CGRect tileRect = [self rectFromOrigin:currentSelection inset:TILE_INSET]; |
[self setNeedsDisplayInRect:tileRect]; |
} |
} |
} |
消除编辑菜单
在您实现的cut:
、copy:
、或paste:
命令返回后,编辑菜单会被自动隐藏。通过下面的代码使它保持可见:
[UIMenuController setMenuController].menuVisible = YES; |
系统可能在任何时候隐藏编辑菜单,比如当显示警告信息或用户触碰屏幕其它区域时,编辑菜单就会被隐藏。如果您有某些状态或屏幕显示需要依赖于编辑菜单是否显示的话,就应该侦听UIMenuControllerWillHide
通告,并执行恰当的动作。