[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??赡茉蚴歉鲬枚加凶x寫剪貼板的能力,可能在同步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名,將自定義數據和從剪貼板讀取到的原始數據封裝到一起,當需要使用自定義數據時,將剪貼板原始數據再還原回去。
該方法需要考慮到從封裝到解析之中是否可能會存在讀取剪貼板的情況,如果會,則不適用。