Swift KVO alternative

You may have noticed that KVO isn’t really supported terribly well in Swift. It feels like a second-class citizen. So, inspired by ReactiveCocoa and some conversations with the uber-smart Manuel Chakravarty, I came up with the below solution which I’d like to share with you.

It’s a deliberately simplified version of Reactive’s MutableProperty, as I find that Reactive is a tough sell when you’re part of a big team. However, it is much more approachable to introduce something small and simple like this.

Note that by prioritising simplicity, it lacks certain features such as thread-safety (which you could implement yourself if required - I generally prefer to do almost everything on the main thread). It is also not provided as a pod/carthage framework because if you brought this into your app, you really should customise it further as per your needs. Plus it’s like 60 lines of code. However, all that said, it is a good replacement for KVO.

So here it is, ready for you to test-drive in a Swift playground:

class Property<T> {
    private var _value: T
    var value: T {
        get { return _value }
        set {
            _value = newValue
            tellSubscribers()
        }
    }
    
    init(_ value: T) {
        _value = value
    }
    
    var subscriptions = [Subscription<T>]()
    func subscribe(subscriber: AnyObject, next: T -> Void) {
        subscriptions.append(
            Subscription(subscriber: subscriber, next: next))
    }
    
    private func tellSubscribers() {
        subscriptions =
            subscriptions.flatMap(tellAndFilterSubscription)
    }
    
    private func tellAndFilterSubscription(subscription:
           Subscription<T>) -> Subscription<T>? {
        if subscription.subscriber != nil { // Subscriber exists.
            subscription.next(_value)
            return subscription
        } else { // Subscriber has gone; cull this subscription.
            return nil
        }
    }
}

struct Subscription<T> {
    weak var subscriber: AnyObject?
    let next: T -> Void
}

How it works

The idea is that it stores a weak reference to the ‘subscriber’ in a subscription struct alongside the ‘subscription’ block, which together goes into a ‘subscriptions’ array. The subscriptions where the subscriber = nil get culled each time it broadcasts a new value.

How to use it

Here’s how you’d declare a property in one of your classes:

class UserManager {
    // Something that isn't nillable, and has an initial value.
    let username = Property("InitialValue")
    
    // Something that is nillable, and has no initial value.
    let nickname = Property<String?>(nil)
}

And here’s how you subscribe to updates on a property. The below code will take care of freeing the block after your view controller disappears, so you don’t need to worry about memory management:

class UserViewController {
    func subscribe() {
        UserManager.sharedManager.username.subscribe(self) {
            [weak self] newValue in
            self?.userLabel.text = newValue
        }
...

And before you know it, you’re half-way towards Reactive coding techniques. See, that wasn’t too hard was it? :)

Tests

Unit tests are great, right? Here’s my approximation of them using a playground:

class Foo { // The one with the property.
    let property = Property("Blah")
}
class Bar { // The observer.
    var string: String?
}
class Test {
    var string: String?
    func test() {
        // Setup bar subscribed to foo's property.
        let foo = Foo()
        var bar: Bar? = Bar()
        foo.property.subscribe(bar!) {
            [weak bar] newValue in
            bar?.string = newValue
        }
        
        // Example of how to subscribe.
        foo.property.subscribe(self) {
            [weak self] newValue in
            self?.string = newValue
        }
        
        // Test it calls the subscriber.
        foo.property.value = "Test"
        print(bar!.string) // Should be "Test"
        print(string) // Should also be "Test"
        
        // Test it culls nilled subscribers.
        print(foo.property.subscriptions.count) // Should be 1.
        bar = nil
        foo.property.value = "GiveItAChanceToCull"
        print(foo.property.subscriptions.count) // Should be 0.
    }
}
Test().test()

Have a good one!

Thanks for reading! And if you want to get in touch, I'd love to hear from you: chris.hulbert at gmail.

Chris Hulbert

(Comp Sci, Hons - UTS)

iOS Developer (Freelancer / Contractor) in Australia.

I have worked at places such as Google, Cochlear, Assembly Payments, News Corp, Fox Sports, NineMSN, FetchTV, Coles, Woolworths, Trust Bank, and Westpac, among others. If you're looking for help developing an iOS app, drop me a line!

Get in touch:
[email protected]
github.com/chrishulbert
linkedin



 Subscribe via RSS