iOS 利用 iCloud Document 同步本地文件(swift 3版本)

要用iCloud来实现本地文件与云端文件的同步,首先要处理好逻辑的问题。

什么时候上传?什么时候下载?先上传还是先下载?如何处理文件冲突?

我要实现的是每天保存一张图片文件(当天过后就不能再修改这个图片文件),用来记录自己的生活状态,所以文件系统比较简单,我希望我的app逻辑也尽量简单,这样比较好维护。所以我设想了一个最简单的同步逻辑:

点击同步按钮后,先上传本地 所有的文件,如果发现要上传的文件在云端已经有了同名文件,那么就不上传这一张图片文件。然后再从云端下载所有文件到本地,同样,检查本地是否有同名文件,如果有,则不下载这张图片文件。最后,每次创建了一张图片文件时,就上传到云端,执行覆盖操作,也就是有同名文件的话,就把云端的同名文件覆盖,以本地上传的为主。

![](http://upload-images.jianshu.io/upload_images/530099-9c100d25716998d9.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

这个逻辑我觉得简单粗暴,可行。那么就开始吧。

开启iCloud Document 能力

在Targets ->Capabilities->iCloud 中打开iCloud特性,如下图:

UIDocument

我们使用UIDocument这个类来处理文件的上传和下载的,不能直接使用这个类,而要创建它的子类,并重新实现下面两个方法来处理数据:

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执行save操作的时候,就会走 contents(forType typeName: String) 方法来把这个方法里面返回值上传到iCloud,对于我们也就是imgData;所以我们在执行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)")
            }
})
//在执行save操作前,要给MyDocument里面的imgdata赋值。
let img = UIImage.init(contentsOfFile: fullpath);
if (img != nil) {
    let imgData = NSData.init(contentsOfFile: fullpath)
    document.imgData = imgData
 }

而document在执行open操作的时候,也就是保存文件到本地,这个时候会走 load(fromContents contents: Any, ofType typeName: String?)方法,这个方法里面的参数contents就是iCloud传给我们的数据,我们可以对这个数据进行处理。在这里我使用一个data接收了这个数据,然后在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

你肯定已经注意到了,在上面我说的document.save方法里面,有一个参数ubiquityURL2,它前面有一个to,这个就是ubiquityURL,它说明了了一个文件在iCloud中保存的位置信息。

举个例子:bundle id如果是com.test.demo,那么对应的ubiquityURL是类似这种:file:///private/var/mobile/Library/Mobile%20Documents/iCloudcomtest~demo.我们需要在后面拼接上Documents来表示这个文件是保存在iCloud中的我的这个应用的Documents下。也就是说iCloudcomtest~demo是用来区分是哪个应用的,iCloudcomtest~demo后面的是用来区分应用下文件路径的。

获取你的应用的跟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的每个实例都是处理一个文件,不同的文件处理会创建不同的实例。

let document = MyDocument(fileURL: ubiquityURL2)

例如:这里的ubiquityURL2=file:///private/var/mobile/Library/Mobile%20Documents/iCloudcomtest~demo/Documents/2017-03-02.png

查询云端文件

我们需要获取到云端的文件,其实也是需要知道云端文件对应的ubiquityURL,而本地的文件也可以转成对应的ubiquityURL(这个后面说),所以这样,云端和本地就构成了一种对应的关系。

查询云端文件,可以主动调用下面的方法:

    //查询云端的文件
    func askForCloudDataQuery() {
        metaDataQuery = NSMetadataQuery()
        metaDataQuery?.searchScopes =
            [NSMetadataQueryUbiquitousDocumentsScope]
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(
                                                CloudDataManager.metadataQueryDidFinishGathering),
                                               name: NSNotification.Name.NSMetadataQueryDidFinishGathering,
                                               object: metaDataQuery!)
        metaDataQuery!.start()
    }

查询到结果,可以走下面的回调方法。

考虑到不需要查询那么多次,我直接查询到Document下所有的文件,然后根据文件名与本地文件名作对比。比对出两个包含ubiquityURL元素的数组,一个是需要上传到云端的,一个是需要下载到本地的,然后分别进行比对操作。

//获取到云端的所有的文件的url,保存到cloudURLs数组里面
    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的数组
    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数组里面
    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的数组
    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!
    }

上传下载操作

获取到要上传和下载的数组后,进行上传和下载的操作就可以了。

//保存文件到本地
    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同步功能就实现了。

需要注意一点的是,在同步之前,一定要判断iCloud是否可用,只有可用的情况下,才能进行上传和下载,不然就会导致crash。

判断是否可用,只需要判断ubiquityURL是否为nil就可以,如果为nil,说明iCloud 账号没有再手机上登录,或者是iCloud Drive按钮没有打开。

源代码:

最后,代码可以在这里找到。仅供参考。使用语言为swift 3 。
https://github.com/chenhuaizhe/src/tree/master/swift/iCloudDemoCode

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,112评论 18 139
  • 通过iOS 8app extensions,我们可以选择多种方式去分享我们app的功能。Document Prov...
    _浅墨_阅读 7,056评论 4 12
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,658评论 4 59
  • 记得三年前刚步入大学校园的时候,我是多么的激动,兴奋!当时就想着,为自己的大学生活好好的规划一下,于是就做了...
    一梦远方阅读 1,447评论 4 8
  • 文丨pinkpink-summer 小丹同学翘着二郎腿悠闲的躺在床上刷手机,忽然她转过头来语气凝重的问我,舅妈,你...
    pinkpinksummer阅读 205评论 0 0