> On Jan 1, 2016, at 7:00 PM, Jared Sinclair via swift-evolution
> <[email protected]> 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
> [email protected] <mailto:[email protected]>
> 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
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution