class MotionKit::TreeLayout
A sensible parent class for any Tree-like layout class. Platform agnostic. Any platform-specific tasks are offloaded to child elements (add_child, remove_child). You could use a TreeLayout
subclass to construct a hierarchy representing a family tree, for instance. But that would be a silly use of MotionKit
.
Public Class Methods
MotionKit::BaseLayout::new
# File lib/motion-kit/helpers/tree_layout.rb, line 60 def initialize(args={}) super @child_layouts = [] @reapply_blocks = [] @elements = {} end
This is an `attr_reader`-like method that also calls `build_view` if the @view doesn't exist, and so you can use it to refer to views that are assigned to ivars in your `layout` method.
@example
class MyLayout < MK::Layout view :label view :login_button def layout # if element id and attr name match, no need to assign to ivar add UILabel, :label # if they don't match you must assign. If you are using # Key-Value observation you should use the setter: self.login_button = add UIButton, :button end end
You can also set multiple views in a single line.
@example
class MyLayout < MK::Layout view :label, :login_button end
# File lib/motion-kit/helpers/tree_layout.rb, line 38 def view(*names) names.each do |name| ivar_name = "@#{name}" define_method(name) do unless instance_variable_get(ivar_name) view = self.get_view(name) unless view build_view unless @view view = instance_variable_get(ivar_name) || self.get_view(name) end self.send("#{name}=", view) return view end return instance_variable_get(ivar_name) end # KVO compliance attr_writer name end end
Public Instance Methods
Instantiates a view via `create` and adds the view to the current target. If there is no context, a default root view can be created if that has been enabled (e.g. within the `layout` method). The block is run in the context of the new view.
# File lib/motion-kit/helpers/tree_layout.rb, line 218 def add(element, element_id=nil, options={}, &block) # make sure we have a target - raises NoContextError if none exists self.target unless @context create_default_root_context end # We want to be sure that the element has a supeview or superlayer before # the style method is called. element = initialize_element(element, element_id) self.apply(:add_child, element, options) style_and_context(element, element_id, &block) element end
Returns all the elements with a given element_id
# File lib/motion-kit/helpers/tree_layout.rb, line 281 def all(element_id) unless is_parent_layout? return parent_layout.all(element_id) end @elements[element_id] || [] end
Just like `all`, but if `all` returns a Layout
, this method returns the layout's view.
# File lib/motion-kit/helpers/tree_layout.rb, line 290 def all_views(element_id) element = all(element_id) if element.is_a?(Layout) element = element.view end element end
# File lib/motion-kit/helpers/tree_layout.rb, line 187 def always(&block) raise ArgumentError.new('Block required') unless block if initial? yield end reapply(&block) return self end
Builds the layout and then returns self for chaining.
# File lib/motion-kit/helpers/tree_layout.rb, line 76 def build view self end
Checks if the layout has been built yet or not.
# File lib/motion-kit/helpers/tree_layout.rb, line 82 def built? !@view.nil? end
# File lib/motion-kit/helpers/tree_layout.rb, line 235 def child_layouts @child_layouts end
instantiates a view, possibly running a 'layout block' to add child views.
# File lib/motion-kit/helpers/tree_layout.rb, line 129 def create(element, element_id=nil, &block) element = initialize_element(element, element_id) style_and_context(element, element_id, &block) element end
# File lib/motion-kit/helpers/tree_layout.rb, line 489 def create_default_root_context if @assign_root # Originally I thought default_root should be `apply`ied like other # view-related methods, but actually this method *only* gets called # from within the `layout` method, and so should already be inside the # correct Layout subclass. @context = root(preset_root || default_root) else raise NoContextError.new("No top level view specified (missing outer 'create' method?)") end end
# File lib/motion-kit/helpers/tree_layout.rb, line 248 def first(element_id) ; get(element_id) ; end
Removes a view from the list of elements this layout is “tracking”, but leaves it in the view hierarchy. Returns the views that were removed.
# File lib/motion-kit/helpers/tree_layout.rb, line 450 def forget(element_id) unless is_parent_layout? return parent_layout.remove(element_id) end removed = nil context(self.view) do removed = all(element_id) @elements[element_id] = nil end removed end
returns the root view that was removed, if any
# File lib/motion-kit/helpers/tree_layout.rb, line 475 def forget_tree(element_id, view) removed = forget_view(element_id, view) if view.subviews view.subviews.each do | sub | if (sub_ids = sub.motion_kit_meta[:motion_kit_ids]) sub_ids.each do | sub_id | forget_tree(sub_id, sub) || [] end end end end removed end
# File lib/motion-kit/helpers/tree_layout.rb, line 462 def forget_view(element_id, view) unless is_parent_layout? return parent_layout.remove_view(element_id, view) end # mp "forgetting #{element_id}, #{view}" removed = nil context(self.view) do removed = @elements[element_id].delete(view) if @elements[element_id] end removed end
Retrieves a view by its element id. This will return the first view with this element_id in the tree, where first means the first object that was added with that name.
# File lib/motion-kit/helpers/tree_layout.rb, line 242 def get(element_id) unless is_parent_layout? return parent_layout.get(element_id) end @elements[element_id] && @elements[element_id].first end
Just like `get`, but if `get` returns a Layout
, this method returns the layout's view.
# File lib/motion-kit/helpers/tree_layout.rb, line 252 def get_view(element_id) element = get(element_id) if element.is_a?(Layout) element = element.view end element end
# File lib/motion-kit/helpers/tree_layout.rb, line 198 def initial(&block) raise ArgumentError.new('Block required') unless block puts('the `initial` method is no longer necessary! all code that *isn\'t in a `reapply` block is now only applied during initial setup.') if initial? yield end return self end
# File lib/motion-kit/helpers/tree_layout.rb, line 183 def initial? @layout_state == :initial end
Retrieves a view by its element id. This will return the last view with this element_id in the tree, where last means the last object that was added with that name.
# File lib/motion-kit/helpers/tree_layout.rb, line 263 def last(element_id) unless is_parent_layout? return parent_layout.last(element_id) end @elements[element_id] && @elements[element_id].last end
Just like `last`, but if `last` returns a Layout
, this method returns the layout's view.
# File lib/motion-kit/helpers/tree_layout.rb, line 272 def last_view(element_id) element = last(element_id) if element.is_a?(Layout) element = element.view end element end
# File lib/motion-kit/helpers/tree_layout.rb, line 208 def name_element(element, element_id) element.motion_kit_id = element_id @elements[element_id] ||= [] @elements[element_id] << element end
This searches for the “nearest” view with a given id. First, all child views are checked. Then the search goes up to the parent view, and its child views are checked. This means any view that is in the parent view's hierarchy is considered closer than a view in a grandparent's hierarchy. This is a “depth-first” search, so any subview that contains a view with the element id
A–B–C–D* Starting at D, E is closer than F, because D&E are siblings.
\ \ \-E But F, G and H are closer than A or I, because they share a \ \-F--G closer *parent* (B). The logic is, "B" is a container, and \-I \-H all views in that container are in a closer family.
# File lib/motion-kit/helpers/tree_layout.rb, line 395 def nearest(element_id) nearest(element_id, from: target) end
Search for a sibling: the next sibling that has the given id
# File lib/motion-kit/helpers/tree_layout.rb, line 315 def next(element_id) self.next(element_id, from: target) end
Returns the ‘N’th element with a given element_id, where “‘N’th” is passed in as `index`
# File lib/motion-kit/helpers/tree_layout.rb, line 300 def nth(element_id, index) self.all(element_id)[index] end
Just like `nth`, but if `nth` returns a Layout
, this method returns the layout's view.
# File lib/motion-kit/helpers/tree_layout.rb, line 306 def nth_view(element_id, index) element = nth(element_id) if element.is_a?(Layout) element = element.view end element end
Search for a sibling: the previous sibling that has the given id
# File lib/motion-kit/helpers/tree_layout.rb, line 350 def prev(element_id) prev(element_id, from: target) end
Blocks passed to `reapply` are only run when `reapply!` is called.
# File lib/motion-kit/helpers/tree_layout.rb, line 163 def reapply(&block) raise ArgumentError.new('Block required') unless block raise InvalidDeferredError.new('reapply must be run inside of a context') unless @context if reapply? yield end block = block.weak! parent_layout.reapply_blocks << [@context, block] return self end
Calls the style method of all objects in the view hierarchy that are part of this layout. The views in a child layout are not styled, but those layouts will receive a `reapply!` message if no root is specified.
# File lib/motion-kit/helpers/tree_layout.rb, line 139 def reapply! root ||= self.view @layout_state = :reapply run_reapply_blocks @child_layouts.each do |child_layout| child_layout.reapply! end @layout_state = :initial return self end
# File lib/motion-kit/helpers/tree_layout.rb, line 153 def reapply? @layout_state == :reapply end
Only intended for private use
# File lib/motion-kit/helpers/tree_layout.rb, line 158 def reapply_blocks @reapply_blocks ||= [] end
Removes a view (or several with the same name) from the hierarchy and forgets it entirely. Returns the views that were removed.
# File lib/motion-kit/helpers/tree_layout.rb, line 422 def remove(element_id) unless is_parent_layout? return parent_layout.remove(element_id) end removed = forget(element_id) context(self.view) do removed.each do |element| self.apply(:remove_child, element) end end removed end
# File lib/motion-kit/helpers/tree_layout.rb, line 435 def remove_view(element_id, view) unless is_parent_layout? return parent_layout.remove_view(element_id, view) end removed = forget_tree(element_id, view) if removed context(self.view) do self.apply(:remove_child, removed) end end removed end
Assign a view to act as the 'root' view for this layout. This method can only be called once, and must be called before `add` is called for the first time (otherwise `add` will create a default root view). This method must be called from inside `layout`, otherwise you should just use `create`.
You can also call this method with just an element_id, and the default root view will be created.
# File lib/motion-kit/helpers/tree_layout.rb, line 95 def root(element, element_id=nil, &block) if @view raise ContextConflictError.new("Already created the root view") end unless @assign_root raise InvalidRootError.new("You should only create a 'root' view from inside the 'layout' method (use 'create' elsewhere)") end @assign_root = false # this method can be called with just a symbol, to assign the root element_id if element.is_a?(Symbol) element_id = element # See note below about why we don't need to `apply(:default_root)` element = preset_root || default_root elsif preset_root && preset_root != element # You're trying to make two roots, one at initialization # and one in your layout itself. raise ContextConflictError.new("Already created the root view") end @view = initialize_element(element, element_id) if block if @context raise ContextConflictError.new("Already in a context") end end style_and_context(@view, element_id, &block) return @view end
Only intended for private use
# File lib/motion-kit/helpers/tree_layout.rb, line 177 def run_reapply_blocks self.reapply_blocks.each do |target, block| context(target, &block) end end
The main view. This method builds the layout and returns the root view.
# File lib/motion-kit/helpers/tree_layout.rb, line 68 def view unless is_parent_layout? return parent_layout.view end @view ||= build_view end
Protected Instance Methods
This method builds the layout and returns the root view.
# File lib/motion-kit/helpers/tree_layout.rb, line 504 def build_view # Only in the 'layout' method will we allow default container to be # created automatically (when 'add' is called) @assign_root = true prev_should_run = @should_run_deferred @should_run_deferred = true layout unless @view if @assign_root create_default_root_context @view = @context else NSLog('Warning! No root view was set in TreeLayout#layout. Did you mean to call `root`?') end end run_deferred(@view) @should_run_deferred = prev_should_run @assign_root = false # context can be set via the 'create_default_root_context' method, which # may be outside a 'context' block, so make sure to restore context to # it's previous value @context = nil if @preset_root @view = WeakRef.new(@view) @preset_root = nil end @view end
Initializes an instance of a view. This will need to be smarter going forward as `new` isn't always the designated initializer.
Accepts a view instance, a class (which is instantiated with 'new') or a `ViewLayout`, which returns the root view.
# File lib/motion-kit/helpers/tree_layout.rb, line 543 def initialize_element(elem, element_id) if elem.is_a?(Class) && elem < TreeLayout layout = elem.new elem = layout.view elsif elem.is_a?(Class) elem = elem.new elsif elem.is_a?(TreeLayout) layout = elem elem = elem.view end if layout if element_id name_element(layout, element_id) end @child_layouts << layout elsif element_id name_element(elem, element_id) end return elem end
# File lib/motion-kit/helpers/tree_layout.rb, line 535 def layout end
Calls the `_style` method with the element as the context, and runs the optional block in that context. This is usually done immediately after `initialize_element`, except in the case of `add`, which adds the item to the tree before styling it.
# File lib/motion-kit/helpers/tree_layout.rb, line 570 def style_and_context(element, element_id, &block) style_method = "#{element_id}_style" if parent_layout.respond_to?(style_method) || block_given? parent_layout.context(element) do if parent_layout.respond_to?(style_method) parent_layout.send(style_method) end if block_given? yield end end end end