Imagining a First-Party Swift KVO Replacement
My last post got me thinking: if Apple were to add a KVO-like service to Swift, what would it look like? I wish I understood more about how programming languages are created, and how compilers work. What follows isn’t really a serious proposal, but just a sketch of what I would personally want to use as a practicing software developer.
What if every Swift class
received an implicit, protected member with a reserved name like observables
which provided a means to register closures as observers for changed values? An example usage might look like this:
let tweet = Tweet(text: “Hi.”) tweet.observables.isLiked.addObserver { (oldValue, newValue) -> Void in // this is otherwise just a standard closure, with identical // memory management rules and variable scope semantics. print(“Value changed.”) } tweet.isLiked = true // Console would log “Value changed.”
The observables
property would be implied, just like self
. It would be an instance of some ad-hoc generated Swift class created by the compiler at compile time. For every observable property of the owning class, this observables
class will have a corresponding observable property. So if you created a class like this:
class Tweet { var isLiked: Bool = false let text: String init(text: String) { self.text = text } }
The Swift compiler would generate a class like this and assign it to the observables
property for Tweet
:
class Tweet_SwiftObservables { let isLiked = Observable<Bool>() }
The Tweet_SwiftObservables
class name would be auto-generated based on the class name of the target class. Notice that only the isLiked
property is carried over, since the text
property of Tweet
is a let
, not a var
.
The isLiked
member of Tweet_SwiftObservables
is an instance of a generic Observable<T>
class, whose implementation would be something like the following (though, of course, more nuanced):
class Observable<T> { typealias Observer = (oldValue: T?, newValue: T?) -> Void private var observers = [UInt: Observer]() func addObserver(observer: Observer) -> Uint { let token: Uint = 0 // generate some unique token self.observers[token] = observer return token } func removeObserverForToken(token: Uint) { self.observers[token] = nil } }
The money shot is the addObserver()
method. This method accepts a single Observer
argument, which is just a simple closure. It would follow all the existing rules of memory management and variable scope as any other closure. addObserver()
returns a unsigned integer token that can be used to remove the observer at a later time.
What I like about my idea is:
- It’s familiar. It resembles the core mechanic of KVO without the antiquated hassle. It uses existing memory management rules. Everything you already understand about closures applies here.
- It’s type-safe. The
Observable<T>
generic class ensures at compile-time that your observers don’t receive an incorrect type. - It’s readable. The syntax is brief without being unclear. Implementing the observation closure at the same call site as
addObserver()
keeps cause and effect as close together as possible. - It’s easy. It abandons a stringly-typed API in favor of a compile-time API. Since the
Foo_SwiftObservables
classes would be auto-generated by the compiler, there’s no need for busywork tasks like keeping redundant manual protocols or keyword constants up to date with the target classes.