Creating ReactiveSwiftRealm — Part 2

On Part 1 I explained how to create the base project compatible with Carthage, Cocoapods and submodules. Now it’s time to code.

On the original post talking about merging ReactiveSwift and Realm I explained what I wanted to get, that’s the first step. I want a reactive wrapper around Realm basic operations. Let’s start with the add operation.

I’m a big fan of TDD so I’ll apply it here, starting with Realm add operation. Before we can test we should add the same linked frameworks we had on the main target to the test target. If we don’t do it we’re going to get weird Xcode errors.

I just need a reactive wrapper around that easy task so it can be easily used in a reactive context. What’s the most basic thing I need?

  • A save method that returns a SignalProducer<(),NoError>

In order to test that I’ll need

  • Call something and it should return a SignalProducer<(),NoError>

My test ended being something like this:

Of course It fails, that’s what I should expect in TDD (seems stupid now but with a big codebase it could happen that you code a test and it passes the first time when you’re expecting it to fail. Seeing a test fail is also important) so let’s fix it so it passes the test (and nothing more).

In ReactiveSwiftRealm group I’m going to create a ReactiveSwiftRealm.swift file with the code required to pass the test:

import ReactiveSwift
import Result

func add()->SignalProducer<(),NoError>{
return SignalProducer(value: ())
}

I required a function to return a SignalProducer and that’s what I got. Now I’ll have to import ReactiveSwift and Result on my test so it knows what a SignalProducer is and then run the test.

With that first test passing I’ve to think in the next feature I need.

  • The SignalProducer should save a Realm object

Ok, so I need and object to be saved and I need the add function to take that object and save it. Let’s code the test!

Ok, again, it fails, as expected. (Yes, you can celebrate that something doesn’t work). Let’s fix it:

  • Import RealmSwift
  • Create a FakeObject class following Realm steps
class FakeObject: Object{
dynamic var id = ""
}
  • Change add function so it takes an Object type parameter (You’ll need the RealmSwift import in add declaration file too)

Now we’ll see that our first test fails, we’ve changed the function declaration so failure is expected. Just change it so it also takes a fake object.

Now our second test fails. Why? Well, it doesn’t save anything, so let’s change the add function so it saves the object. We’ll find and issue here, we need a realm reference in order to store that object in the database. Ok, let’s add a realm parameter to our add function so we can use it inside.

Of course both test will fail as we’ve changed the function again. As we need a realm instance in both test I’m going to create it at ReactiveSwiftRealmTest level so I can use it on any test. I’m going to use the Realm testing feature that allows me to use an in memory realm (instead of a real db):

override func setUp() {
super.setUp()
Realm.Configuration.defaultConfiguration.inMemoryIdentifier = self.name
realm = try! Realm()
}

This way we could be sure that nothing is stored and that each test runs in a clean Realm instance. realm is a class variable:

var realm:Realm!

At this point we can remove the first test we created, it was created just for example purposes and we’re adding many more so just remove it.

Next step, I want this add SignalProducer to send a Signal when object is stored. Right now I have an empty SignalProducer, that’s useless.

That’s what I want, I run the test and it passes. Wait, what? That’s right, test passes because we’re returning a SignalProducer with a value so it already calls all we need. We need the test to fail so we’re going to change it to a real SignalProducer so it fails.

func add(object:Object,realm:Realm)->SignalProducer<(),NoError>{
return SignalProducer{ observer,_ in
try! realm.write {
realm.add(object)
}

}
}

As we’re testing an async operation created from the SignalProducer we need to add some code to our test so it uses expectations:

func testAddSendsSignal(){
let expectation = self.expectation(description: "ready")
let fakeObject = FakeObject()
add(object: fakeObject,realm:realm).startWithValues { value in
let objects = self.realm.objects(FakeObject.self)
XCTAssertEqual(objects.count, 1)
expectation.fulfill()
}

waitForExpectations(timeout: 0.1){ error in

}
}

Now test fails as expected, we don’t send a signal so it fails: great. Now let’s send that signal once realm operation is completed:

func add(object:Object,realm:Realm)->SignalProducer<(),NoError>{
return SignalProducer{ observer,_ in
try! realm.write {
realm.add(object)
}
observer.send(value: ())
observer.sendCompleted()
}
}

Run the test again and now it passes. Everything worked as expected.

We already have our add method, let’s move to the update one. I’ll move faster over this one just to the point where I found an issue.

I was happy using my add and update tested methods until my app crashed, ouch! As Realm says on it’s online docs it will crash if you use an object in a different thread and that was exactly what I was doing. I went back to my tests and reproduced the issue:

func testUpdateChangesObjectOnDifferentThread(){
let expectation = self.expectation(description: "ready")
let fakeObject = FakeObject()
fakeObject.value = "oldValue"
try! realm.write {
realm.add(fakeObject)
}

let objects = self.realm.objects(FakeObject.self)
XCTAssertEqual(objects.count, 1)
DispatchQueue.global(qos: .background).async {
update(object: fakeObject, realm: self.realm) { realm in
fakeObject.value = "updatedValue"
}.startWithValues { _ in
let objects = self.realm.objects(FakeObject.self)
XCTAssertEqual(objects.first?.value, "updatedValue")
expectation.fulfill()
}
}

waitForExpectations(timeout: 0.1){ error in

}
}

The best way I’ve found to avoid this issue is never, ever, call this in a background thread. If I want to do a background operation that should live inside the function and return back to the main thread. So, what do I need now?

  • update should fire an error if not called on main thread
  • update should allow me to say if it should run on main thread or background thread
  • update should always send it’s signal on main thread

So let’s add some more tests before I can make this one pass.

func testUpdateSendsErrorWhenNotOnMainThread(){
let expectation = self.expectation(description: "ready")
let fakeObject = FakeObject()
fakeObject.value = "oldValue"
try! realm.write {
realm.add(fakeObject)
}

let objects = self.realm.objects(FakeObject.self)
XCTAssertEqual(objects.count, 1)
DispatchQueue.global(qos: .background).async {
update(object: fakeObject, realm: self.realm, operation: {
fakeObject.value = "updatedValue"
}).on( failed: { error in
XCTAssertEqual(error, .wrongThread)
expectation.fulfill()
}).start()
}

waitForExpectations(timeout: 0.1){ error in

}
}

And now comes the tricky one, if I can’t use the object in another thread how could I run an operation in background. Fortunately Realm has a feature called ThreadSafeReference perfect for this case but it could fail, so we should handle the error with our error enum.

We should also change how update works in this case, as the object reference we’ll use in the operation closure could be the one from the background thread so we should operate with that one instead of our fakeObject instance.

But, I pass an Object instance, that would be useless in the closure. No problem: Generics to the rescue!

Batman loves generics
func update<T:Object>(object:T,realm:Realm,thread:ReactiveSwiftRealmThread = .main,operation:@escaping (_ object:T) -> ())->SignalProducer<(),ReactiveSwiftRealmError>{
return SignalProducer{ observer,_ in
if !Thread.isMainThread{
observer.send(error: .wrongThread)
return
}
switch thread{
case .main:
try! realm.write {
operation(object)
}
observer.send(value: ())
observer.sendCompleted()
case .background:
let objectRef = ThreadSafeReference(to: object)
DispatchQueue(label: "background").async {
let realm = try! Realm()
guard let object = realm.resolve(objectRef) else {
observer.send(error: .deletedInAnotherThread)
return
}
try! realm.write {
operation(object)
}
observer.send(value: ())
observer.sendCompleted()
}
}

}
}

Update function got a lot bigger but test passes. In case the operation should be performed in background we need a new Realm instance (instances can’t be shared between threads or app will crash), check if our object isn’t deleted from another thread and then we perform the write operation using the new object reference as the parameter to our closure.

Now we have one last test. I don’t want this update function to mess with code outside it so the signal should be sended in the same thread the function was called, the main thread.

func testUpdateInBackgroundSendsSignalInMain(){
let expectation = self.expectation(description: "ready")
let fakeObject = FakeObject()
fakeObject.value = "oldValue"
try! realm.write {
realm.add(fakeObject)
}

let objects = self.realm.objects(FakeObject.self)
XCTAssertEqual(objects.count, 1)
update(object: fakeObject, realm: realm, thread: .background) {object in
XCTAssertFalse(Thread.isMainThread)
object.value = "updatedValue"
}.on(value: {
XCTAssertTrue(Thread.isMainThread)
expectation.fulfill()
}).start()

waitForExpectations(timeout: 0.1){ error in

}
}

Fails, we change our update code:

func update<T:Object>(object:T,realm:Realm,thread:ReactiveSwiftRealmThread = .main,operation:@escaping (_ object:T) -> ())->SignalProducer<(),ReactiveSwiftRealmError>{
return SignalProducer{ observer,_ in
if !Thread.isMainThread{
observer.send(error: .wrongThread)
return
}
switch thread{
case .main:
try! realm.write {
operation(object)
}
observer.send(value: ())
observer.sendCompleted()
case .background:
let objectRef = ThreadSafeReference(to: object)
DispatchQueue(label: "background").async {
let realm = try! Realm()
guard let object = realm.resolve(objectRef) else {
observer.send(error: .deletedInAnotherThread)
return
}
try! realm.write {
operation(object)
}
DispatchQueue.main.async {
observer.send(value: ())
observer.sendCompleted()
}
}
}

}
}

And now it passes. I’ve avoided an app crash and I can perform background updates.

Time to stop adding and refactor what we have. I see some things that could be better although using generics was pretty cool:

  • Now both functions handle background differently, I think add function should work in background using the same idea as update
  • Global functions are cool, but performing operations directly on objects would be better (reducing the number of parameters)
  • Realm instance should’t be required
  • That closure parameter is ugly as hell, let’s refactor it with a generic typealias

Refactor, check all the tests failing, update tests and check it all works now. I can’t paste all the code here but you can read it on the repo. I’ll add too a new test for the background feature added to the add function. This case is more complex as added objects could be new or already existing objects. Using the self property we could check if the object is already stored and use ThreadSafeReference only when required.

I’ll add the remove function following the same idea and no we move to the next big issue, what about arrays? I need to add arrays support so I can add, update or delete an array of objects.

I’ll add the tests for:

  • Elements added
  • Elements added in background
  • Elements updated
  • Elements updated in background
  • Elements removed
  • Elements removed in background

The big issue here is background, we now have a list of objects that could crash.

I’ll also add the update option to both add functions so realm could update the object instead of ignoring it if it’s already saved (it has same id). Please check the repo to see tests and code.

I also added an error case for single add function so instead of an exception it will send an object already exists error. I didn’t add it to array adding because checking any of the object exists by id would be too much overhead and could make the function really slow.

Now we’ve all the methods I need to write data to realm database, next step would be queries but that would be on part 3.