Disk

Platform: iOS 9.0+ Language: Swift 4 Carthage compatible License: MIT

安装使用调试开发者的话文档使用 Disk 的应用许可证贡献有问题?

Disk 是一个强大简单的文件管理库,它在设计时充分考虑了 Apple 的 iOS 数据存储指南。Disk 充分利用了 Swift 4 中引入的全新 Codable 协议,使您能够持久化结构体,而无需担心编码/解码问题。 Disk 还可以帮助您仅用一行代码即可将图像和其他数据类型保存到磁盘。

兼容性

Disk 需要 iOS 9+,并且与使用 Swift 4.0 及更高版本的项目兼容。 因此,使用 Disk 时必须至少使用 Xcode 9。

安装

Disk 支持 CocoaPods 1.7.0 的新多 Swift 功能,适用于 Swift 4.0、4.2 和 5.0。 只需在您的 Podfile 中指定 supports_swift_versions

platform :ios, '9.0'
target 'ProjectName' do
use_frameworks!
supports_swift_versions '< 5.0' # configure this for your project

    pod 'Disk', '~> 0.6.4'

end

(如果遇到问题,请 pod repo update 并重试)

github "saoudrizwan/Disk"
dependencies: [
    .Package(url: "https://github.com/saoudrizwan/Disk.git", "0.6.4")
]

然后在要使用的文件中 import Disk

使用

Disk 目前支持以下类型的持久化

这些通常是您需要在 iOS 上持久化的唯一类型。

Disk 遵循 Apple 的 iOS 数据存储指南,因此允许您将文件保存在四个主要目录和共享容器中

Documents 目录 .documents

只有用户生成的数据,或应用程序无法以其他方式重新创建的数据,才应存储在 <Application_Home>/Documents 目录中,并且将由 iCloud 自动备份。

Caches 目录 .caches

可以再次下载或重新生成的数据应存储在 <Application_Home>/Library/Caches 目录中。 您应放入 Caches 目录的文件的示例包括数据库缓存文件和可下载内容,例如杂志、报纸和地图应用程序使用的内容。

使用此目录来写入您希望在应用程序启动之间或应用程序更新期间持久存在的任何特定于应用程序的支持文件。 您的应用程序通常负责添加和删除这些文件(请参阅 辅助方法)。 它还应该能够根据需要重新创建这些文件,因为 iTunes 会在设备的完整恢复期间删除它们。 在 iOS 2.2 及更高版本中,iTunes 不会备份此目录的内容。

请注意,系统可能会删除 Caches/ 目录以释放磁盘空间,因此您的应用程序必须能够根据需要重新创建或下载这些文件。

Application Support 目录 .applicationSupport

将应用程序创建的支持文件放在 <Application_Home>/Library/Application Support 目录中。 通常,此目录包括应用程序用于运行但应向用户隐藏的文件。 此目录还可以包括数据文件、配置文件、模板和从应用程序包加载的资源的修改版本。

Temporary 目录 .temporary

仅临时使用的数据应存储在 <Application_Home>/tmp 目录中。 尽管这些文件不会备份到 iCloud,但请记住在使用完这些文件后删除它们,以免它们继续占用用户设备上的空间。

应用程序组共享容器 .sharedContainer(appGroupName: String)

单个设备上的多个应用程序可以访问共享目录,只要这些应用程序在 com.apple.security.application-groups 授权数组中具有相同的 groupIdentifier,如 将应用程序添加到应用程序组授权密钥参考 中所述。

有关更多信息,请访问文档:https://developer.apple.com/documentation/foundation/nsfilemanager/1412643-containerurlforsecurityapplicati


有了所有这些要求和最佳实践,适当地使用 iOS 文件系统可能很难,这就是 Disk 诞生的原因。 Disk 使遵循这些繁琐的规则变得简单而有趣。

使用 Disk 很容易。

Disk 通过 throw 抛出错误。 请参阅 使用 Do-Catch 处理错误

结构体(必须符合 Codable

假设我们有一个名为 Message 的数据模型……

struct Message: Codable {
    let title: String
    let body: String
}

……并且我们想将消息持久化到磁盘……

let message = Message(title: "Hello", body: "How are you?")
try Disk.save(message, to: .caches, as: "message.json")

……或者我们可能想将其保存在文件夹中……

try Disk.save(message, to: .caches, as: "Folder/message.json")

……然后我们可能希望稍后检索此消息……

let retrievedMessage = try Disk.retrieve("Folder/message.json", from: .caches, as: Message.self)

如果您 Option + 单击 retrievedMessage,则 Xcode 将显示其类型为 Message。 很棒,不是吗? example

那么后台发生了什么? Disk 首先将 message 转换为 JSON 数据,然后 原子地写入 该数据到 <Application_Home>/Library/Caches/Folder/message.json 处新创建的文件。 然后,当我们检索 message 时,Disk 会自动将 JSON 数据转换为我们的 Codable 结构体类型。

结构体数组怎么样?

由于 Codable 的强大功能,存储和检索结构体数组与上面的代码一样容易。

var messages = [Message]()
for i in 0..<5 {
    messages.append(Message(title: "\(i)", body: "..."))
}
try Disk.save(messages, to: .caches, as: "messages.json")
let retrievedMessages = try Disk.retrieve("messages.json", from: .caches, as: [Message].self)

追加结构体 (感谢 @benpackard 的建议)

Disk 还允许您将结构体或结构体数组追加到具有相同类型数据的文件中。

try Disk.append(newMessage, to: "messages.json", in: .caches)

注意:您可以将单个结构体追加到空文件中,但是为了再次正确检索该结构体,您必须将其作为数组检索。

使用自定义 JSONEncoderJSONDecoder (感谢 @nixzhu@mecid

在后台,Disk 使用 Apple 的 JSONEncoderJSONDecoder 类来编码和解码原始 JSON 数据。 如果您需要特殊的编码或解码策略,例如,可以使用这些类的自定义实例。

let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
try Disk.save(messages, to: .caches, as: "messages.json", encoder: encoder)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let retrievedMessages = try Disk.retrieve("messages.json", from: .caches, as: [Message].self, decoder: decoder)

注意:追加 Codable 结构需要 Disk 首先解码文件位置的任何现有值,追加新值,然后将生成的结构编码到该位置。

try Disk.append(newMessage, to: "messages.json", in: .caches, decoder: decoder, encoder: encoder)

图像

let image = UIImage(named: "nature.png")
try Disk.save(image, to: .documents, as: "Album/nature.png")
let retrievedImage = try Disk.retrieve("Album/nature.png", from: .documents, as: UIImage.self)

图像数组

多个图像被保存到一个新文件夹。 然后,每个图像都被命名为 0.png、1.png、2.png 等。

var images = [UIImages]()
// ...
try Disk.save(images, to: .documents, as: "FolderName/")

您不需要在文件夹名称后包含“/”,但这样做是为了声明您不是将所有图像的数据写入一个文件,而是将多个文件写入一个新文件夹。

let retrievedImages = try Disk.retrieve("FolderName", from: .documents, as: [UIImage].self)

假设您将一堆图像保存到一个文件夹,如下所示

try Disk.save(deer, to: .documents, as: "Nature/deer.png")
try Disk.save(lion, to: .documents, as: "Nature/lion.png")
try Disk.save(bird, to: .documents, as: "Nature/bird.png")

也许您甚至将 JSON 文件保存到此 Nature 文件夹中

try Disk.save(diary, to: .documents, as: "Nature/diary.json")

然后您可以像这样检索 Nature 文件夹中的所有图像

let images = try Disk.retrieve("Nature", from: .documents, as: [UIImage].self)

……这将返回 -> [deer.png, lion.png, bird.png]

追加图像

与追加结构只会修改现有 JSON 文件不同,追加图像会将该图像作为独立文件添加到文件夹中。

try Disk.append(goat, to: "Nature", in: .documents)

注意:建议手动使用 save(:to:as:) 函数保存独立的图像,以便在您以后想要检索图像时,为该图像文件指定名称。 使用 append(:to:in:) 函数会导致创建一个具有自动生成的名称的文件(即,如果您将图像追加到已存在图像的文件夹中(0.png、1.png、2.png),则新图像将被命名为 3.png。)如果图像名称不重要,那么使用 append(:to:in:) 很好。 追加图像数组的行为类似。

Data

如果您尝试保存 .mp4 视频数据之类的数据,那么 Disk 的 Data 方法将帮助您使用文件系统来持久化所有数据类型。

let videoData = Data(contentsOf: videoURL, options: [])
try Disk.save(videoData, to: .documents, as: "anime.mp4")
let retrievedData = try Disk.retrieve("anime.mp4", from: .documents, as: Data.self)

Data 数组

Disk 保存 Data 对象数组,就像保存图像数组一样,作为文件夹中的文件。

var data = [Data]()
// ...
try Disk.save(data, to: .documents, as: "videos")
let retrievedVideos = try Disk.retrieve("videos", from: .documents, as: [Data].self)

如果您要从包含图像和 JSON 文件的文件夹中检索 [Data],则这些文件将包含在返回值中。 继续从 图像数组 部分的示例

let files = try Disk.retrieve("Nature", from: .documents, as: [Data].self)

……将返回 -> [deer.png, lion.png, bird.png, diary.json]

追加 Data

追加 DataData 数组与追加图像或图像数组类似——将创建具有自动生成的名称的新文件并添加到指定的文件夹中。

try Disk.append(newDataObject, to: "Folder/", in: .documents)

大文件

了解何时在后台线程处理文件系统非常重要。 磁盘是同步的,使您可以更好地控制文件系统上的读/写操作。 Apple 指出“因为文件操作涉及访问磁盘,所以几乎总是首选异步执行这些操作。”

Grand Central Dispatch 是以异步方式处理磁盘的最佳方法。 这是一个例子

activityIndicator.startAnimating()
DispatchQueue.global(qos: .userInitiated).async {
    do {
        try Disk.save(largeData, to: .documents, as: "Movies/spiderman.mp4")
    } catch {
        // ...
    }
    DispatchQueue.main.async {
        activityIndicator.stopAnimating()
        // ...
    }
}

别忘了处理这些类型的任务被中断的情况。

iOS 11 卷信息

Apple 在第 204 届会议中介绍了几个出色的 iOS 存储实践,重点介绍了 iOS 11 中添加的几个新的 NSURL 卷容量详细信息。 此信息使我们能够衡量何时适合将数据存储在用户的磁盘上。

Disk.totalCapacity
Disk.availableCapacity
Disk.availableCapacityForImportantUsage
Disk.availableCapacityForOpportunisticUsage

注意:这些变量返回可选的 Int 类型,因为检索文件系统资源值可能会失败并返回 nil。 但是,这种情况极不可能发生,并且此行为仅用于安全目的。

辅助方法

try Disk.clear(.caches)
try Disk.remove("video.mp4", from: .documents)
if Disk.exists("album", in: .documents) {
    // ...
}
try Disk.move("album/", in: .documents, to: .caches)
try Disk.rename("currentName.json", in: .documents, to: "newName.json")
let url = try Disk.url(for: "album/", in: .documents)
try Disk.doNotBackup("album", in: .documents)

应用主目录中的所有内容都会被备份,但应用程序包本身、缓存目录和临时目录除外。

try Disk.backup("album", in: .documents)

(通常,除非您绝对确定要持久化数据,无论用户的设备处于何种状态,否则您永远不应使用 .doNotBackup(:in:).backup(:in:) 方法。)

URL 对应项

这些辅助方法中的大多数都有 URL 对应项,以防您想直接使用其文件系统 URL 处理文件。

let fileUrl = try Disk.url(for: "file.json", in: .documents)
try Disk.remove(fileUrl)
if Disk.exists(fileUrl) {
    // ...
}
let newUrl = try Disk.url(for: "Folder/newFileName.json", in: .documents)
try Disk.move(fileUrl, to: newUrl)
try Disk.doNotBackup(fileUrl)
try Disk.backup(fileUrl)
if Disk.isFolder(fileUrl) {
    // ...
}

调试

Disk 非常彻底,这意味着它不会放过任何错误的机会。 Disk 的几乎所有方法都会代表 Foundation 或自定义 Disk 错误抛出错误,这些错误值得您注意。 这些错误包含大量信息,例如描述、失败原因和恢复建议

do {
    if Disk.exists("posts.json", in: .documents) {
        try Disk.remove("posts.json", from: .documents)
    }
} catch let error as NSError {
    fatalError("""
        Domain: \(error.domain)
        Code: \(error.code)
        Description: \(error.localizedDescription)
        Failure Reason: \(error.localizedFailureReason ?? "")
        Suggestions: \(error.localizedRecoverySuggestion ?? "")
        """)
}

上面的示例处理了处理文件系统时最常见的错误:删除不存在的文件。

开发者的话

在开发 iOS 8 年以上之后,我遇到了几乎所有可用的数据持久化方法(Core Data、Realm、NSKeyedArchiverUserDefaults 等)。 除了 NSKeyedArchiver 之外,没有真正符合要求的,但是需要跳过的环节太多了。 在 Swift 4 发布后,我对 Codable 协议感到非常兴奋,因为我知道它在 JSON 编码方面可以提供什么。 处理网络响应的 JSON 数据并将其转换为可用的结构从未如此简单。 Disk 旨在将处理数据的这种简单性扩展到文件系统。

假设我们从网络请求中获取一些数据...

let _ = URLSession.shared.dataTask(with: request) { (data, response, error) in
    DispatchQueue.main.async {
        guard error == nil else { fatalError(error!.localizedDescription) }
        guard let data = data else { fatalError("No data retrieved") }

        // ... we could directly save this data to disk...
        try? Disk.save(data, to: .caches, as: "posts.json")

    }
}.resume()

... 稍后将其检索为 [Post]...

let posts = try Disk.retrieve("posts.json", from: .caches, as: [Post].self)

Disk 消除了将数据编码为所需类型所需的许多繁琐的手工操作,并且做得很好。 Disk 还简化了必要的但单调的任务,例如清除缓存或临时目录(根据 Apple 的iOS 数据存储指南的要求)

try Disk.clear(.temporary)

Disk 也比替代持久化解决方案(如 NSKeyedArchiver)快得多,因为它直接与文件系统交互。 最重要的是,Disk 在抛出错误时非常彻底,确保您了解问题发生的原因。

文档

Option + 单击任何 Disk 的方法以获取详细文档。 documentation

使用 Disk 的应用

许可

Disk 使用 MIT 许可证。 如果您有任何问题或想分享您使用 Disk 的方式,请提出问题。

贡献

请随时创建问题以提出功能请求或发送您认为可以补充 Disk 及其理念的任何添加内容的拉取请求。

有疑问吗?

通过电子邮件hello@saoudmr.com或 Twitter @sdrzn 联系我。 如果您遇到错误或希望添加功能,请创建问题