创建ReactiveSwiftRealm-第2部分

在第1部分中,我解释了如何创建与Carthage,Cocoapods和子模块兼容的基础项目。 现在是时候编写代码了。

在最初的帖子中,关于合并ReactiveSwift和Realm的过程中,我解释了我想要获得的东西,这是第一步。 我想要一个围绕Realm基本操作的反应式包装器。 让我们从添加操作开始。

我是TDD的忠实拥护者,所以我将从Realm add操作开始在这里应用它。 在测试之前,我们应该将在主要目标上拥有的相同链接框架添加到测试目标。 如果我们不这样做,将会得到奇怪的Xcode错误。

我只需要围绕该简单任务的反应式包装,以便可以在反应式上下文中轻松使用它。 我最需要的是什么?

  • 一个保存方法,该方法返回SignalProducer

为了测试我需要

  • 调用某些东西,它应该返回SignalProducer

我的测试结束是这样的:

当然,它会失败,这是我应该在TDD中期望的(现在看起来很愚蠢,但是由于代码库很大,您可能会编写测试代码,并且在您期望测试失败时第一次通过测试。看到测试失败也是重要),因此请对其进行修复,使其通过测试(仅此而已)。

在ReactiveSwiftRealm组中,我将创建一个ReactiveSwiftRealm.swift文件,其中包含通过测试所需的代码:

 导入ReactiveSwift 
导入结果

func add()-> SignalProducer {
返回SignalProducer(值:())
}

我需要一个函数来返回SignalProducer,这就是我得到的。 现在,我必须在测试中导入ReactiveSwift和Result,以便它知道什么是SignalProducer,然后运行测试。

通过第一个测试,我必须考虑我需要的下一个功能。

  • SignalProducer应该保存一个Realm对象

好的,因此我需要保存对象,并且需要添加功能来获取该对象并将其保存。 让我们编写测试代码!

好的,再次失败,正如预期的那样。 (是的,您可以庆祝某些事情行不通)。 让我们修复它:

  • 导入RealmSwift
  • 按照Realm步骤创建FakeObject类
  FakeObject类:Object { 
动态var id =“”
}
  • 更改添加函数,使其带有对象类型参数(您也需要在添加声明文件中导入RealmSwift)

现在,我们将看到我们的第一个测试失败,我们已经更改了函数声明,因此可以预期会失败。 只需对其进行更改,使其也需要一个伪造的对象。

现在我们的第二项测试失败了。 为什么? 好吧,它什么都不保存,所以让我们更改add函数,以便保存对象。 我们将在这里找到并发布,我们需要一个领域参考才能将该对象存储在数据库中。 好的,让我们在add函数中添加一个realm参数,以便我们可以在内部使用它。

当然,由于我们再次更改了功能,两项测试都将失败。 由于我们在两个测试中都需要一个领域实例,因此我将在ReactiveSwiftRealmTest级别上创建它,以便可以在任何测试中使用它。 我将使用领域测试功能,该功能允许我使用内存中领域(而不是真实的数据库):

 覆盖func setUp(){ 
super.setUp()
Realm.Configuration.defaultConfiguration.inMemoryIdentifier = self.name
境界=尝试! 领域()
}

这样,我们可以确保没有任何存储,并且每个测试都在干净的Realm实例中运行。 域是一个类变量:

var realm:领域!

在这一点上,我们可以删除我们创建的第一个测试,该测试仅出于示例目的而创建,我们将添加更多的测试,因此只需将其删除即可。

下一步,我希望此添加的SignalProducer在对象存储时发送一个Signal。 现在我有一个空的SignalProducer,这没用。

这就是我想要的,我运行了测试并且通过了。 等一下 没错,测试通过了,因为我们返回的是一个带有值的SignalProducer,因此它已经调用了我们所需的一切。 我们需要测试失败,因此我们将其更改为真正的SignalProducer,以使其失败。

  func add(object:Object,realm:Realm)-> SignalProducer  { 
返回SignalProducer {观察者,_ in
尝试! realm.write {
realm.add(对象)
}

}
}

在测试从SignalProducer创建的异步操作时,我们需要在测试中添加一些代码,以便使用期望值:

  func testAddSendsSignal(){ 
让期望= self.expectation(描述:“就绪”)
让fakeObject = FakeObject()
add(object:fakeObject,realm:realm).startWithValues {value in
让对象= self.realm.objects(FakeObject.self)
XCTAssertEqual(objects.count,1)
Expectation.fulfill()
}

waitForExpectations(超时:0.1){错误

}
}

现在测试按预期失败,我们没有发送信号,因此失败了:很好。 现在让我们在领域操作完成后发送该信号:

  func add(object:Object,realm:Realm)-> SignalProducer  { 
返回SignalProducer {观察者,_ in
尝试! realm.write {
realm.add(对象)
}
reader.send(value:())
reader.sendCompleted()
}
}

再次运行测试,现在通过了。 一切都按预期进行。

我们已经有了add方法,让我们转到更新之一。 我将在这一问题上走得更快,直到发现问题为止。

我很高兴使用添加和更新经过测试的方法,直到我的应用崩溃为止,哎呀! 正如Realm在其在线文档中所说的那样,如果您在另一个线程中使用一个对象,它将崩溃,而这正是我在做的事情。 我返回测试并重现了该问题:

  func testUpdateChangesObjectOnDifferentThread(){ 
让期望= self.expectation(描述:“就绪”)
让fakeObject = FakeObject()
fakeObject.value =“ oldValue”
尝试! realm.write {
realm.add(fakeObject)
}

让对象= self.realm.objects(FakeObject.self)
XCTAssertEqual(objects.count,1)
DispatchQueue.global(qos:.background).async {
update(object:fakeObject,realm:self.realm){
fakeObject.value =“ updatedValue”
} .startWithValues {_
让对象= self.realm.objects(FakeObject.self)
XCTAssertEqual(objects.first?.value,“ updatedValue”)
Expectation.fulfill()
}
}

waitForExpectations(超时:0.1){错误

}
}

我发现避免此问题的最佳方法是永远不要在后台线程中调用此方法。 如果我想做一个后台操作,该操作应该存在于函数中并返回主线程。 那么,我现在需要什么?

  • 如果未在主线程上调用更新,则应引发错误
  • 更新应该让我说它应该在主线程还是后台线程上运行
  • 更新应始终在主线程上发送其信号

因此,在我通过这一测试之前,让我们添加更多测试。

  func testUpdateSendsErrorWhenNotOnMainThread(){ 
让期望= self.expectation(描述:“就绪”)
让fakeObject = FakeObject()
fakeObject.value =“ oldValue”
尝试! realm.write {
realm.add(fakeObject)
}

让对象= self.realm.objects(FakeObject.self)
XCTAssertEqual(objects.count,1)
DispatchQueue.global(qos:.background).async {
更新(对象:fakeObject,领域:self.realm,操作:{
fakeObject.value =“ updatedValue”
})。on(失败:{错误
XCTAssertEqual(错误,.wrongThread)
Expectation.fulfill()
})。开始()
}

waitForExpectations(超时:0.1){错误

}
}

现在出现了棘手的问题,如果我不能在另一个线程中使用该对象,该如何在后台运行操作。 幸运的是,Realm具有一种称为ThreadSafeReference的功能,非常适合这种情况,但是它可能会失败,因此我们应该使用错误枚举来处理错误。

在这种情况下,我们还应该更改update的工作方式,因为我们将在操作闭包中使用的对象引用可能是来自后台线程的对象引用,因此我们应使用该引用而不是fakeObject实例进行操作。

但是,我传递了一个Object实例,该实例在闭包中将毫无用处。 没问题:泛型抢救!

  func update (object:T,realm:Realm,thread:ReactiveSwiftRealmThread = .main,operation:@escaping(_ object:T)->())-> SignalProducer  { 
返回SignalProducer {观察者,_ in
如果!Thread.isMainThread {
reader.send(错误:.wrongThread)
返回
}
切换线{
案例.main:
尝试! realm.write {
操作(对象)
}
reader.send(value:())
reader.sendCompleted()
案例.background:
让objectRef = ThreadSafeReference(to:object)
DispatchQueue(label:“ background”)。async {
让境界=尝试! 领域()
保护让对象= realm.resolve(objectRef)else {
观察者发送(错误:.deletedInAnotherThread)
返回
}
尝试! realm.write {
操作(对象)
}
reader.send(value:())
reader.sendCompleted()
}
}

}
}

更新功能更大了,但是测试通过了。 万一该操作应在后台执行,我们需要一个新的Realm实例(实例无法在线程之间共享,否则应用程序将崩溃),检查是否未从另一个线程中删除我们的对象,然后使用新对象引用作为闭包的参数。

现在我们有最后一个测试。 我不希望这个更新函数弄乱它外面的代码,所以信号应该在调用该函数的同一个线程(主线程)中发送。

  func testUpdateInBackgroundSendsSignalInMain(){ 
让期望= self.expectation(描述:“就绪”)
让fakeObject = FakeObject()
fakeObject.value =“ oldValue”
尝试! realm.write {
realm.add(fakeObject)
}

让对象= self.realm.objects(FakeObject.self)
XCTAssertEqual(objects.count,1)
update(object:fakeObject,realm:realm,thread:.background){对象在
XCTAssertFalse(Thread.isMainThread)
object.value =“ updatedValue”
} .on(value:{
XCTAssertTrue(Thread.isMainThread)
Expectation.fulfill()
})。开始()

waitForExpectations(超时:0.1){错误

}
}

失败,我们更改更新代码:

  func update (object:T,realm:Realm,thread:ReactiveSwiftRealmThread = .main,operation:@escaping(_ object:T)->())-> SignalProducer  { 
返回SignalProducer {观察者,_ in
如果!Thread.isMainThread {
reader.send(错误:.wrongThread)
返回
}
切换线{
案例.main:
尝试! realm.write {
操作(对象)
}
reader.send(value:())
reader.sendCompleted()
案例.background:
让objectRef = ThreadSafeReference(to:object)
DispatchQueue(label:“ background”)。async {
让境界=尝试! 领域()
保护让对象= realm.resolve(objectRef)else {
观察者发送(错误:.deletedInAnotherThread)
返回
}
尝试! realm.write {
操作(对象)
}
DispatchQueue.main.async {
reader.send(value:())
reader.sendCompleted()
}
}
}

}
}

现在它过去了。 我避免了应用程序崩溃,并且可以执行后台更新。

是时候停止添加和重构我们拥有的东西了。 尽管使用泛型非常酷,但我发现有些事情可能会更好:

  • 现在两个函数对背景的处理方式有所不同,我认为add函数应该使用与update相同的思想在后台工作
  • 全局函数很酷,但是直接在对象上执行操作会更好(减少参数数量)
  • 不需要Realm实例
  • 该闭包参数很难理解,让我们使用通用的typealias对其进行重构

重构,检查所有失败的测试,更新测试并立即检查所有工作。 我无法在此处粘贴所有代码,但是您可以在存储库中阅读它。 我还将为添加到添加功能的后台功能添加新的测试。 这种情况更加复杂,因为添加的对象可能是新对象或已经存在的对象。 使用self属性,我们可以检查对象是否已存储,并且仅在需要时使用ThreadSafeReference。

我将按照相同的想法添加remove函数,不,我们进入下一个大问题,数组呢? 我需要添加数组支持,以便可以添加,更新或删除对象数组。

我将添加以下测试:

  • 添加元素
  • 在背景中添加的元素
  • 元素已更新
  • 元素在后台更新
  • 元素已删除
  • 在背景中删除的元素

这里最大的问题是背景,我们现在有一个可能崩溃的对象列表。

我还将在两个添加函数中都添加了update选项,因此realm可以更新对象,而不是忽略已保存的对象(具有相同的ID)。 请检查回购以查看测试和代码。

我还为单个添加函数添加了一个错误案例,因此它会发送一个对象已经存在的错误而不是异常。 我没有将其添加到数组添加中,因为检查id是否存在任何对象会导致过多的开销,并且可能会使函数真正变慢。

现在,我们有了将数据写入领域数据库所需的所有方法,下一步将是查询,但这将在第3部分中进行。