FileSinki
使用 CloudKit 在 iOS、MacOS 和 tvOS 之间轻松进行文件同步。
基本用法
import FileSinkiFileSyncable
采用 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 文件。


