理解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;

剪貼板還有更直接的查看當前存儲的所有數據,就是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);

但是輸出結果來看,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??赡茉蚴歉鲬枚加凶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名,將自定義數據和從剪貼板讀取到的原始數據封裝到一起,當需要使用自定義數據時,將剪貼板原始數據再還原回去。

該方法需要考慮到從封裝到解析之中是否可能會存在讀取剪貼板的情況,如果會,則不適用。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內容