FileSinki
使用 CloudKit 在 iOS、MacOS 和 tvOS 之间轻松进行文件同步。
基本用法
import FileSinki
FileSyncable
采用 FileSyncable 协议,以使您的数据与 FileSinki 一起工作。
最基本的功能是 shouldOverwrite
,它决定如果本地副本和远程(云端)副本的数据冲突时将执行什么操作。
struct SaveGame: FileSyncable {
let score: Double
func shouldOverwrite(other: Self) -> Bool {
return score > other.score
}
}
如果您的结构 / 类已经符合 Comparable,则默认情况下,如果 self > other,则 shouldOverwrite 会覆盖。
保存、加载和删除
// load a SaveGame from a file with path: "SaveGames/player1.save"
FileSinki.load(SaveGame.self,
fromPath: "SaveGames/player1.save") { saveGame, wasRemote in
// closure *may* be called multiple times,
// if the cloud has a better version of saveGame
}
// save a saveGame to a file with path: "SaveGames/player1.save"
FileSinki.save(saveGame,
toPath: "SaveGames/player1.save") { finalVersion in
// closure *may* be called with finalVersion
// if the saveGame changed as a result of a merge
// or a better available version
}
// delete the saveGame
FileSinki.delete(saveGame, at: "SaveGames/player1.save")
高级使用
可合并
采用 FileMergable 协议并实现 merge(with:)
方法以在设备间合并 FileSyncable 对象。返回新合并的对象/结构体,将用于此。
struct SaveGame: FileSyncable, FileMergable {
let trophies: [Trophy]
func merge(with other: Self) -> Self? {
let combinedTrophies = (trophies + other.trophies).sorted()
return SaveGame(trophies: combinedTrophies)
}
}
如果你从 merge(with:)
方法返回 nil,则 FileSinki 将回退到 shouldOverwrite(other:)
交互式/异步选择和合并
如果你的决定是否覆盖/如何合并更为复杂,需要用户干预或异步工作,可以实现以下其中一个函数
extension SaveGame: FileSyncable {
func shouldOverwriteAsync(other: SaveGame,
keep: @escaping ShouldOverwriteClosure) {
// Do any kind of async decision making necessary.
// You just have to call keep() with the version you want to keep
SomeUserPrompt.chooseBetween(self, other) { userSelection in
keep(userSelection)
}
}
}
extension SaveGame: FileMergable, FileSyncable {
func mergeAsync(with other: SaveGame,
merged: @escaping MergedClosure) {
// Do any kind of async merging necessary.
// You just have to call merged() with the
// final merged version you want to keep
SomeSaveGameMergerThing.merge(self, other) { mergedSaveGame in
merged(mergedSaveGame)
}
}
}
在这里,你可以异步或在不同线程上执行任何工作,但必须在工作完成后使用最终项目调用 keep
或 merged
。
观察变化
类似于向 NotificationCenter
添加观察者,你可以观察在其它设备上发生的项目变化
FileSinki.addObserver(self,
for: SaveGame.self,
path: "SaveGames/player1.save") { changed in
// any time a SaveGame in the file player1.save changes remotely, this closure will be called.
let changedSaveGame = changed.item
print("Observed FileSinki change in \(changedSaveGame) with local URL \(changed.localURL) and path: \(changed.path)")
}
如果提供的路径以尾部斜杠 /
结尾,则将递归地检查该文件夹中的任何文件的变化
FileSinki.addObserver(self,
for: SaveGame.self,
path: "SaveGames/") { changed in
// any time a SaveGame anywhere in SaveGames/ changes remotely, this closure will be called.
let changedSaveGame = changed.item
print("Observed change in \(changedSaveGame) with local URL \(changed.localURL) and path: \(changed.path)")
}
二进制文件
如果你正在处理原始数据文件或不可编码的对象/结构体,可以使用 FileSinki 在原始数据级别上进行。
保存、加载和删除
// load a PDF from a file with path: "test.pdf"
FileSinki.loadBinaryFile(fromPath: "test.pdf",
mergeAsync: { left, right, merged in
let leftPDF = PDF(data: left)
let rightPDF = PDF(data: right)
SomePDFMerger.merge(leftPDF, rightPDF) { finalMergedPDF in {
merged(finalMergedPDF.data)
}
}) { data, wasRemote in
// closure *may* be called multiple times,
// if the cloud has a better version of your data
let loadedPDF = PDF(data: data) // the final data object which has been merged across devices
}
FileSinki.saveBinaryFile(pdf.data,
toPath: "test.pdf",
mergeAsync: { left, right, merged in
let leftPDF = PDF(data: left)
let rightPDF = PDF(data: right)
SomePDFMerger.merge(leftPDF, rightPDF) { finalMergedPDF in {
merged(finalMergedPDF.data)
}
}) { finalData in
// closure *may* be called with finalData
// if the data changed as a result of a merge
// or a better available version
let loadedPDF = PDF(data: finalData) // the final data object which has been merged across devices
}
FileSinki.deleteBinaryFile(pdf.data, at: "test.pdf")
观察更改
观察二进制文件远程更改比FileSyncables有限得多。您只会收到通知哪些路径/本地url已更改。加载二进制文件是您的责任。
FileSinki.addObserver(self,
path: "test.pdf") { changed in
// any time test.pdf changes remotely, this closure will be called.
print("Observed a binary file change with path: \(changed.path)")
// You'll probably want to actually do something now that you know a binary file has changed remotely.
FileSinki.loadBinaryFile(...
}
URL和文件夹
默认情况下,FileSinki将文件放入.applicationSupportDirectory + bundle name
。您可以使用可选的root
参数指定不同的位置。
// load a SaveGame from a file with path: "SaveGames/player1.save" inside the Documents directory
FileSinki.load(SaveGame.self,
fromPath: "SaveGames/player1.save",
root: .documentDirectory) { saveGame, wasRemote in
}
您也可以从一个本地url传入完整路径
let saveGameURL: URL = ... // some local file URL
FileSinki.load(SaveGame.self,
fromPath: saveGameURL.path) { saveGame, wasRemote in
}
请注意,tvOS仅支持写入到.caches
文件夹。FileSinki自动使用该文件夹而不是.applicationSupportDirectory
,因此您不必担心。
压缩
内部FileSinki总是将压缩后版本的数据存储在云中。将压缩版本也存储在本地也有很多优势。压缩和解压缩通常比磁盘访问要快得多,且编码文件一般压缩效果极佳。
所有以上FileSinki操作都有压缩版本。例如
// load a compressed SaveGame from a file with path: "SaveGames/player1.save"
FileSinki.loadCompressed(SaveGame.self,
fromPath: "SaveGames/player1.save") { saveGame, wasRemote in
}
// save a compressed saveGame to a file with path: "SaveGames/player1.save"
FileSinki.saveCompressed(saveGame,
toPath: "SaveGames/player1.save") { finalVersion in
// closure *may* be called with finalVersion
// if the saveGame changed as a result of a merge
// or a better available version
}
// delete the compressed saveGame
FileSinki.deleteCompressed(saveGame, at: "SaveGames/player1.save")
所使用的压缩是Apple的LZFSE
Data+Compression.swift
和Codable+Compression.swift
中有几个实用的压缩函数,它们不涉及文件同步
Objective-C
FileSinki支持Objective-C,但功能仅限于保存和加载NSData
。
@import FileSinki;
[FileSinki loadBinaryFileFromPath:@"test.pdf"
root:NSApplicationSupportDirectory
mergeAsync:^(NSData *left, NSData *right, void (^merged)(NSData *data)) {
// decode left and right data, merge and then pass on the final mergedData to merged()
NSData *mergedData = [mergedPDF data];
merged(mergedData);
} loaded:^(NSData *finalData, BOOL wasRemote) {
if (!finalData) {
return;
}
}];
[FileSinki saveBinaryFile:pdfData
toPath:@"test.pdf"
root:NSApplicationSupportDirectory
mergeAsync:^(NSData *left, NSData *right, void (^ merge)(NSData *mergedData)) {
// decode left and right data, merge and then pass on the final mergedData to merged()
NSData *mergedData = [mergedPDF data];
merged(mergedData);
} finalVersion:^(NSData *finalVersion) {
// do stuff with the final merged data
}];
[FileSinki deleteBinaryFile:pdfData
atPath:@"test.pdf"
root:NSApplicationSupportDirectory];
[FileSinki addObserver:self
path:@"SaveGames/"
root:NSApplicationSupportDirectory
itemsChanged:^(NSArray<ChangeItem *> * changedItems) {
for (ChangeItem *item in changedItems) {
printf("File changed at %s\n", item.localURL.absoluteString.UTF8String);
}
}];
安装和配置
安装
FileSinki可以通过Swift包管理器
或CocoaPods
进行安装
pod 'FileSinki'
CloudKit 配置
- 在应用程序的
功能
中启用CloudKit
。注意您的应用程序 CloudKit 容器标识符供稍后使用。
- 在 https://icloud.developer.apple.com 中,前往您的应用程序开发
模式
,并添加一个名为FileSinki
的新记录类型
,具有以下自定义字段
path
(类型:String)type
(类型:String)asset
(类型:Asset)data
(类型:Bytes)deleted
(类型:Int(64))
- 在 FileSinki 记录模式中,点击
编辑索引
,添加以下索引
recordName
(可以查询的
)type
(可以查询的
)path
(可搜索的
)
并保存更改。
最终结果应如下所示
注意:一旦您已验证 FileSinki 在开发环境中工作正常,请不要忘记将模式部署到生产
AppDelegate
将以下代码添加到您的 AppDelegate(或等效的 MacOS 代表函数)中
- 将
FileSinki.setup()
和registerForRemoteNotifications()
添加到didFinishLaunchingWithOptions
与您的 CloudKit 容器标识符
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FileSinki.setup(cloudKitContainer: "iCloud.com.MyCompanyName.MyCoolApp")
application.registerForRemoteNotifications() // required for live change observing
}
- 将
FileSinki.didBecomeActive()
添加到applicationDidBecomeActive
func applicationDidBecomeActive(_ application: UIApplication) {
FileSinki.didBecomeActive()
}
- 将
FileSinki.receivedNotification(userInfo)
添加到didReceiveRemoteNotification
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
FileSinki.receivedNotification(userInfo)
completionHandler(.newData)
}
注意:在我的经验中,application.registerForRemoteNotifications()
将不起作用,并且它没有任何 didReceiveRemoteNotification
或它的 didFail
等效项将在首次调用后至少24小时被调用。在某个时候,它将开始工作,一旦苹果推送通知服务完成它的工作。
作者
- 詹姆斯·瓦纳斯 (@jamesvanas)
许可证
FileSinki 是使用 MIT 许可证发布的。有关详细信息,请参阅 LICENSE 文件。