lib/motion/observation.rb in rm-extensions-0.0.9 vs lib/motion/observation.rb in rm-extensions-0.1.0
- old
+ new
@@ -2,95 +2,179 @@
module ObjectExtensions
module Observation
+ def rmext_observation_proxy
+ @rmext_observation_proxy ||= ObservationProxy.new(self.inspect)
+ end
+
# observe an object.key. takes a block that will be called with the
# new value upon change.
#
# rmext_observe_passive(@model, "name") do |val|
# p "name is #{val}"
# end
def rmext_observe_passive(object, key, &block)
- wop = ::RMExtensions::WeakObserverProxy.get(self)
- b = -> (old_value, new_value) do
- block.call(new_value) unless block.nil?
- end
- wop.observe(object, key, &b)
+ rmext_observation_proxy.observe(object, key, &block)
end
# like +rmext_observe_passive+ but additionally fires the callback immediately.
def rmext_observe(object, key, &block)
- # p "+ rmext_observe", self, object, key
rmext_observe_passive(object, key, &block)
block.call(object.send(key)) unless block.nil?
end
# unobserve an existing observation
def rmext_unobserve(object, key)
- wop = ::RMExtensions::WeakObserverProxy.get(self)
- wop.unobserve(object, key)
- wop.clear_empty_targets!
+ if @rmext_observation_proxy
+ @rmext_observation_proxy.unobserve(object, key)
+ end
end
# unobserve all existing observations
def rmext_unobserve_all
- wop = ::RMExtensions::WeakObserverProxy.get(self)
- wop.unobserve_all
+ if @rmext_observation_proxy
+ @rmext_observation_proxy.unobserve_all
+ end
end
+ # register a callback when an event is trigger on this object
+ def rmext_on(event, &block)
+ rmext_observation_proxy.on(event, &block)
+ end
+
+ # remove a specific callback for an event on this object
+ def rmext_off(event, &block)
+ if @rmext_observation_proxy
+ @rmext_observation_proxy.off(event, &block)
+ end
+ end
+
+ # remove all event callbacks on this object
+ def rmext_off_all
+ if @rmext_observation_proxy
+ @rmext_observation_proxy.off_all
+ end
+ end
+
+ # trigger an event with args on this object
+ def rmext_trigger(event, *args)
+ if @rmext_observation_proxy
+ @rmext_observation_proxy.trigger(event, *args)
+ end
+ end
+
end
end
- # Proxy class used to hold the actual observation and watches for the real
- # class intended to hold the observation to be deallocated, so the
- # observation can be cleaned up.
- class WeakObserverProxy
- include BW::KVO
- rmext_weak_attr_accessor :obj
- attr_accessor :strong_object_id, :strong_class_name
- def initialize(strong_object)
- self.obj = strong_object
- self.strong_object_id = strong_object.object_id
- self.strong_class_name = strong_object.class.name
- self.class.weak_observer_map[strong_object_id] = self
- strong_object.rmext_on_dealloc(&kill_observation_proc)
+ # # Proxy class used to hold the actual observation and watches for the real
+ # # class intended to hold the observation to be deallocated, so the
+ # # observation can be cleaned up.
+ class ObservationProxy
+ COLLECTION_OPERATIONS = [ NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, NSKeyValueChangeReplacement ]
+ DEFAULT_OPTIONS = NSKeyValueObservingOptionNew
+
+ def initialize(desc)
+ @desc = desc
+ @events = {}
+ @targets = {}
+ # p "created #{self.inspect} for #{@desc}"
end
- # isolate this in its own method so it wont create a retain cycle
- def kill_observation_proc
- proc { |x|
- # uncomment to verify deallocation is working. if not, there is probably
- # a retain cycle somewhere in your code.
- # p "kill_observation_proc", self
- self.obj = nil
- unobserve_all
- self.class.weak_observer_map.delete(strong_object_id)
- }
+
+ # clean up on dellocation
+ def dealloc
+ # p "dealloc #{self.inspect} for #{@desc}"
+ unobserve_all
+ off_all
+ super
end
- # get rid of targets that dont contain anything to avoid retain cycles.
- def clear_empty_targets!
- return if @targets.nil?
- @targets.each_pair do |target, key_paths|
- if !key_paths || key_paths.size == 0
- @targets.delete(target)
+
+ def observe(target, key_path, &block)
+ target.addObserver(self, forKeyPath:key_path, options:DEFAULT_OPTIONS, context:nil) unless registered?(target, key_path)
+ add_observer_block(target, key_path, &block)
+ end
+
+ def unobserve(target, key_path)
+ return unless registered?(target, key_path)
+ target.removeObserver(self, forKeyPath:key_path)
+ remove_observer_block(target, key_path)
+ end
+
+ def remove_observer_block(target, key_path)
+ return if target.nil? || key_path.nil?
+
+ key_paths = @targets[target]
+ if !key_paths.nil? && key_paths.has_key?(key_path.to_s)
+ key_paths.delete(key_path.to_s)
+ end
+ end
+
+ def unobserve_all
+ keys = @targets.keys.clone
+ while keys.size > 0
+ target = keys.pop
+ target_hash = @targets[target]
+ paths = target_hash.keys.clone
+ while paths.size > 0
+ key_path = paths.pop
+ target.removeObserver(self, forKeyPath:key_path)
end
end
- nil
+ @targets.clear
end
- def inspect
- "#{strong_class_name}:#{strong_object_id}"
+
+ def registered?(target, key_path)
+ !target.nil? && !@targets[target].nil? && @targets[target].has_key?(key_path.to_s)
end
- def targets
- @targets
+
+ def add_observer_block(target, key_path, &block)
+ return if target.nil? || key_path.nil? || block.nil?
+ @targets[target] ||= {}
+ @targets[target][key_path.to_s] ||= []
+ @targets[target][key_path.to_s] << block
end
- def self.weak_observer_map
- Dispatch.once { @weak_observer_map = {} }
- @weak_observer_map
+
+ # NSKeyValueObserving Protocol
+
+ def observeValueForKeyPath(key_path, ofObject:target, change:change, context:context)
+ return if target.nil?
+ key_paths = @targets[target] || {}
+ blocks = key_paths[key_path] || []
+ blocks.each do |block|
+ args = [ change[NSKeyValueChangeNewKey] ]
+ args << change[NSKeyValueChangeIndexesKey] if collection?(change)
+ block.call(*args)
+ end
end
- def self.get(obj)
- return obj if obj.is_a?(WeakObserverProxy)
- weak_observer_map[obj.object_id] || new(obj)
+
+ def collection?(change)
+ COLLECTION_OPERATIONS.include?(change[NSKeyValueChangeKindKey])
+ end
+
+ def on(event, &block)
+ return if event.nil? || block.nil?
+ @events[event.to_s] ||= []
+ @events[event.to_s] << block
+ end
+
+ def off(event, &block)
+ return if event.nil? || block.nil? || !@events.key?(event.to_s)
+ @events[event.to_s].delete_if { |b| b == block }
+ nil
+ end
+
+ def off_all
+ @events.clear
+ end
+
+ def trigger(event, *args)
+ return if event.nil? || !@events.key?(event.to_s)
+ @events[event.to_s].each do |block|
+ block.call(*args)
+ end
+ nil
end
end
end
Object.send(:include, ::RMExtensions::ObjectExtensions::Observation)