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)