Search code examples
iosswiftmvvmrealmreactive-cocoa

Realm List KVO observation


The Realm Swift documentation states that most of the properties you use on a model class can be observed using KVO. Using ReactiveCocoa, for every property I have on a model class, I create a comparable rac_ prefixed property that sends value changes which I can then use to bind to views in an MVVM style architecture.

An example model class could look like:

 class Post: Object {
     dynamic var text = ""
     private(set) lazy var rac_text: AnyProperty<String> = { [unowned self] in
         return AnyProperty(initialValue: self.name, signal: self.rac_valuesForKeyPath("text", observer: self).toSignal().takeUntil(self.willDeallocSignal())
     }()
 }

This is incredibly handy becuase 1) it's not mutable from the outside (AnyProperty vs MutableProperty and 2) only lives as long as the model does with .takeUntil(self.willDeallocSignal()). (I also wanted to ask, is the [unowned self] bit necessary here? I'm not sure if self gets captured or not, always been bad at that).

The problem comes in with List properties. Lists can't be marked dynamic which makes sense, their type can't be represented in objective-c. And key value observing works just fine, with one major caveat.

Take the same class with a relationship list property:

class Post: Object {
    let users: List<User> = List<User>()
}

The corresponding reactive observation property should look something along the lines of:

private(set) lazy var rac_users: AnyProperty<List<User>> = {
    return AnyProperty(initialValue: self.users, signal: self.rac_valuesForKeyPath("users", observer: self).toSignal().takeUntil(self.willDeallocSignal()))
}()

However on observation, the signal doesn't emit List objects, it emits RLMArray objects. I've had to jerryrig a signal producer that looks like:

private(set) lazy var rac_posts: AnyProperty<List<Post>> = { [unowned self] in
    return AnyProperty<List<Post>>(initialValue: self.posts, producer: self.rac_valuesForKeyPath("posts", observer: self)
        .toSignalProducer()
        .assumeNoErrors()
        .map { $0 as! RLMArray }
        .map { array in
            var list = List<Post>()
            for i in 0..<array.count {
                if let element =  array[i] as? Post {
                    list.append(element)
                }
            }
            return list
    })
}()

Of course, the if let statement always fails because RLMObject can't be cast to Post. So I either need a) a way to convert RLMObjects to Objects or b) a way to kvo on a list property that emits a list. I tested it using traditional KVO and got the same results.


Solution

  • You can observe properties of type List with Realm Swift by using addNotificationBlock.

    This method takes a closure, which is called on every change. At least as long as you don't block the run loop, then notifications might come coalesced. The initial value is reported as well via this mechanism, so you might get additional signals through that, which you might not expect.

    You should be able to wire up this with Reactive Cocoa like seen below:

    private(set) lazy var rac_posts: AnyProperty<List<Post>> = { [unowned self] in
        return AnyProperty<List<Post>>(initialValue: self.posts, signal: Signal<List<Post>>() { [unowned self] observer in
            let notificationToken = self.posts.addNotificationBlock { list in
                observer.sendNext(list)
            }
    
            return ActionDisposable() {
                notificationToken.stop()
            }
        }
    }()
    

    Once #3359 is merged, you will also get fine-grained notifications, which will inform you about the detailed change within the list.