美文网首页常用的第三方iOS学习征服iOS
iOS 利用 iCloud Document 同步本地文件(sw

iOS 利用 iCloud Document 同步本地文件(sw

作者: 陈怀哲 | 来源:发表于2017-03-22 19:42 被阅读665次

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

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

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

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

    ![](http:https://img.haomeiwen.com/i530099/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

    相关文章

      网友评论

      • 柚子不酸酸酸:想问一下,这些 iCloud Drive 中的文件,用户可以在 Files APP中看到吗?,也就是用户能否手动查看、编辑、删除这些文件?
        陈怀哲:@Aollio 可以
      • 小阳哥_:如何查看下载进度?
        陈怀哲:@小阳哥_ 没有做进度部分
      • 谢衣丶:可否在iCloud的documents文件夹下自己再创建子文件夹?
        陈怀哲:@谢衣丶 没研究,你找找文档

      本文标题:iOS 利用 iCloud Document 同步本地文件(sw

      本文链接:https://www.haomeiwen.com/subject/xmaonttx.html