motion/core/kvo.rb
# Usage example:
#
# class ExampleViewController < UIViewController
# include BubbleWrap::KVO
#
# def viewDidLoad
# @label = UILabel.alloc.initWithFrame [[20,20],[280,44]]
# @label.text = ""
# view.addSubview @label
#
# observe(@label, :text) do |old_value, new_value|
# puts "Changed #{old_value} to #{new_value}"
# end
# end
#
# end
#
# @see https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html#//apple_ref/doc/uid/10000177i
module BubbleWrap
module KVO
class Registry
COLLECTION_OPERATIONS = [ NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, NSKeyValueChangeReplacement ]
OPTION_MAP = {
new: NSKeyValueChangeNewKey,
old: NSKeyValueChangeOldKey
}
attr_reader :callbacks, :keys
def initialize(value_keys = [:old, :new])
@keys = value_keys.inject([]) do |acc, key|
value_change_key = OPTION_MAP[key]
if value_change_key.nil?
raise RuntimeError, "Unknown value change key #{key}. Possible keys: #{OPTION_MAP.keys}"
end
acc << value_change_key
end
@callbacks = Hash.new do |hash, key|
hash[key] = Hash.new do |subhash, subkey|
subhash[subkey] = Array.new
end
end
end
def add(target, key_path, &block)
return if target.nil? || key_path.nil? || block.nil?
block.weak! if BubbleWrap.use_weak_callbacks?
callbacks[target][key_path.to_s] << block
end
def remove(target, key_path)
return if target.nil? || key_path.nil?
key_path = key_path.to_s
callbacks[target].delete(key_path)
# If there no key_paths left for target, remove the target key
if callbacks[target].empty?
callbacks.delete(target)
end
end
def registered?(target, key_path)
callbacks[target].has_key? key_path.to_s
end
def remove_all
callbacks.clear
end
def each_key_path
callbacks.each do |target, key_paths|
key_paths.each_key do |key_path|
yield target, key_path
end
end
end
private
def observeValueForKeyPath(key_path, ofObject: target, change: change, context: context)
key_paths = callbacks[target] || {}
blocks = key_paths[key_path] || []
args = change.values_at(*keys)
args << key_path
blocks.each do |block|
block.call(*args)
end
end
end
DEFAULT_OPTIONS = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
IMMIDEATE_OPTIONS = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial
def observe(target = self, key_paths, &block)
key_paths = [key_paths].flatten
key_paths.each do |key_path|
if not observers_registry.registered?(target, key_path)
target.addObserver(observers_registry, forKeyPath:key_path, options:DEFAULT_OPTIONS, context:nil)
end
# Add block even if observer is registed, so multiplie blocks can be invoked.
observers_registry.add(target, key_path, &block)
end
end
def observe!(target = self, key_paths, &block)
key_paths = [key_paths].flatten
key_paths.each do |key_path|
registered = immediate_observers_registry.registered?(target, key_path)
immediate_observers_registry.add(target, key_path, &block)
# We need to first register the block, and then call addObserver, because
# observeValueForKeyPath will fire immedeately.
if not registered
target.addObserver(immediate_observers_registry, forKeyPath:key_path, options: IMMIDEATE_OPTIONS, context:nil)
end
end
end
def unobserve(target = self, key_paths)
remove_from_registry_if_exists(target, key_paths, observers_registry)
end
def unobserve!(target = self, key_paths)
remove_from_registry_if_exists(target, key_paths, immediate_observers_registry)
end
def unobserve_all
observers_registry.each_key_path do |target, key_path|
target.removeObserver(observers_registry, forKeyPath:key_path)
end
immediate_observers_registry.each_key_path do |target, key_path|
target.removeObserver(immediate_observers_registry, forKeyPath:key_path)
end
observers_registry.remove_all
immediate_observers_registry.remove_all
end
private
def observers_registry
@observers_registry ||= Registry.new
end
def immediate_observers_registry
@immediate_observers_registry ||= Registry.new([:new])
end
def remove_from_registry_if_exists(target, key_paths, registry)
key_paths = [key_paths].flatten
key_paths.each do |key_path|
if registry.registered?(target, key_path)
target.removeObserver(registry, forKeyPath:key_path)
registry.remove(target, key_path)
end
end
end
end
end