Forked 提供了一种通用的方法来管理 Swift 应用程序中的共享数据,从而控制数据竞争和竞态条件。它可以用来替代 actor、锁和队列,或者与它们结合使用。
Forked 可以在单个 iOS 应用程序、Swift 服务器或分布式网络中运行。例如,ForkedCloudKit
包仅需几行代码即可支持跨设备的数据同步。
简而言之,还在等什么呢?开始 Forked 吧!1
谁愿意在不确定一个框架是否适合自己之前就投入时间呢?没有人,就是这样!
因此,我们已将 Forkers 示例应用程序上传到 App Store 供您试用。(请注意,它是非公开的,因此请使用链接而不是搜索。)Forkers 应用程序基于 Forked 构建,并且源代码就在这里。试用一下,别忘了测试 iCloud 同步!
添加到您的 Package.swift
dependencies: [
.package(url: "https://github.com/drewmccormack/Forked.git", from: "0.1.0")
]
https://github.com/drewmccormack/Forked.git
Forked
以及您需要的任何子包Sendable
值类型对数据建模,这些类型可以在线程和隔离域之间轻松传递Codable
,可轻松持久化到磁盘和云服务Forked 基于类似于 Git 的分散模型。它跟踪对共享数据资源的更改,并使用三向合并解决冲突。您拥有控制权,并且永远不会丢失对数据的任何更改。
与锁、队列和 actor 相比,Forked 不会序列化对资源的访问。相反,Forked 提供了一种 branching 机制,可以系统地创建数据的副本,这些副本可以并发修改,并在稍后合并。
Forked 会处理 branching 过程中涉及的所有逻辑,包括在分支(称为 *forks*)发散时保留数据的副本。此“发散”副本称为 *common ancestor*,它很重要,因为在需要再次合并 forks 时,Forked 可以使用它来确定更改的内容以及在哪个 fork 中。
使用 Forked,您可以随时安全地合并分支 — 以任何顺序 — 使用强大的合并算法,这些算法远远超出其他数据建模框架中可用的算法。例如,Forked 利用所谓的无冲突复制数据类型 (CRDT) 以一种对人类来说合乎逻辑的方式合并文本,而不是选择一种只有机器才能理解的解决方案。
准备好玩了吗?让我们通过示例了解 Forked。
这是您的第一个 fork
import Forked
let uiFork = Fork(name: "ui")
let intResource = QuickFork<Int>(initialValue: 0, forks: [uiFork])
QuickFork
是一种方便的方法来创建一个包含单个值的内存中的 ForkedResource
,在本例中是一个 Int
。
我们还声明了 uiFork
,这是一个命名的 fork。除了您自己创建的 forks 之外,所有 ForkedResource
实例都有一个名为 main
的中心 fork,可以与任何其他 fork 合并。
让我们更新 uiFork
上的 Int
,并独立更新 main
fork 上的 Int
,然后合并以获得结果
try intResource.update(uiFork, with: 1)
try intResource.update(.main, with: 2)
try intResource.mergeIntoMain(from: uiFork)
let resultInt = try intResource.value(in: .main)!
在这种情况下,resultInt
将为 2
,因为那是最近设置的值。
对于像 Int
这样的原子类型,合并的结果不是很令人感兴趣;Forked 的真正威力来自于它合并复杂数据类型的能力。
让我们首先定义一个结构体,以便我们可以控制 Int
的合并行为。
struct AccumulatingInt: Mergeable {
var value: Int = 0
func merged(withSubordinate other: Self, commonAncestor: Self) throws -> Self {
return AccumulatingInt(value: self.value + other.value - commonAncestor.value)
}
}
通过遵循 Mergeable
协议,AccumulatingInt
可以完全控制其合并方式。
Mergeable
协议需要函数 merged(withSubordinate:commonAncestor:)
。subordinate
是来自另一个 fork 的冲突值,而 commonAncestor
是两个 fork 发散时的值。
AccumulatingInt
的合并算法确定自创建公共祖先以来,每个 fork 上发生了哪些更改,并将这些更改相加,以生成一个新值。
如果我们在原始示例中使用 AccumulatingInt
而不是 Int
,则结果将为 3
,因为 uiFork
递增了 1
,而 main
fork 递增了 2
,总共为 3
。
因此,您可以提出可以以您选择的任何方式合并的结构体,但这些合并算法可能会很快变得复杂。这就是子包 ForkedMerge
的用武之地:它提供了标准的内置合并算法。
想象一下,我们正在开发一个使用这个过度简化模型的文本编辑器
import Forked
import ForkedMerge
struct TextDocument: Mergeable {
var text: String = ""
func merged(withSubordinate other: Self, commonAncestor: Self) throws -> Self {
let newText = try TextMerger().merge(
self.text,
withSubordinate: other.text,
commonAncestor: commonAncestor.text
)
return TextDocument(text: newText)
}
}
它看起来不多,但您刚刚为完全协作的文本编辑器创建了模型。例如,如果模型最初包含文本“Fork Yeah”,并且……
TextMerger
将合并为“Fork yeah!!!”完全控制合并是件好事,而 ForkedMerge
提供的合并算法使其更容易将事物组合在一起,但是如果 Forked
可以自动生成此代码,那不是更好吗?
这就是 ForkedModel
的用途。它使用 Swift 宏使定义全局数据模型几乎变得微不足道。
让我们更新 TextDocument
以使用 ForkedModel
import Forked
import ForkedModel
@ForkedModel
struct TextDocument {
@Merged var text: String = ""
}
“其余的部分呢?” 我听到你哭了。没有其余的部分!这就是 forking 的全部!
此代码等效于我们在上一节中手动编写的代码。它可以构成完全协作的文本编辑器的基础,或者只是一个通过 iCloud 同步的个人编辑器。
它不会止步于此:结构体中的每个属性都会独立合并。您可以从简单类型的标准 *原子* 合并中进行选择,也可以为常见的 Swift 类型(如 String
、Array
、Dictionary
和 Set
)选择高级合并算法。
为了演示,这是一个更复杂的 TextDocument
示例
import Forked
import ForkedModel
@ForkedModel
struct TextDocument {
var id: UUID = UUID()
@Merged var text: String = ""
@Merged var tags: Set<String> = []
@Merged(using: .textMerge) var comment: String = ""
@Merged var editCount: AccumulatingInt = .init()
var cursorPosition: Int = 0
}
@Merged
属性告诉 ForkedModel
该属性是 Mergeable
,它应该使用适当的合并算法。大多数常见类型都有默认值,但是您可以通过将不同的合并算法传递给 using:
参数来覆盖此默认值。
如果您有一个自定义的 Mergeable
类型,例如 AccumulatingInt
,则应用 @Merged
将导致它使用您提供的 merged(withSubordinate:commonAncestor:)
方法进行合并。
未附加 @Merged
的属性将以原子方式合并,较新的更改优先于较旧的更改。属性将以属性方式合并,基于属性本身的最新更改
开始使用 Forked
的一个好方法是查看提供的示例应用程序。它们的难度范围从非常基本到功能齐全的基于 iCloud 的联系人应用程序。
Actors 很好地解决了 Swift 中的数据竞争问题,但它们对竞态条件没有任何帮助,甚至可能产生新的竞态条件。此示例显示您可以在 actor 内部使用 ForkedResource
以直接的方式处理竞态条件。
设置一个简单的可合并模型,类似于上面的模型。UI 允许您在两个不同的 fork 中更改文本和计数器的值,然后按下一个按钮,您就可以看到它们是如何合并的。
此示例中的模型非常简单,并且其重要性次于如何设置 CloudKitExchange
以与 iCloud 同步数据。该示例显示了如何使用 ForkedResource
在磁盘上进行存储,更新属性以在 SwiftUI 中显示,以及监视 fork 的更改以便在从 iCloud 收到更改时刷新 UI。
Forkers 是一款联系人应用程序,用于跟踪您最喜欢的 forkers。该模型比其他示例更复杂,显示了如何嵌套 Mergeable
类型,在本例中是联系人的 Array
。它还与 iCloud 集成,提供功能齐全的本地优先联系人应用程序。
每个子包都有可用的文档。
这是核心包,需要使用任何其他包。它提供了 ForkedResource
,它是 Forked
的基本构建块。
此包提供了 Mergeable
类型的标准合并算法。它还包括许多无冲突复制数据类型 (CRDT)。
此包提供了 @ForkedModel
和 @Merged
宏,允许您使用值类型定义全局数据模型。
这提供了 CloudKitExchange
类,它可以自动将 ForkedResource
在设备之间与 iCloud 同步。
欢迎贡献!请随时提交 pull request 或提出 issue。
Forked 在 MIT 许可证下可用。有关更多信息,请参见 LICENSE 文件。
“Forking”笑话的灵感来自 The Good Place。快去看吧! ↩