[TOC]
一、基本概念
iOS系统剪贴板有两种:公共剪贴板和私有剪贴板。
公共剪贴板:用于不知道对方是什么APP之间传递数据(比如用户复制粘贴)
私有剪贴板:用于同一个Team的APP之间传递数据(比如手Q互联跳转到手Q -- iOS10以前支持该能力)
1.1 初始化方法
公共剪贴板
//Objc-C
UIPasteboard *generalPasteboard = [UIPasteboard generalPasteboard]; //快捷创建方法
UIPasteboard *generalPasteboard = [UIPasteboard pasteboardWithName:UIPasteboardNameGeneral create:TRUE];
//Swift
let gPasteBoard = UIPasteboard.general //快捷创建方法
let gPasteBoard = UIPasteboard(name: UIPasteboard.Name.init(UIPasteboardNameGeneral), create: true)
公共剪贴板有两种初始化方法,一个是快捷方法,一个是通用方法。一般使用快捷方法。
其中通用方法的第一个参数是剪贴板命名,传UIPasteboardNameGeneral
代表取的是公共剪贴板,也可以传任意字符串,则创建的是私有剪贴板;第二个参数传YES or TRUE,表示如果当前没有该Name的剪贴板,就创建一个剪贴板。
公共剪贴板的命名为com.apple.UIKit.pboard.general
PS. 在iOS10以前,还有一种公共剪贴板,命名为
UIPasteboardNameFind
,该剪贴板会记录所有在搜索框(Search Bar)里的搜索记录,该剪贴板在iOS10被系统废弃。
私有剪贴板
//Objec-C
UIPasteboard *pPasteboard = [UIPasteboard pasteboardWithName:@"privatePasteBorad" create:TRUE];
UIPasteboard *uPasteboard = [UIPasteboard pasteboardWithUniqueName];
//Swift
let pPasteBoard = UIPasteboard(name: UIPasteboard.Name.init("privatePasteBorad"), create: true)
let uPasteBoard = UIPasteboard.withUniqueName()
私有剪贴板也有两种初始化方法,一个是通用方法,一个是快捷方法。一般使用通用方法。
通用方法和创建公共剪贴板的通用方法是同一个,第一个参数传命名,第二个参数传是否要创建剪贴板。
便捷方法pasteboardWithUniqueName
是由系统随机指定一个唯一ID的命名,每次调用该方法,获取到的命名都是不一样的,最终命名像“56A00E5B-08F2-480E-8BAB-68FF93ED759F”这样。
1.2 常用属性和接口
常用属性
//查看当前剪贴板的命名
@property(readonly,nonatomic) UIPasteboardName name;
//是否持久化(iOS10该接口被废弃)
@property(readonly,getter=isPersistent,nonatomic) BOOL persistent;
//此剪切板的改变次数 系统级别的剪切板只有当设备重新启动时 这个值才会清零
@property(readonly,nonatomic) NSInteger changeCount;
常用接口
+ (void)removePasteboardWithName:(UIPasteboardName)pasteboardName;
根据命名删除剪贴板,当调用该接口删除剪贴板后,再调用[UIPasteboard pasteboardWithName:create:NO]
,得到的剪贴板为空。
PS.系统公共剪贴板通过该接口是无法删除的。
- (void)setPersistent:(BOOL)persistent; //iOS10该接口被废弃
持久化接口,可以设置私有剪贴板持久化,APP杀进程,重启后,剪贴板依旧存在,除非被卸载该剪贴板才会消失。
持久化能力在iOS10以后被废除,改用APP Groups替代。现在公共剪贴板默认持久化,私有剪贴板默认不持久化。不持久化的剪贴板,只能在创建它的进程内使用,如果想跨进程,需要使用APP Groups能力。
数据存储
剪贴板就好似一个字典,也是以<key,value>的形式存储数据,其中keys就是pasteboardTypes
,相关接口如下
@property(nonatomic, readonly) NSArray<NSString *> * pasteboardTypes;
//判断是否包含某些types的值
- (BOOL)containsPasteboardTypes:(NSArray<NSString *> *)pasteboardTypes;
//获取数据
- (nullable NSData *)dataForPasteboardType:(NSString *)pasteboardType;
- (nullable id)valueForPasteboardType:(NSString *)pasteboardType;
//存储数据
- (void)setValue:(id)value forPasteboardType:(NSString *)pasteboardType;
- (void)setData:(NSData *)data forPasteboardType:(NSString *)pasteboardType;
剪贴板有4种常用数据属性来存储字符串、链接、图片和颜色
//单数据
@property(nullable,nonatomic,copy) NSString *string;
@property(nullable,nonatomic,copy) NSURL *URL;
@property(nullable,nonatomic,copy) UIImage *image;
@property(nullable,nonatomic,copy) UIColor *color;
//多数据
@property(nullable,nonatomic,copy) NSArray<NSString *> *strings;
@property(nullable,nonatomic,copy) NSArray<NSURL *> *URLs;
@property(nullable,nonatomic,copy) NSArray<UIImage *> *images;
@property(nullable,nonatomic,copy) NSArray<UIColor *> *colors;
有时候需要知道剪贴板是否有某一种属性,直接去读这些不确定有没有内容的属性会有些耗时,在iOS10以后,苹果新增了判断属性是否存在的属性,如下:
@property (nonatomic, readonly) BOOL hasStrings;
@property (nonatomic, readonly) BOOL hasURLs;
@property (nonatomic, readonly) BOOL hasImages;
@property (nonatomic, readonly) BOOL hasColors;
剪贴板还有更直接的查看当前存储的所有数据,就是items
,items
是一个数组,里面存放的是字典,字典的key就是pasteboardType
。
@property(nonatomic,copy) NSArray<NSDictionary<NSString *, id> *> *items;
@property(readonly,nonatomic) NSInteger numberOfItems; //items数组item的个数
- (void)addItems:(NSArray<NSDictionary<NSString *, id> *> *)items; //追加item内容
当设置string
后,实际是存储到了items
里
gPasteBoard.string = @"bbb";
NSLog(@"items:%@", gPasteBoard.items);
输出如下
items:(
{
"public.utf8-plain-text" = bbb;
}
)
1.3 特性
1)覆盖写
剪贴板每次调用setXXX
都是覆盖写,比如先设置了string,再设置image,那么string会被清空掉。
//比如先设置了
gPasteBoard.string = @"aaa";
//再设置
gPasteBoard.image = image;
//再取
NSLog(gPasteBoard.string);
最终输出为“nil”。
但是调用addXXX
就不是覆盖写,而是追加,比如addItems:
。
举个例子,首先设置string
内容为“bbb”
gPasteBoard.string = @"bbb";
NSLog(@"image:%@, string:%@, url:%@, color:%@, types:%@, items:%@", gPasteBoard.image, gPasteBoard.string, gPasteBoard.URL, gPasteBoard.color, gPasteBoard.pasteboardTypes, gPasteBoard.items);
输出如下
image:(null), string:bbb, url:(null), color:(null), types:(
"public.utf8-plain-text"
), items:(
{
"public.utf8-plain-text" = bbb;
}
)
再调用addItems
去追加内容
[gPasteBoard addItems:@[@{@"public.utf8-plain-text":@"ccc",}]];
NSLog(@"image:%@, string:%@, url:%@, color:%@, types:%@, items:%@", gPasteBoard.image, gPasteBoard.string, gPasteBoard.URL, gPasteBoard.color, gPasteBoard.pasteboardTypes, gPasteBoard.items);
输出如下
image:(null), string:bbb, url:(null), color:(null), types:(
"public.utf8-plain-text"
), items:(
{
"public.utf8-plain-text" = bbb;
},
{
"public.utf8-plain-text" = ccc;
}
)
可以看到直接打印string
内容还是“bbb”,并且items里面多了一个字典,里面存储的是新追加的“ccc”。
2)一URL多用
给剪贴板设置URL
,string
也会被赋值对应的字符串。
gPasteBoard.URL = [NSURL URLWithString:@"www.aa.com"];
NSLog(@"string:%@, url:%@", gPasteBoard.string, gPasteBoard.URL);
最终输出为string:www.aa.com, url:www.aa.com
设置其他的属性呢?
UIImage* image = [UIImage imageNamed:@"001"];
UIPasteboard* gPasteBoard = [UIPasteboard generalPasteboard];
gPasteBoard.image = image;
NSLog(@"1)image:%@, string:%@, url:%@, color:%@, types:%@", gPasteBoard.image, gPasteBoard.string, gPasteBoard.URL, gPasteBoard.color, gPasteBoard.pasteboardTypes);
gPasteBoard.string = @"aaaa";
NSLog(@"2)image:%@, string:%@, url:%@, color:%@, types:%@", gPasteBoard.image, gPasteBoard.string, gPasteBoard.URL, gPasteBoard.color, gPasteBoard.pasteboardTypes);
gPasteBoard.URL = [NSURL URLWithString:@"www.aa.com"];
NSLog(@"3)image:%@, string:%@, url:%@, color:%@, types:%@", gPasteBoard.image, gPasteBoard.string, gPasteBoard.URL, gPasteBoard.color, gPasteBoard.pasteboardTypes);
gPasteBoard.color = UIColor.redColor;
NSLog(@"4)image:%@, string:%@, url:%@, color:%@, types:%@", gPasteBoard.image, gPasteBoard.string, gPasteBoard.URL, gPasteBoard.color, gPasteBoard.pasteboardTypes);
输出如下
1)image:<UIImage:0x2804d8a20 anonymous {33, 33}>, string:(null), url:(null), color:(null), types:(
"com.apple.uikit.image",
"public.png",
"public.jpeg"
)
2)image:(null), string:aaaa, url:(null), color:(null), types:(
"public.utf8-plain-text"
)
3)image:(null), string:www.aa.com, url:www.aa.com, color:(null), types:(
"public.url",
"public.utf8-plain-text"
)
4)image:(null), string:(null), url:(null), color:UIExtendedSRGBColorSpace 1 0 0 1, types:(
"com.apple.uikit.color"
)
从types中也可以看出剪贴板里存储的类型,设置URL会设置两个type,一个是url,一个是string。
二、系统版本变动
2.1 iOS9的一些变动
1)应用退后台不能访问剪贴板
在iOS9及以上,退后台再访问剪贴板,会得到nil。
该改动策略是为了安全。用户可能存在从一个APP复制密码到另一个APP粘贴的情况,如果被后台其他APP监听到,就有盗号风险。
参考:《UIPasteboard returns nil in the background》
2.2 iOS10的一些改动
1)UIPasteboardNameFind 被废弃
在iOS10以前,还有一种公共剪贴板,命名为UIPasteboardNameFind
,该剪贴板会记录所有在搜索框(Search Bar)里的搜索记录,该剪贴板在iOS10被系统废弃。
2)持久化接口(setPersistent:)被废弃
以前可以设置私有剪贴板持久化为YES,即使APP杀进程,重启后,剪贴板依旧存在,除非APP被卸载才会消失。
现在共有剪贴板默认持久化,私有剪贴板默认不持久化。
不持久化的剪贴板,只能使用在创建它的进程内。如果想跨进程,使用APP Groups能力。
3)新增便捷判断常用数据属性是否有数据的接口,并且该接口会检查数据类型
@property (nonatomic, readonly) BOOL hasStrings;
@property (nonatomic, readonly) BOOL hasURLs;
@property (nonatomic, readonly) BOOL hasImages;
@property (nonatomic, readonly) BOOL hasColors;
实际上在我试验后发现,官网说的“会检查数据类型”并不靠谱。
我故意给string设置了url数据,给url设置了string数据
UIPasteboard* gPasteBoard = [UIPasteboard generalPasteboard];
[gPasteBoard setItems:@[@{
@"public.utf8-plain-text":[NSURL URLWithString:@"www.cc.com"],
@"public.url":@"www.cc.com",
}]];
NSLog(@"image:%@, string:%@, url:%@, color:%@, types:%@, items:%@ (numberOfItems:%ld)", gPasteBoard.image, gPasteBoard.string, gPasteBoard.URL, gPasteBoard.color, gPasteBoard.pasteboardTypes, gPasteBoard.items, gPasteBoard.numberOfItems);
NSLog(@"hasStrings:%d(strings:%@), hasURLs:%d(URLs:%@)",gPasteBoard.hasStrings, gPasteBoard.strings, gPasteBoard.hasURLs, gPasteBoard.URLs);
但是输出结果来看,strings
和URLs
均为空,而hasStrings
和hasURLs
均为YES。
image:(null), string:(null), url:(null), color:(null), types:(
"public.url",
"public.utf8-plain-text"
), items:(
{
"public.url" = {length = 10, bytes = 0x7777772e63632e636f6d};
"public.utf8-plain-text" = {length = 58, bytes = 0x62706c69 73743030 a201025a 7777772e ... 00000000 00000017 };
}
) (numberOfItems:1)
hasStrings:1(strings:()), hasURLs:1(URLs:())
4)新增接口(setItems:options:)
设置有效期时间
通过设置optionUIPasteboardOptionExpirationDate
,可以设定数据的有效期,当超过有效期,数据被清空。
测试代码如下,我设置了当前时间的3秒后失效,并适用定时器来打印每秒剪贴板里的数据。
UIPasteboard* gPasteBoard = [UIPasteboard generalPasteboard];
[gPasteBoard setItems:@[@{ @"public.utf8-plain-text" : @"Happy", }] options:@{
UIPasteboardOptionExpirationDate:[[NSDate date] dateByAddingTimeInterval:3]
}];
__block int count = 1;
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_timer, ^{
NSLog(@"%d)string:%@", count++, gPasteBoard.string);
});
dispatch_resume(_timer);
输出如下
1)string:Happy
2)string:Happy
3)string:Happy
4)string:(null)
可以看到第4秒剪贴板数据被清空了。
多设备传递数据
iOS10新增功能,公共剪贴板允许在不同设备间传递数据。如果不想用此功能,可以设置optionUIPasteboardOptionLocalOnly
为YES。
UIPasteboard* gPasteBoard = [UIPasteboard generalPasteboard];
[gPasteBoard setItems:@[@{@"public.utf8-plain-text" : @"Beauty",}] options:@{
UIPasteboardOptionLocalOnly:@(YES)
}];
在测试多设备同步过程中并不顺利,主要是设置为NO允许多设备同步时,达不到很实时的状态,还可能出现先复制了A,同步A后,复制了B,同步B后,又同步了A。可能原因是各应用都有读写剪贴板的能力,可能在同步B后,猜测某个应用又把A写进去了。
但是可以确定是,UIPasteboardOptionLocalOnly为YES后是不会同步的。
2.3 iOS11的一些改动
iOS11新增对新数据类型NSItemProvider
的支持,该新数据类型多用于iPad新增的Drag&Drop能力。
//数据提供者(iOS11+)
@property (nonatomic, copy) NSArray<__kindof NSItemProvider *> *itemProviders;
- (void)setItemProviders:(NSArray<NSItemProvider *> *)itemProviders localOnly:(BOOL)localOnly expirationDate:(NSDate * _Nullable)expirationDate;
// Automatically creates item providers wrapping the objects passed in.
- (void)setObjects:(NSArray<id<NSItemProviderWriting>> *)objects;
- (void)setObjects:(NSArray<id<NSItemProviderWriting>> *)objects localOnly:(BOOL)localOnly expirationDate:(NSDate * _Nullable)expirationDate;
三、进阶用法
3.1 在剪贴板加上自定义数据且不删除原来已有的内容
公共剪贴板在不同APP间传递数据,很有可能取的时候已经有复制的内容了,通常是string
属性有内容。
而这时我们要是直接写数据到公共剪贴板是覆盖写,这时候该怎么处理?
方法一
首先,由基本概念可知,四大常用属性string
、URL
、image
、color
都是存在items
里的,并且通过打印可以发现它们对应的pasteboardType
string -> "public.utf8-plain-text"
URL -> "public.url"
image -> "com.apple.uikit.image", "public.png", "public.jpeg"
color -> "com.apple.uikit.color"
其中image
对应了三个key,直接调用gPasteBoard.image = image;
三个key都会被赋上值;反过来通过调用[gPasteBoard setItems:@[@{@"com.apple.uikit.image":image]}];
key传任意一个,再调用gPasteBoard.image
都能获取到值。
对其他三个属性也是一样,通过setItems:
方法,只要设置了对应的key,四大常用属性都能有值。
根据这一特性,我们就可以做些操作了——首先模拟剪贴板已经有数据了
UIPasteboard* gPasteBoard = [UIPasteboard generalPasteboard];
gPasteBoard.string = @"play";
NSLog(@"1) string:%@, items:%@", gPasteBoard.string, gPasteBoard.items);
输出如下
1) string:play, items:(
{
"public.utf8-plain-text" = play;
}
)
然后我们需要往里面传自定义数据userInfo
(直接设置值为字典会失败,可以转成NSData。)
NSDictionary* userInfo = @{
@"a" : @"aaa",
@"b" : [NSURL URLWithString:@"bbb"],
@"c" : UIColor.grayColor
};
NSData* userInfoData = [NSKeyedArchiver archivedDataWithRootObject:userInfo requiringSecureCoding:YES error:nil];
NSString* oString = gPasteBoard.string;
[gPasteBoard setItems:@[@{
@"public.utf8-plain-text" : oString,
@"myUserInfo" : userInfoData
}]];
NSLog(@"2) string:%@, items:%@", gPasteBoard.string, gPasteBoard.items);
输出如下
2) string:play, items:(
{
myUserInfo = {length = 45, bytes = 0x7b0a2020 22612220 3a202261 6161222c ... 20226363 63220a7d };
"public.utf8-plain-text" = play;
}
)
可以看到gPasteBoard.string
里是有原来的数据的,items
里有我们的新数据,取的时候只需要取第一个item字典,通过对应key获取即可。
方法二
在方法一的基础上,可以把setItems:
改为addItems:
,取的时候需注意遍历items
数组去取数据。
[gPasteBoard addItems:@[@{
// @"public.utf8-plain-text" : oString, /*addItems就不需要这句了
@"myUserInfo" : userInfoData
}]];
NSLog(@"2) string:%@, items:%@", gPasteBoard.string, gPasteBoard.items);
输出如下
2) string:play, items:(
{
"public.utf8-plain-text" = play;
},
{
myUserInfo = {length = 45, bytes = 0x7b0a2020 22612220 3a202261 6161222c ... 20226363 63220a7d };
}
)
方法三
除了用items
相关接口,还可以用别的比如setData:forPasteboardType:
, 随便取个type名,将自定义数据和从剪贴板读取到的原始数据封装到一起,当需要使用自定义数据时,将剪贴板原始数据再还原回去。
该方法需要考虑到从封装到解析之中是否可能会存在读取剪贴板的情况,如果会,则不适用。