SwiftCloudDrive

作者:Drew McCormack (Mastodon: @drewmccormack@mastodon.cloud)

一个易于使用的 Swift 封装器,用于 iCloud Drive。

SwiftCloudDrive 处理了诸如文件协调、访问安全作用域资源、文件冲突和云元数据查询等复杂性,从而为在 iCloud 中处理文件提供了直接的异步函数。它使得在云端处理文件几乎和使用本地 FileManager 一样简单。

何时 SwiftCloudDrive 是一个好的解决方案?

对于 iCloud 的高级用途,您可能应该直接使用 Apple 的框架。 这让您拥有最大的控制权,但代价是学习曲线陡峭。

例如,如果您需要完全控制 iCloud Drive 中的文件何时下载到设备,或者拥有像 Apple 的 Photos 这样的应用程序,在用户请求之前通常希望将文件保留在云中,则不应使用 SwiftCloudDrive。

如果您希望将应用的所有 iCloud Drive 文件都存储在设备上以及云中,SwiftCloudDrive 可以帮助您更快地启动和运行。 您不必担心文件协调、元数据查询或冲突解决,这些都是使用 iCloud Drive 的一部分。 SwiftCloudDrive 将为您提供一个简单的类来上传、下载、查询和更新,其工作方式与您已经熟悉的 FileManager 类型非常相似。

使用 SwiftCloudDrive

集成包

要开始使用,请将 SwiftCloudDrive 包添加到您的 Xcode 项目,然后在 Xcode 中应用的 Target 的 Signing & Capabilities 选项卡中启用 iCloud Drive。 您可以接受默认容器标识符,也可以选择自定义标识符。

设置驱动器

如果您使用的是默认的 iCloud 容器,那么在您的应用程序源代码中设置 CloudDrive 可以简单到如下所示:

import SwiftCloudDrive
let drive = try await CloudDrive()

如果您有自定义的 iCloud 容器,只需传入标识符即可。

let customDrive = try await CloudDrive(storage: .iCloudContainer(containerIdentifier: "iCloud.com.yourcompany.app"))

在上述情况下,您将直接访问容器根目录中的文件,但您也可以将 CloudDrive 锚定到容器的特定子目录,如下所示:

let subDrive = try await CloudDrive(relativePathToRootInContainer: "Sub/Directory/Of/Choice")

如果目录不存在,CloudDrive 将为您创建该目录。

查询文件状态

一旦你拥有了一个 CloudDrive 对象,你就可以像使用 FileManager 一样查询文件状态。 但有一个很大的区别:由于文件可能是远程的,并且必须下载,所以所有的函数调用都是 async 的。

要确定目录是否存在,您需要这样做:

let path = RootRelativePath(path: "Path/To/Dir")
let exists = try await drive.directoryExists(at: path)

如果您想知道特定文件是否存在,您可以使用:

let exists = try await drive.fileExists(at: path)

使用路径

正如您在前一节中看到的,RootRelativePath 结构用于引用相对于 CloudDrive 根目录的路径。

如果您经常使用特定的固定路径,扩展 RootRelativePath 来定义静态常量会很有用。

extension RootRelativePath {
    static let images = Self(path: "Images")
}

let imagesExist = try await drive.directoryExists(at: .images)
let dogImageExists = try await drive.fileExists(at: .images.appending("Dog.jpeg"))

正如您所看到的,RootRelativePath 还定义了一个 appending 函数,这使得扩展现有路径变得简单。

创建目录

在云中创建目录同样简单。 请注意,如果缺少中间目录,也会创建这些目录。

try await drive.createDirectory(at: .images)

上传和下载文件

要将文件移动到云中,您可以将本地文件“上传”到容器,或者您可以将 Data 直接写入文件。

let data = "Some text".data(using: .utf8)!
try await drive.writeFile(with: data, at: .root.appending("file.txt"))

在本例中,我们使用内置的静态常量 root 来构建路径,但我们也可以只使用 RootRelativePath("file.txt")

要从云容器外部上传文件,您可以使用如下代码:

try await drive.upload(from: "/Users/eddy/Desktop/image.jpeg", to: .images.appending("image.jpeg"))

更新文件

一旦您在云中拥有一个文件,您可以通过再次上传来更改它,但您必须首先删除旧版本,否则会发生错误。

let cloudPath: RootRelativePath = .images.appending("image.jpeg")
try? await drive.removeFile(at: cloudPath)
try await drive.upload(from: "/Users/eddy/Desktop/image_new.jpeg", to: cloudPath)

或者,您可以写入内容,而无需先删除现有文件。

try await drive.writeFile(with: newImageData, at: cloudPath)

如果您需要更精细的控制,您可以进行任何您喜欢的就地更新,如下所示:

try await drive.updateFile(at: imagePath) { fileURL in
    // Make any changes you like to the file at `fileURL`
    // You can also throw an error
}

删除文件和目录

正如我们已经看到的,您可以非常轻松地删除文件和目录。

try await drive.removeFile(at: cloudFilePath)
try await drive.removeDirectory(at: cloudDirPath)

如果遇到错误的项类型,则删除失败并抛出错误。

观察远程更改

在使用远程设备上的更改时,了解何时下载了新文件或已应用更新非常重要。 您可以为此目的注册 CloudDriveObserver

class Controller: CloudDriveObserver {
    func cloudDriveDidChange(_ drive: CloudDrive, rootRelativePaths: [RootRelativePath]) {
        // Decide if data needs refetching due to remote changes,
        // or if changes need to be applied in the UI
    }
}

let controller = Controller()
drive.observer = controller

请注意,传递给观察者的相对路径不一定是新文件,也可能是具有更新的文件,甚至是已删除的文件。 您的代码应考虑到这些可能性,并进行适当调整。