Swift Keyed Callbacks
Overview
On a recent project I was working on, I needed a key value store to persist some data which my app needed in various spots and wanted to somehow receive updates whenever values for certain keys were updated.
I started out with a simple in memory one (backed by a Dictionary), here’s what it looked like:
protocol KeyValueStore
{
func valueForKey(key: String) -> String
func setValue(value: String, forKey key: String)
func removeValueForKey(key: String)
}
class DictionaryKeyValueStore : KeyValueStore
{
private var store = [String : String]()
// MARK: KeyValueStore
func valueForKey(key: String) -> String?
{
return store[key]
}
func setValue(value: String, forKey key: String)
{
store[key] = value
}
func removeValueForKey(key: String)
{
store.removeValueForKey(key)
}
}
In situations like this, a protocol is perfect! It removes any commitments to a particular implementation and allows an alternate one to be provided at a later time with minimal change to the users of that component.
A Simple Approach
My first (poor) stab at a solution looked like this:
protocol KeyValueStore
{
func valueForKey(key: String) -> String
func setValue(value: String, forKey key: String)
func removeValueForKey(key: String)
func registerForUpdates(key: String, callback: (key: String, newValue:String)->Void)
}
class DictionaryKeyValueStore : KeyValueStore
{
typealias UpdatesCallback = (key: String, newValue:String?)->Void
private var store = [String : String]()
private var keyedCallbacks = [String: [UpdatesCallback]]()
// MARK: KeyValueStore
func valueForKey(key: String) -> String?
{
return store[key]
}
func setValue(value: String, forKey key: String)
{
store[key] = value
notifyChanges(key, newValue: value)
}
func removeValueForKey(key: String)
{
store.removeValueForKey(key)
notifyChanges(key, newValue: nil)
}
func registerForUpdates(key: String, callback: UpdatesCallback)
{
if var keyCallbacks = keyedCallbacks[key]
{
keyCallbacks.append(callback)
keyedCallbacks[key] = keyCallbacks
}
else
{
keyedCallbacks[key] = [callback]
}
}
// MARK: Private
private func notifyChanges(key: String, newValue: String?)
{
if let keyCallbacks = keyedCallbacks[key]
{
for callback in keyCallbacks
{
callback(key: key, newValue: newValue)
}
}
}
}
This worked as expected and allowed me to continue with my work but it did have its flaws. The main one being, there was no way of deregistering callbacks. This can be a problem especially if the store’s lifetime exceeds the lifetime of the classes that register callbacks.
Making it generic
Before solving the deregistration problem, I wanted to spend some time on making callback management a bit more generic as I found myself needing callbacks elsewhere in the app.
The callback management code was extracted to its own generic class.
class KeyedCallbacks<CallbackParameters>
{
typealias Callback = (CallbackParameters) -> Void
private var keyedCallbacks = [String: [Callback]]()
func register(key: String, callback: Callback)
{
if var keyCallbacks = keyedCallbacks[key]
{
keyCallbacks.append(callback)
keyedCallbacks[key] = keyCallbacks
}
else
{
keyedCallbacks[key] = [callback]
}
}
func performCallbacksForKey(key:String, withParameters parameters: CallbackParameters)
{
if let keyCallbacks = keyedCallbacks[key]
{
for callback in keyCallbacks
{
callback(parameters)
}
}
}
}
class DictionaryKeyValueStore : KeyValueStore
{
typealias CallbackParameters = (key: String, newValue:String?)
typealias UpdatesCallback = CallbackParameters->Void
private var store = [String : String]()
private var keyedCallbacks = KeyedCallbacks<CallbackParameters>()
// MARK: KeyValueStore
func valueForKey(key: String) -> String?
{
return store[key]
}
func setValue(value: String, forKey key: String)
{
store[key] = value
notifyChanges(key, newValue: value)
}
func removeValueForKey(key: String)
{
store.removeValueForKey(key)
notifyChanges(key, newValue: nil)
}
func registerForUpdates(key: String, callback: UpdatesCallback)
{
keyedCallbacks.register(key , callback: callback)
}
// MARK: Private
private func notifyChanges(key: String, newValue: String?)
{
keyedCallbacks.performCallbacksForKey(key, withParameters: (key: key, newValue: newValue ))
}
}
This relieved the key value store from the callback management responsibility making it cleaner and also allowed me to re-use the callback management class.
Deregistering callbacks
Now for the interesting part, how do we remove callbacks?
Since the callbacks are keyed in a dictionary we can remove all callbacks for a given key - but that is not desired, we want to remove a single callback for a given key. Had closures been equatable or had some means of comparison the following would have been suffice:
func deregister(key: String, callback: Callback)
{
if var keyCallbacks = keyedCallbacks[key],
index = keyCallbacks.indexOf(callback) // This does not work!
{
keyCallbacks.removeAtIndex(index)
if keyCallbacks.count > 0
{
keyedCallbacks[key] = keyCallbacks
}
else
{
keyedCallbacks.removeValueForKey(key)
}
}
}
Sadly there is no reliable way to compare two closures in Swift. A few options have been mentioned on Stackoverflow - but as I say not reliable, nor nice for that matter.
Besides Even if there was, this solution may look good implementation wise, but its not so great for the users of the KeyedCallbacks
class. They would now need to keep a reference to their callbacks and loose the ability to define them inline. What’s worse is if one of the callers instance methods is registered instead - that would result in a memory leak!
There are of course ways to mitigate the leaks, but all in all this is not a great solution.
A better approach
The issue we were facing was identifying closures in order to remove specific ones. Perhaps we could internally create some kind of token or handle that is equatable and hand it back to the caller. To deregister, the caller can hand back the handle and internally the KeyedCallbacks class will know exactly which callback to remove.
This is what that could look like:
protocol CallbackHandle
{
}
// ..
func register(key: String, callback: Callback) -> CallbackHandle
func deregister(handle: CallbackHandle)
// ..
public protocol CallbackHandle
{
}
private class KeyedCallbackHandle<CallbackParameters> : CallbackHandle, Equatable
{
typealias Callback = (CallbackParameters) -> Void
let key: String
let callback : Callback
init(key: String, callback: Callback)
{
self.key = key
self.callback = callback
}
}
private func ==<T>(lhs: KeyedCallbackHandle<T>, rhs: KeyedCallbackHandle<T>) -> Bool
{
return lhs === rhs
}
public class KeyedCallbacks<CallbackParameters>
{
typealias Callback = (CallbackParameters) -> Void
private var keyedHandles = [String: [KeyedCallbackHandle<CallbackParameters>]]()
public init()
{
}
public func register(key: String, callback: Callback) -> CallbackHandle
{
let handle = KeyedCallbackHandle<CallbackParameters>(key:key, callback:callback)
if var handles = keyedHandles[key]
{
handles.append(handle)
keyedHandles[key] = handles
}
else
{
keyedHandles[key] = [handle]
}
return handle
}
public func deregister(handle: CallbackHandle)
{
if let keyedCallbackHandle = handle as? KeyedCallbackHandle<CallbackParameters>,
var handles = keyedHandles[keyedCallbackHandle.key],
let index = handles.indexOf(keyedCallbackHandle)
{
handles.removeAtIndex(index)
if handles.count > 0
{
keyedHandles[keyedCallbackHandle.key] = handles
}
else
{
keyedHandles.removeValueForKey(keyedCallbackHandle.key)
}
}
}
public func performCallbacksForKey(key:String, withParameters parameters: CallbackParameters)
{
if let handles = keyedHandles[key]
{
for handle in handles
{
handle.callback(parameters)
}
}
}
}
Taking it a step further
Here’s the solution I landed on in the end which was inspired by a concept a few of my colleagues adopted in a different project.
We could make the callback automatically deregister as soon as the CallbackHandle
goes out of scope. As such, callers don’t need to worry about deregistering, all they need to do is just keep the handle alive as long as they need callbacks.
To do so, we’re going to need an additional closure in KeyedCallbackHandle
that gets executed when it gets deallocated.
private class KeyedCallbackHandle<CallbackParameters> : CallbackHandle, Equatable
{
typealias Callback = (CallbackParameters) -> Void
let key: String
let callback : Callback
var onDeinit : (()->Void)?
init(key: String, callback: Callback)
{
self.key = key
self.callback = callback
}
deinit
{
onDeinit?()
}
}
The register method can now attach some additional code to that closure to remove the callback.
// ..
public func register(key: String, callback: Callback) -> CallbackHandle
{
let handle = KeyedCallbackHandle<CallbackParameters>(key:key, callback:callback)
handle.onDeinit = { [weak self, weak handle] in
if let s = self, let h = handle
{
s.deregister(h)
}
}
if var handles = keyedHandles[key]
{
handles.append(handle)
keyedHandles[key] = handles
}
else
{
keyedHandles[key] = [handle]
}
return handle
}
// ..
The only thing left to do is to create weak wrapper for the KeyedCallbackHandle
to store in the internal dictionary. That way the only strong reference to the handle will be the with the caller.
private class WeakKeyedCallbackHandle<CallbackParameters> : Equatable
{
weak var handle : KeyedCallbackHandle<CallbackParameters>?
init(_ handle: KeyedCallbackHandle<CallbackParameters>)
{
self.handle = handle
}
}
private func ==<T>(lhs: WeakKeyedCallbackHandle<T>, rhs: WeakKeyedCallbackHandle<T>) -> Bool
{
return lhs.handle == rhs.handle
}
public class KeyedCallbacks<CallbackParameters>
{
typealias Callback = (CallbackParameters) -> Void
private var keyedHandles = [String: [WeakKeyedCallbackHandle<CallbackParameters>]]()
public init()
{
}
public func register(key: String, callback: Callback) -> CallbackHandle
{
let handle = KeyedCallbackHandle<CallbackParameters>(key:key, callback:callback)
handle.onDeinit = { [weak self, weak handle] in
if let s = self, let h = handle
{
s.deregister(h)
}
}
let weakHandle = WeakKeyedCallbackHandle(handle)
if var handles = keyedHandles[key]
{
handles.append(weakHandle)
keyedHandles[key] = handles
}
else
{
keyedHandles[key] = [weakHandle]
}
return handle
}
public func deregister(handle: CallbackHandle)
{
if let keyedCallbackHandle = handle as? KeyedCallbackHandle<CallbackParameters>,
var handles = keyedHandles[keyedCallbackHandle.key],
let index = handles.indexOf(WeakKeyedCallbackHandle(handle))
{
handles.removeAtIndex(index)
if handles.count > 0
{
keyedHandles[keyedCallbackHandle.key] = handles
}
else
{
keyedHandles.removeValueForKey(keyedCallbackHandle.key)
}
}
}
public func performCallbacksForKey(key:String, withParameters parameters: CallbackParameters)
{
if let weakHandles = keyedHandles[key]
{
for weakHandle in weakHandles
{
weakHandle.handle?.callback(parameters)
}
}
}
}
Finally some tidy ups can be made. The code that adds and removes callbacks from the dictionary can be extracted to its own class. This will keep the the main class shorter and easier to follow.
private class KeyedCallbackHandles<CallbackParameters>
{
private var keyedHandles = [String: [WeakKeyedCallbackHandle<CallbackParameters>]]()
func add(handle: KeyedCallbackHandle<CallbackParameters>)
{
handle.onDeinit = { [weak self, weak handle] in
if let s = self, let h = handle
{
s.remove(h)
}
}
let weakHandle = WeakKeyedCallbackHandle(handle)
if var handles = keyedHandles[handle.key]
{
handles.append(weakHandle)
keyedHandles[handle.key] = handles
}
else
{
keyedHandles[handle.key] = [weakHandle]
}
}
func remove(handle: KeyedCallbackHandle<CallbackParameters>)
{
if var handles = keyedHandles[handle.key],
let index = handles.indexOf(WeakKeyedCallbackHandle(handle))
{
handles.removeAtIndex(index)
if handles.count > 0
{
keyedHandles[handle.key] = handles
}
else
{
keyedHandles.removeValueForKey(handle.key)
}
}
}
func handlesForKey(key: String) -> [KeyedCallbackHandle<CallbackParameters>]?
{
var handles : [KeyedCallbackHandle<CallbackParameters>]?
if let weakHandles = keyedHandles[key]
{
handles = weakHandles.flatMap({ $0.handle })
}
return handles
}
}
public class KeyedCallbacks<CallbackParameters>
{
typealias Callback = (CallbackParameters) -> Void
private var keyedHandles = KeyedCallbackHandles<CallbackParameters>()
public init()
{
}
public func register(key: String, callback: Callback) -> CallbackHandle
{
let handle = KeyedCallbackHandle<CallbackParameters>(key:key, callback:callback)
keyedHandles.add(handle)
return handle
}
public func deregister(handle: CallbackHandle)
{
if let keyedCallbackHandle = handle as? KeyedCallbackHandle<CallbackParameters>
{
keyedHandles.remove(keyedCallbackHandle)
}
}
public func performCallbacksForKey(key:String, withParameters parameters: CallbackParameters)
{
if let handles = keyedHandles.handlesForKey(key)
{
for handle in handles
{
handle.callback(parameters)
}
}
}
}
Usage
With KeyedCallbacks
complete, the key value store needs one minor tweak to return the callback handle.
// ..
class DictionaryKeyValueStore : KeyValueStore
{
// ..
private var keyedCallbacks = KeyedCallbacks<CallbackParameters>()
// ..
func registerForUpdates(key: String, callback: UpdatesCallback) -> CallbackHandle
{
return keyedCallbacks.register(key , callback: callback)
}
// ..
}
// ..
Putting it all together - this is what registering for callbacks on the key value store looks like:
public func usage()
{
let keyValueStore = DictionaryKeyValueStore()
let handle1 = keyValueStore.registerForUpdates("a") { print(" ## \($0)") }
unused(handle1)
do
{
let handle2 = keyValueStore.registerForUpdates("a") { print(" >> \($0)") }
unused(handle2)
keyValueStore.setValue("cool!", forKey: "a")
//prints
//
// ## ("a", Optional("cool!"))
// >> ("a", Optional("cool!"))
}
keyValueStore.setValue("warmer", forKey: "a")
//prints
//
// ## ("a", Optional("warmer"))
}
public func unused(_ : Any)
{
// This is needed to supress the unused variable warning
}
… and we’re done! We now have a re-usable component for managing callbacks in a scoped fashion albeit not thread safe.
The complete example can be seen here. Also the complete class with further tidy ups and tests can be found on GitHub.