patternswiftMinor
Usability of this Observable class
Viewed 0 times
usabilityclassthisobservable
Problem
I created an Observable class that allows me to subscribe to a wrapped value. The implementation looks like this:
This allows me to create a variable and observer it as follows:
```
var observableString = Observable("String")
observableString.addObser
class Observable {
typealias DidChangeHandler = (oldValue: ValueType?, newValue: ValueType) -> ()
// MARK: Properties
var value : ValueType {
didSet {
for (owner, handlers) in self._observers {
for handler in handlers {
handler(oldValue: oldValue, newValue: value)
}
}
}
}
// MARK: Initializers
init(_ value: ValueType) {
self.value = value
}
// MARK: Methods
func addObserverForOwner(owner: AnyObject, triggerImmediately: Bool, handler: DidChangeHandler) {
if let index = self._indexOfOwner(owner) {
// since the owner exists, add the handler to the existing array
self._observers[index].handlers.append(handler)
}
else {
// since the owner does not already exist, add a new tuple with the
// owner and an array with the handler
self._observers.append(owner: owner, handlers: [handler])
}
if (triggerImmediately) {
handler(oldValue: nil, newValue: self.value)
}
}
func removeObserversForOwner(owner: AnyObject) {
if let index = self._indexOfOwner(owner) {
self._observers.removeAtIndex(index)
}
}
// MARK: Private Properties
private var _observers: [(owner: AnyObject, handlers: [DidChangeHandler])] = []
// MARK: Private Methods
private func _indexOfOwner(owner: AnyObject) -> Int? {
var index: Int = 0
for (possibleOwner, handlers) in self._observers {
if possibleOwner === owner {
return index
}
index++
}
return nil
}
}This allows me to create a variable and observer it as follows:
```
var observableString = Observable("String")
observableString.addObser
Solution
You worry about
Since what we're doing with this is so similar to the existing KVO in Objective-C (which can also be used in Swift), we'd do well to make this look and feel like KVO.
The KVO method looks like this:
Now, I'm not going to recommend a direct copy. We can drop the
But, let's make some changes, shall we?
First of all, let's change our handler. Instead of a method that takes an
We're doing this, because we should change
The first two are self explanatory.
I'm not suggesting you use the
But anyway, about our
If the
If the
Anyways, changing the add observer method to:
Means that you can always add options in the future without screwing up any of the existing uses. And changing the handler to taking a dictionary means we throw all the values we want to send to the handler into the dictionary, and not only is the handler flexible for different current use cases, it is also flexible for all future use cases.
As a final note, I might include constant values to use as the keys to the "change" dictionary.
triggerImmediately being too verbose (which can be fixed), yet you have addObserverForOwner as the primary function name, and for me, this is the part that is a bit too verbose.Since what we're doing with this is so similar to the existing KVO in Objective-C (which can also be used in Swift), we'd do well to make this look and feel like KVO.
The KVO method looks like this:
addObserver:forKeyPath:options:context:. In KVO however, we don't get to pick our own handler. We just have to implement observeValueForKeyPath:ofObject:change:context:.Now, I'm not going to recommend a direct copy. We can drop the
forKeyPath: part of the first method. The general consensus with KVO also seems to be that context: is relatively useless (it's a value you send when you start observing, and it is sent back to you in the call back method). I also do like that you're suggesting the user can pick whatever method or function they want as the handler.But, let's make some changes, shall we?
First of all, let's change our handler. Instead of a method that takes an
oldValue and a newValue, let's use a method that just takes a dictionary, shall we?We're doing this, because we should change
triggerImmediately to options, and provide an enum that lets users build a bitmask for their options. In KVO, we have the following options:NSKeyValueObservingOptionNew
NSKeyValueObservingOptionOld
NSKeyValueObservingOptionInitial
NSKeyValueObservingOptionPrior
The first two are self explanatory.
NSKeyValueObservingOptionInitial is a flag that say the handler block should be called before the addObserver method is finished--we send the current value of the observable to the handler block as a "new" value.NSKeyValueObservingOptionPrior means the user wants to know about the values just before they're actually changed. In the case of Swift, this means we'd fire in willSet with this option on (but we also still fire in didSet).I'm not suggesting you use the
NSKeyValueObservingOption enum. I suggest you create your own enum. As a minimum, include these options, but you may want to include others as well at some point in the future. Maybe a StopObservingAfterChange option that automatically removes the observer after the first didSet for example.But anyway, about our
handler. It should just take a dictionary argument as I stated earlier. What values it contains will depend on the scenario and which bits are set in the options.If the
new bit is set, the dictionary will always hold the new value. If the old bit is set, it will hold the old value in every situation it can (i.e. not when the initial bit is set and we fire before the addObserver method returns).If the
prior bit is set, when we call the handler from willSet, we include another key: "prior" which holds a boolean value of true. I guess you could realistically include this key every time just set it to false in every other scenario.Anyways, changing the add observer method to:
addObserver(observer:AnyObject, options:ObservableOptions, handler:DidChangeHandler);Means that you can always add options in the future without screwing up any of the existing uses. And changing the handler to taking a dictionary means we throw all the values we want to send to the handler into the dictionary, and not only is the handler flexible for different current use cases, it is also flexible for all future use cases.
As a final note, I might include constant values to use as the keys to the "change" dictionary.
Code Snippets
addObserver(observer:AnyObject, options:ObservableOptions, handler:DidChangeHandler);Context
StackExchange Code Review Q#58220, answer score: 4
Revisions (0)
No revisions yet.