FileSinki 1.0.2

FileSinki 1.0.2

James Vanas 维护。



FileSinki 1.0.2

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)
        }       
    }
}

在这里,你可以异步或在不同线程上执行任何工作,但必须在工作完成后使用最终项目调用 keepmerged

观察变化

类似于向 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.swiftCodable+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 配置

  1. 在应用程序的 功能 中启用 CloudKit。注意您的应用程序 CloudKit 容器标识符供稍后使用。

App Capabilities

  1. https://icloud.developer.apple.com 中,前往您的应用程序开发 模式,并添加一个名为 FileSinki 的新 记录类型,具有以下 自定义字段
  • path(类型:String)
  • type(类型:String)
  • asset(类型:Asset)
  • data(类型:Bytes)
  • deleted(类型:Int(64))
  1. 在 FileSinki 记录模式中,点击 编辑索引,添加以下索引
  • recordName可以查询的
  • type可以查询的
  • path可搜索的

并保存更改。

最终结果应如下所示

CloudKitRecordType

注意:一旦您已验证 FileSinki 在开发环境中工作正常,请不要忘记将模式部署到生产

Deploy to Production

AppDelegate

将以下代码添加到您的 AppDelegate(或等效的 MacOS 代表函数)中

  1. 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
}
  1. FileSinki.didBecomeActive() 添加到 applicationDidBecomeActive
func applicationDidBecomeActive(_ application: UIApplication) {      
    FileSinki.didBecomeActive()
}
  1. 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小时被调用。在某个时候,它将开始工作,一旦苹果推送通知服务完成它的工作。

作者

许可证

FileSinki 是使用 MIT 许可证发布的。有关详细信息,请参阅 LICENSE 文件。