背景
公司最近开发一款App,需要支持iCloud来存储一些文件.在此分享一下实现过程中的一些开发经验.
iCloud工作流程

以上就是对iCloud工作的一个图解,相信大家一看就懂,我就不过多解释了,让我们赶紧微笑面对需求吧
产品需求一
要求iCloud Drive中包含我们产品的文件夹(即我们的iCloud文件对外开放)
看到这个需求,立马想到肯定是info.plist里面设置就行了呗
眼睛都看花了,找到这三个跟需求搭点边,有没有用先试试咯
Supports opening documents in place = true
Supports Document Browser = true
Application supports iTunes file sharing = true
然后验证之后,发现没啥用.经过多番苦找,方法如下:
- 第一步
<key>NSUbiquitousContainers</key>
<dict>
<key>iCloud.$(PRODUCT_BUNDLE_IDENTIFIER)</key>
<dict>
<key>NSUbiquitousContainerIsDocumentScopePublic</key>
<true/>
<key>NSUbiquitousContainerName</key>
<string>你的App名称</string>
<key>NSUbiquitousContainerSupportedFolderLevels</key>
<string>Any</string>
</dict>
</dict>
- 第二步
随便往iCloud中上传一点文件或创建文件夹,运行程序,大功告成
产品需求二
上传文件
继承UIDocument
class CloudDocument: UIDocument {
lazy var data = Data()
override func contents(forType typeName: String) throws -> Any {
data
}
override func load(fromContents contents: Any, ofType typeName: String?) throws {
guard let data = contents as? Data else { return }
self.data = data
}
}
开始上传,大概就这样(上传完可能并不会立马上传到云端,想知道进度需要主动监听通知,跟监听下载进度一样)
func upload(_ file: File, to url: URL) {
let document = CloudDocument(fileURL: file.url)
document.data = file.data
document.save(to: url, for: .forCreating) {
print($0)
}
}
如果想将文件改动上传也很简单,只需将已经下载好的文件通过它的path或者url打开之后保存即可,系统会自动上传
产品需求三
文件下载带进度
- 第一步,获取容器路径
func documentRootURL() {
// 官方建议异步线程获取
DispatchQueue.global().async { [weak self] in
let url = FileManager.default.url(forUbiquityContainerIdentifier: nil)
DispatchQueue.main.async { [weak self] in
if url == nil {
print("前往开启iCloud Drive")
} else {
self?.documentURL = url?.appendingPathComponent("Document")
}
}
}
}
- 第二步获取文件列表
func iCloudFiles() {
let query = NSMetadataQuery()
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
var observer: NSObjectProtocol?
observer = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: .main) { _ in
query.stop()
if observer != nil {
NotificationCenter.default.removeObserver(observer!)
}
let files = query.results.compactMap { $0 as? NSMetadataItem }
files.forEach {
print($0.value(forAttribute: NSMetadataItemDisplayNameKey)!)
}
}
query.start()
}
乍一看没啥问题(墙裂建议对NSMetadataItem进行封装),运行之后才发现这样的文件列表不是我们想要的,它把整个iCloud中的文件都给我们了,不慌,query应该可以设置过滤条件,修改如下
func iCloudFiles() {
let query = NSMetadataQuery()
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
query.predicate = NSPredicate(format: "%K.URLByDeletingLastPathComponent == %@", NSMetadataItemURLKey, documentURL!)
var observer: NSObjectProtocol?
observer = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidFinishGathering, object: query, queue: .main) { _ in
query.stop()
if observer != nil {
NotificationCenter.default.removeObserver(observer!)
}
let files = query.results.compactMap { $0 as? NSMetadataItem }
files.forEach {
print($0.value(forAttribute: NSMetadataItemDisplayNameKey)!)
}
}
query.start()
}
至此,文件列表终于出来了,那我可以开始下载了吧,找找方法,终于找到一个
func download(_ file: NSMetadataItem) {
do {
try FileManager.default.startDownloadingUbiquitousItem(at: file.value(forAttribute: NSMetadataItemURLKey) as! URL)
} catch let error as NSError {
print(error.localizedDescription)
}
}
不过,好像没有进度回调啊,FileManager代理看了下也不像.看来只有指望代理了
// 先查询文件状态(比如下载文件,并不是立即下载的,严谨一点还要判断是否有错误,在这里我就暂时不判断了)
func queryFileStatusChanged(at path: String, callback: (() -> ())? = nil) {
let query = NSMetadataQuery()
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
query.predicate = NSPredicate(format: "%K == %@", NSMetadataItemPathKey, path)
var observer: NSObjectProtocol?
observer = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: .main) { _ in
query.stop()
if observer != nil {
NotificationCenter.default.removeObserver(observer!)
}
callback?()
}
query.start()
}
在上一步监听状态改变之后,我们可以开始监听下载进度了
func queryProgress(at path: String) {
let query = NSMetadataQuery()
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
query.predicate = NSPredicate(format: "%K == %@", NSMetadataItemPathKey, path)
var observer: NSObjectProtocol?
observer = NotificationCenter.default.addObserver(forName: .NSMetadataQueryDidUpdate, object: query, queue: .main) { _ in
func stopQuery() {
query.stop()
query.disableUpdates()
if observer != nil {
NotificationCenter.default.removeObserver(observer!)
}
}
guard let file = query.results.first as? NSMetadataItem else {
stopQuery()
return
}
// 上传进度也是如此,只不过参数变了而已
let downloadPercent = file.value(forAttribute: NSMetadataUbiquitousItemPercentDownloadedKey) as! Double
print("进度是...", downloadPercent)
if downloadPercent == 100 {
stopQuery()
}
}
query.start()
query.enableUpdates()
}
需要注意的是iCloud文件操作并不是实时的,大多数情况下需要监听文件状态,所谓的文件操作也都需要将云端的文件下载到本地之后在进行,所以很有必要在文件列表中加上文件状态标识
产品需求四
解决文件冲突
func resolveFileConflict(at url: URL) {
guard let versions = NSFileVersion.unresolvedConflictVersionsOfItem(at: url) else { return }
for version in versions {
// 文档修改时间
print(version.modificationDate)
// 上传文档的设备名称
print(version.localizedNameOfSavingComputer)
// 文档名称
print(version.localizedName)
if user.need {
version.isResolved = true
} else {
try? version.remove()
}
}
}
建议做一个冲突文件列表让用户选择哪一个版本
参考:
https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/DocumentBasedAppPGiOS/Introduction/Introduction.html
网友评论