理解iOS系统剪贴板

[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;

剪贴板还有更直接的查看当前存储的所有数据,就是itemsitems是一个数组,里面存放的是字典,字典的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多用

给剪贴板设置URLstring也会被赋值对应的字符串。

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);

但是输出结果来看,stringsURLs均为空,而hasStringshasURLs均为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属性有内容。
而这时我们要是直接写数据到公共剪贴板是覆盖写,这时候该怎么处理?

方法一

首先,由基本概念可知,四大常用属性stringURLimagecolor都是存在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名,将自定义数据和从剪贴板读取到的原始数据封装到一起,当需要使用自定义数据时,将剪贴板原始数据再还原回去。

该方法需要考虑到从封装到解析之中是否可能会存在读取剪贴板的情况,如果会,则不适用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容