Swift’s Limited Oberservers and How To Fix Them

Last week I was tearing apart code generated by the Swift 3.1 compiler to look at how the Swift observers, willSet, and didSet are implemented. I was surprised by a number of things, mostly by how limited the observers are.

Let’s start with the background. Swift variables are, in actuality, a lot closer to .NET properties. When you declare the following class:

open class Sample {
   public var x:Int = 0
}

The swift compiler will reserve space in the class for x, but it will also author two hidden methods, a getter for x and a setter for x. Inside the class, x will act like a field, but outside the class the compiler may choose to use the hidden methods instead.

Now let’s add in willSet and didSet methods:

open class Sample {
   public var x:Int = 0 {
       willSet {
          print("Sample x will be set to \(newValue)")
       }
       didSet {
          print("Sample x changed, old value is \(oldValue)")
       }
   }
}

Now if we make and instance of Sample and set it’s x to, say, 7, this will print “Sample x will be set to 7” and then “Sample x changed, old value is 0”.

There are some limitations though. In Swift, there are a few different types of properties: stored properties and computed properties. In these examples, x is a stored property. If x was computed property, we’d have problems.

open class Sample {
   public var w:Int = 0
   public var x:Int {
       get {
           return w * 2
       }
       set {
           w = newValue / 2
       }
       willSet { // compiler error
          print("Sample x will be set to \(newValue)")
       }
       didSet { // compiler error
          print("Sample x changed, old value is \(oldValue)")
       }
   }
}

Swift hates when you put observers on computed properties. Unless they’re overrides.

open class Sample {
   public var w:Int = 0
   public var x:Int {
       get {
           return w * 2
       }
       set {
           w = newValue / 2
       }
   }
}
open class SubSample : Sample {
    override var x:Int {
       willSet { // legal
          print("SubSample x will be set to \(newValue)")
       }
       didSet { // legal
          print("SubSample x changed, old value is \(oldValue)")
       }
   }
}

The reason this is the case is in the Swift language reference:

The stored or computed nature of an inherited property is not known by a subclass

And with all this information, we can get a clear picture at how this is implemented.  Here’s a more complete example with output:

open class Sample {
   public var x:Int = 0 {
      willSet {
         print("Sample willSet")
      }
      didSet {
         print("Sample didSet")
      }
   }
}
open class SubSample : Sample {
    override var x:Int {
       willSet {
          print("SubSample willSet")
       }
       didSet { // legal
          print("SubSample didSet")
       }
   }
}
let cl = SubSample()
cl.x = 7
// output:
// SubSample willSet
// Sample willSet
// Sample didSet
// SubSample didSet

Note that the parent’s willSet/didSet pair are nested inside the child’s. This has to do with the implementation of the setters. Each setter looks syntactically like a field set, but there’s more. It ends up looking like this:

open class Sample {
   public var x:Int = 0 {
      set (newValue) {
          let oldValue = x_field
          willSet(newValue)
          x_field = newValue
          didSet(oldValue)
      }
      willSet {
         print("Sample willSet")
      }
      didSet {
         print("Sample didSet")
      }
   }
}
open class SubSample : Sample {
    override var x:Int {
      set (newValue) {
          let oldValue = super.x
          willSet(newValue)
          super.x = newValue
          didSet(oldValue)
      }
      willSet {
          print("SubSample willSet")
      }
      didSet { // legal
          print("SubSample didSet")
      }
   }
}
 

What happens is that if you don’t supply a setter, the swift compiler will inject the willSet/didSet code for you. And that explains why you don’t get this code in computed properties. The compiler would have to inject the willSet/didSet as a prelude/postlude, but it starts to get complicated especially when the setter has multiple exit points or unusual flow. They punted rather than generate unpredictable code and I don’t fault them for it.

But there’s a problem with the observers as is. They’re just not good for very much, honestly. I’d call them syntactic sugar, but they’re not because they don’t really sweeten things. This is more like Java where if you want to do something actually useful, you end up typing in a pile of boiler plate code (violating D.R.Y.). Let’s call it syntactic coffee (I dislike coffee, it’s nasty and bitter). Here’s what you’d really like an observer to do: have a list of clients that subscribe to it and get notified when the code reaches the point of inflection. Instead, Swift hides the point of inflections and only objects in the hierarchy have access to them. If you want more, you have to do a lot more work and at that point why are you even using willSet/didSet?

To be fair, the .NET event pattern (which is a single broadcaster, multiple listener model) is nearly as bad in terms of repetition, but at least the use of EventHandler<T> has made that better, compared with what it used to be.

Can we make this better? Somewhat.

public class Broadcaster {
    private var _currentTag:Int = 0
    private var _listeners:[(_listener:(T)->(), _tag:Int)] = []
    public init() { }
 
    // represents a synchronous lock
    private func lock( _ lock: Any, closure: () -> ()) {
        objc_sync_enter(lock)
        closure()
        objc_sync_exit(lock)
    }
 
    // add a listener to the chain of listeners
    public func add(listener:@escaping (T)->()) -> Int
    {
        var tag = 0
        lock (self) {
            tag = _currentTag;
            _currentTag = _currentTag + 1
            _listeners.append((listener, tag))
        }
        return tag;
    }
 
    private func indexOf(tag: Int) -> Int? {
        return _listeners.index(where: { (l, t) in return t == tag })
    }
 
    // remove a listener associated with tag.
    // returns true if it was found and removed
 
    public func remove(tag: Int) -> Bool
    {
        var index:Int? = nil
        lock (self) {
            index = indexOf(tag:tag)
            if index != nil {
                _listeners.remove(at: index!)
            }
        }
        return index != nil
    }
 
    // returns true if there is a listener associated with the given tag
    public func contains(tag:Int) -> Bool {
        var containsIt = false
        lock (self) {
            containsIt = indexOf(tag:tag) != nil
        }
        return containsIt
    }
 
    // returns a listner for the given tag, nil is not found
    public func listenerFor(tag: Int) -> ((T)->())? {
        var listener: ((T)->())? = nil
        lock (self) {
            if let index = indexOf(tag:tag) {
                listener = _listeners[index]._listener
            }
        }
        return listener
    }
 
    // broadcast the value to each listener
    public func broadcast(value:T) {
        lock (self) {
            _listeners.forEach() { listener in listener._listener(value) }
        }
    }
 
    // sugar operator
    static func <= (left: Broadcaster, right: @escaping (T)->()) -> Int {
        return left.add(listener: right)
    }
}
 
public class Sample {
   public var willSetX = Broadcaster()
   public var didSetX = Broadcaster()
   public var x:Int {
       willSet {
           willSetX.broadcast(newValue)
       }
       didSet {
           didSetX.broadcast(newValue)
       }
   }
}
let cl = Sample()
cl.willSetX <= { newValue in print("outside listener heard a new value \(newValue)"); }
cl.x = 5
// output
// outside listener heard a new value 5

My Broadcaster class is a little more than is needed in most cases, but the thread safety is important, especially in apps where the UI is running on the main thread and broadcasting changes to other threads. I did this with the lock function. Swift has a cute syntactic hack that if you write a function whose last argument is a closure you can either pass the closure in as an argument or put the closure afterwards. Since closure syntax was designed to always be in braces, it makes it look like you’re adding new syntax to the language. In fact, you’re not and the abstraction here is leaky as all hell and bit me a couple times while I was using it. For example, in the add() method, I wanted to return the tag inside the closure, but that makes no sense since the closure type returns an empty tuple (aka, void), so I had to put in a state variable. There’s not much you can do to fix this.

But we see that with this little gem, we can at least at proper even handling to Swift even if it still has a somewhat bitter syntactic coffee flavor.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.