[swift-evolution] Swift-Native Alternative to KVO
Charles Srstka
cocoadev at charlessoft.com
Fri Jan 1 20:11:44 CST 2016
> On Jan 1, 2016, at 7:00 PM, Jared Sinclair via swift-evolution <swift-evolution at swift.org> wrote:
>
> The one-to-many observer pattern could really use a first-party, native Swift solution. The day-to-day practice of writing iOS / OS X applications needs it, and we end up falling back on antiquated APIs like KVO or NSNotificationCenter, or short-lived third-party libraries. This is an essential need that deserves a fresh approach.
>
> What follows is a rough proposal for a Swift-native “KVO alternative”.
>
>
> What Usage Would Look Like:
>
> 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.”
>
> Behind the Scenes:
>
> - When compiling a Swift class “Foo", the compiler would also generate a companion “Foo_SwiftObservables” class.
>
> - When initializing an instance of a Swift class, an instance of the companion “ Foo_SwiftObservables” class would be initialized and set as the value of a reserved member name like “observables”. This member would be implicit, like `self`.
>
> - The auto-generated “ Foo_SwiftObservables” class would have a corresponding property for every observable property of the target class, with an identical name.
>
> - Each property of the auto-generated “ Foo_SwiftObservables” class would be an instance of a generic `Observable<T>` class, where `T` would be assigned to the value of the associated property of the target class.
>
> - The `Observable<T>` class would have two public functions: addObserver() and removeObserver().
>
> - The addObserver() function would take a single closure argument. This closure would have a signature like: (oldValue: T?, newValue: T?) -> Void.
>
> - Observer closures would have the same memory management and variable scope rules as any other closure. Callers would not be obligated to remove their observer closures. Doing so would be a non-mandatory best practice.
>
>
> Rough Code Examples
>
> Consider a class for a Twitter client like:
>
> class Tweet {
> var isLiked: Bool = false
> let text: String
>
> init(text: String) {
> self.text = text
> }
> }
> The compiler would generate a companion observables class:
>
> class Tweet_SwiftObservables {
> let isLiked = Observable<Bool>()
> }
> Notice that only the `isLiked` property is carried over, since the `text` property of `Tweet` is a let, not a var.
>
> The generic Observable class would be something like (pseudo-codish):
>
> 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
> }
> }
>
> Benefits of This Approach
>
> It’s familiar. It resembles the core mechanic of KVO without the 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.
>
>
> Thanks for reading,
>
>
> --
> Jared Sinclair
> @jaredsinclair
> jaredsinclair.com <http://jaredsinclair.com/> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org <mailto:swift-evolution at swift.org>
> https://lists.swift.org/mailman/listinfo/swift-evolution <https://lists.swift.org/mailman/listinfo/swift-evolution>
I’ve been thinking about this for a while, as well. I think the problem would be better served by a simpler approach. Generating entire new classes is the way the existing KVO mechanism works, but I don’t really see the necessity in it.
Here’s my counter-pitch: add an “observable" keyword on property declarations, so your “isLIked” property would look like this:
class Tweet {
observable var isLiked: Bool = false
let text: String
init(text: String) {
self.text = text
}
}
My first instinct is to make observations based on strings, as KVO does, as this is easier to integrate with XIB-based user interface elements, as well as making it possible for it to interact with the legacy KVO system. Using strings also makes it possible to bind to key paths, which can be useful. However, if one wanted to make the observations based on pointers rather than strings, for type safety, that would also be possible.
The strings would be provided by a parameter on the “observable” attribute (i.e. observable(“foo”)); if no parameter is provided, Swift would automatically insert the property name as the key.
When a property is marked “observable”, the Swift compiler rewrites the class to something resembling the following pseudocode:
class Tweet {
var isLiked_Observers: [(oldValue: Bool, newValue: Bool) -> ()]? = nil
var isLiked: Bool {
didSet(oldValue) {
// optional for performance reasons; in the common case where there are no observers,
// checking an optional has less of a performance penalty than checking whether an array is empty.
if let observers = self.isLiked_Observers {
for eachObserver in observers {
eachObserver(oldValue: oldValue, newValue: self.isLiked)
}
}
}
}
let text: String
init(text: String) {
self.text = text
}
}
If there are no observers, the only cost added to the setter would be that of setting an optional.
What usage would look like:
let tweet = Tweet(text: “Hi.”)
tweet.addObserverFor(“isLiked") { oldValue, newValue in
print(“Value changed.”)
}
tweet.isLiked = true // Console would log “Value changed.”
If isLiked later becomes a calculated property, it would add a “depedencies” attribute, which would return a set of other observable properties, like so:
class Tweet {
observable var numberOfLikes: Int = 0
observable var isLiked: Bool {
dependencies { return ["numberOfLikes"] }
get { return !self.numberOfLikes.isEmpty }
set(isLiked) {
if isLiked {
self.numberOfLikes += 1
} else {
self.numberOfLikes = 0 // or something; it’s just an example
}
}
}
let text: String
init(text: String) {
self.text = text
}
}
In this example, the “isLiked” property would generate an observation on “numberOfLikes” that would look something like the following. For this example, we introduce a cached version of the previous value of “isLiked" so that we have a copy of the old value in the case that the dependency changes (and isLiked’s willSet and didSet thus won’t fire). An alternative solution would be to run observation closures before *and* after setting each property, as KVO does; however, this would not perform as well, and would be more difficult to manage in multithreaded situations.
// generated by the compiler and added to initialization
self.addObserverFor(“numberOfLikes”) { [weak self] _, _ in
if let observers = self.observers {
for eachObserver in observers {
eachObserver(oldValue: isLiked_Cached, newValue: self.isLiked)
}
}
isLiked_Cached = self.isLiked
}
This would cause our notifications to be fired even for computed properties when one of the dependencies changes.
One final little perk would allow us to specify a dispatch queue upon which observations should be fired:
class Tweet {
observable var isLiked: Bool = false {
dispatchQueue { return dispatch_get_main_queue() }
}
let text: String
init(text: String) {
self.text = text
}
}
The benefits of this should not need explanation.
Benefits of this approach:
- It’s relatively simple and lightweight, with no additional classes being created, and the common case adding only the cost of an optional check to property setters.
- It’s easy to understand and to use.
- All observable properties are marked “observable” in the UI. As an example of why this is desirable, consider that you had been observing the “isLiked” property from the first example, and then the implementation of the Tweet class changed to the “numberOfLikes” implementation without the author considering KVO. Your observer code would break, and you would no longer get notifications for “isLiked” if “numberOfLikes” changed. Having an “observable” keyword communicates a contract to the user that this property will remain observable, and any changes will be made in a way that won’t break observability. Such contract is important to have if your client code is relying on the observation working properly.
- An additional benefit to only adding the observation code for properties that are explicitly marked “observable” is that the optional-check performance costs, as well as the memory allocation for the array of closures, can be skipped for properties that don’t need to be observable.
Alternatives Considered:
One could possibly base the observation around actual pointers, rather than strings. This would preclude any future extension to make this system interact with the existing KVO system, as well as necessitate some sort of rethinking about how bindings would be set up in XIB files, but it would add greater type safety (particularly, the compiler could enforce that observations could only be done on properties that were actually declared as observable).
One could introduce *two* arrays of observation closures, one to be fired before the setter runs and one afterward. This would work more similarly to how KVO works, but would introduce an extra performance cost, as each set would have to check *two* optionals rather than just one, and since the setter would have to wait for the “willSet” closure to finish before setting the property, which could slow down worker code. This would, however, eliminate the need for keeping a cached value for observable computed properties.
Charles
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160101/3eef09cd/attachment.html>
More information about the swift-evolution
mailing list