要用iCloud來實現(xiàn)本地文件與云端文件的同步溜歪,首先要處理好邏輯的問題漂佩。
什么時候上傳庄岖?什么時候下載豁翎?先上傳還是先下載?如何處理文件沖突隅忿?
我要實現(xiàn)的是每天保存一張圖片文件(當天過后就不能再修改這個圖片文件)心剥,用來記錄自己的生活狀態(tài),所以文件系統(tǒng)比較簡單背桐,我希望我的app邏輯也盡量簡單优烧,這樣比較好維護。所以我設(shè)想了一個最簡單的同步邏輯:
點擊同步按鈕后牢撼,先上傳本地 所有的文件匙隔,如果發(fā)現(xiàn)要上傳的文件在云端已經(jīng)有了同名文件,那么就不上傳這一張圖片文件。然后再從云端下載所有文件到本地纷责,同樣捍掺,檢查本地是否有同名文件,如果有再膳,則不下載這張圖片文件挺勿。最后,每次創(chuàng)建了一張圖片文件時喂柒,就上傳到云端不瓶,執(zhí)行覆蓋操作,也就是有同名文件的話灾杰,就把云端的同名文件覆蓋蚊丐,以本地上傳的為主。
這個邏輯我覺得簡單粗暴艳吠,可行麦备。那么就開始吧。
開啟iCloud Document 能力
在Targets ->Capabilities->iCloud 中打開iCloud特性昭娩,如下圖:
UIDocument
我們使用UIDocument這個類來處理文件的上傳和下載的凛篙,不能直接使用這個類,而要創(chuàng)建它的子類栏渺,并重新實現(xiàn)下面兩個方法來處理數(shù)據(jù):
import UIKit
class MyDocument: UIDocument {
var data:NSData?
var imgData:NSData?
//處理文件上傳
override func contents(forType typeName: String) throws -> Any {
if typeName == "public.png" {
return imgData ?? NSData()
} else {
return Data()
}
}
//處理文件下載
override func load(fromContents contents: Any, ofType typeName: String?) throws {
if let userContent = contents as? NSData {
data = userContent
}
}
}
當我在其它地方使用這個類的實例對象document執(zhí)行save操作的時候呛梆,就會走 contents(forType typeName: String) 方法來把這個方法里面返回值上傳到iCloud,對于我們也就是imgData磕诊;所以我們在執(zhí)行save操作前填物,要給MyDocument里面的imgdata賦值。
document.save(to: ubiquityURL2,
for: .forOverwriting,
completionHandler: {(success: Bool) -> Void in
if success {
print("iCloud create OK \(ubiquityURL2)")
} else {
print("iCloud create failed \(ubiquityURL2)")
}
})
//在執(zhí)行save操作前秀仲,要給MyDocument里面的imgdata賦值融痛。
let img = UIImage.init(contentsOfFile: fullpath);
if (img != nil) {
let imgData = NSData.init(contentsOfFile: fullpath)
document.imgData = imgData
}
而document在執(zhí)行open操作的時候,也就是保存文件到本地神僵,這個時候會走 load(fromContents contents: Any, ofType typeName: String?)方法雁刷,這個方法里面的參數(shù)contents就是iCloud傳給我們的數(shù)據(jù),我們可以對這個數(shù)據(jù)進行處理保礼。在這里我使用一個data接收了這個數(shù)據(jù)沛励,然后在document.open的success的block里面進行處理,把它保存到了本地炮障。
document.open(completionHandler: {(success: Bool) -> Void in
if success {
let data = document.data
if data == nil {
print("iCloud file open failed,error ,data = nil ")
}else {
Utils.saveFileToImgFolder(fileName: fileName!, data: data!)
print("iCloud file open OK")
}
} else {
print("iCloud file open failed")
}
})
ubiquityURL
你肯定已經(jīng)注意到了目派,在上面我說的document.save方法里面,有一個參數(shù)ubiquityURL2胁赢,它前面有一個to企蹭,這個就是ubiquityURL,它說明了了一個文件在iCloud中保存的位置信息。
舉個例子:我的bundle id如果是com.test.demo,那么對應(yīng)的ubiquityURL是類似這種:file:///private/var/mobile/Library/Mobile%20Documents/iCloudcomtest~demo.我們需要在后面拼接上Documents來表示這個文件是保存在iCloud中的我的這個應(yīng)用的Documents下谅摄。也就是說iCloudcomtest~demo是用來區(qū)分是哪個應(yīng)用的徒河,iCloudcomtest~demo后面的是用來區(qū)分應(yīng)用下文件路徑的。
獲取你的應(yīng)用的跟ubiquityURL的方法是通過以下方法:
func initUbiquityURL() {
let filemgr = FileManager.default
let ubiquityURL = filemgr.url(forUbiquityContainerIdentifier: nil)
//如果ubiquityURL = nil送漠,可能是沒有登錄賬戶顽照,或者沒有打開iCloud
guard ubiquityURL != nil else {
print("Unable to access iCloud Account")
print("Open the Settings app and enter your Apple ID into iCloud settings")
return
}
}
需要說明的是,MyDocument的初始化是需要ubiquityURL的闽寡,準確來說是它處理的這個文件的ubiquityURL代兵。可以這么理解爷狈,UIDocument的每個實例都是處理一個文件植影,不同的文件處理會創(chuàng)建不同的實例。
let document = MyDocument(fileURL: ubiquityURL2)
例如:這里的ubiquityURL2=file:///private/var/mobile/Library/Mobile%20Documents/iCloudcomtest~demo/Documents/2017-03-02.png
查詢云端文件
我們需要獲取到云端的文件淆院,其實也是需要知道云端文件對應(yīng)的ubiquityURL何乎,而本地的文件也可以轉(zhuǎn)成對應(yīng)的ubiquityURL(這個后面說),所以這樣土辩,云端和本地就構(gòu)成了一種對應(yīng)的關(guān)系。
查詢云端文件抢野,可以主動調(diào)用下面的方法:
//查詢云端的文件
func askForCloudDataQuery() {
metaDataQuery = NSMetadataQuery()
metaDataQuery?.searchScopes =
[NSMetadataQueryUbiquitousDocumentsScope]
NotificationCenter.default.addObserver(self,
selector: #selector(
CloudDataManager.metadataQueryDidFinishGathering),
name: NSNotification.Name.NSMetadataQueryDidFinishGathering,
object: metaDataQuery!)
metaDataQuery!.start()
}
查詢到結(jié)果拷淘,可以走下面的回調(diào)方法。
考慮到不需要查詢那么多次指孤,我直接查詢到Document下所有的文件启涯,然后根據(jù)文件名與本地文件名作對比。比對出兩個包含ubiquityURL元素的數(shù)組恃轩,一個是需要上傳到云端的结洼,一個是需要下載到本地的,然后分別進行比對操作叉跛。
//獲取到云端的所有的文件的url松忍,保存到cloudURLs數(shù)組里面
func metadataQueryDidFinishGathering(notification: NSNotification) -> Void
{
let query: NSMetadataQuery = notification.object as! NSMetadataQuery
query.disableUpdates()
NotificationCenter.default.removeObserver(self,
name: NSNotification.Name.NSMetadataQueryDidFinishGathering,
object: query)
query.stop()
print("query.valueLists = \(query.valueLists),query.resultCount = \(query.resultCount)")
if cloudURLs == nil {
cloudURLs = Array.init()
}else {
cloudURLs?.removeAll()
}
if query.resultCount != 0 {
let count = (query.results as Array).count
for i in 0..<count{
let resultURL = query.value(ofAttribute: NSMetadataItemURLKey,
forResultAt: i) as! URL
cloudURLs?.append(resultURL)
}
print("cloudURLs = \(cloudURLs!)")
//下載到本地
self.downloadFileToLocal()
}
//上傳到云端
self.uploadFileToCloud()
}
//獲取要上傳到icloud的url的數(shù)組
func getNeedUploadToCloudUbiquityURLArray() -> [URL] {
var finalArr = localURLs
for local in localURLs! {
for cloud in cloudURLs! {
if cloud == local {
let index = finalArr?.index(of: local)
finalArr?.remove(at: index!)
continue
}
}
}
print("getNeedUploadToCloudUbiquityURLArray finalArr = \(finalArr!)")
return finalArr!
}
獲得本地文件
//獲得本地所有文件的需要上傳到cloud的時候的url,保存到localURLs數(shù)組里面
func initLocalURLs() {
if localURLs == nil {
localURLs = Array.init()
}else {
localURLs?.removeAll()
}
let allImgNames = Utils.getAllFileNameInImgFolder()
for imgName in allImgNames {
let imgURL = ubiquityURLOfImg?.appendingPathComponent(imgName)
localURLs?.append(imgURL!)
}
print("localURLs = \(localURLs)")
}
//獲取要保存到本地的url的數(shù)組
func getNeedDownloadTolocalUbiquityURLArray() -> [URL] {
var finalArr = cloudURLs
for cloud in cloudURLs! {
for local in localURLs! {
if cloud == local {
let index = finalArr?.index(of: cloud)
finalArr?.remove(at: index!)
continue
}
}
}
print("getNeedDownloadTolocalUbiquityURLArray finalArr = \(finalArr!)")
return finalArr!
}
上傳下載操作
獲取到要上傳和下載的數(shù)組后筷厘,進行上傳和下載的操作就可以了鸣峭。
//保存文件到本地
func downloadFileToLocal() {
//保存文件到本地
let downloadArr = cloudURLs!// self.getNeedDownloadTolocalUbiquityURLArray()
for ubiquityURL in downloadArr {
let fileName = ubiquityURL.path.components(separatedBy: "/").last
let document = MyDocument(fileURL: ubiquityURL as URL)
document.open(completionHandler: {(success: Bool) -> Void in
if success {
let data = document.data
if data == nil {
print("iCloud file open failed,error ,data = nil ")
}else {
Utils.saveFileToImgFolder(fileName: fileName!, data: data!)
print("iCloud file open OK")
}
} else {
print("iCloud file open failed")
}
})
}
}
//上傳文件到云端
func uploadFileToCloud() {
//上傳文件到icloud
let uploadArr = self.getNeedUploadToCloudUbiquityURLArray()
for ubiquityURL2 in uploadArr {
let document = MyDocument(fileURL: ubiquityURL2)
let fileName = ubiquityURL2.path.components(separatedBy: "/").last
let fileManager = FileManager.default
let fullpath = Utils.getImgFolderPath().appending("/\(fileName!)")
if fileManager.fileExists(atPath: fullpath) {
// print("FILE AVAILABLE")
} else {
print("FILE NOT AVAILABLE at \(fullpath)")
}
let img = UIImage.init(contentsOfFile: fullpath);
if (img != nil) {
let imgData = NSData.init(contentsOfFile: fullpath)
document.imgData = imgData
}
document.save(to: ubiquityURL2,
for: .forOverwriting,
completionHandler: {(success: Bool) -> Void in
if success {
print("iCloud create OK \(ubiquityURL2)")
} else {
print("iCloud create failed \(ubiquityURL2)")
}
})
}
}
做完這些,基本就大功告成了酥艳,一個簡單的iCloud同步功能就實現(xiàn)了摊溶。
需要注意一點的是,在同步之前充石,一定要判斷iCloud是否可用莫换,只有可用的情況下,才能進行上傳和下載,不然就會導致crash拉岁。
判斷是否可用坷剧,只需要判斷ubiquityURL是否為nil就可以,如果為nil膛薛,說明iCloud 賬號沒有再手機上登錄听隐,或者是iCloud Drive按鈕沒有打開。
源代碼:
最后哄啄,代碼可以在這里找到雅任。僅供參考。使用語言為swift 3 咨跌。
https://github.com/chenhuaizhe/src/tree/master/swift/iCloudDemoCode