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

new(args={}) click to toggle source
Calls superclass method MotionKit::BaseLayout::new
# File lib/motion-kit/helpers/tree_layout.rb, line 60
def initialize(args={})
  super
  @child_layouts = []
  @reapply_blocks = []
  @elements = {}
end
view(*names) click to toggle source

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

add(element, element_id=nil, options={}, &block) click to toggle source

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
all(element_id) click to toggle source

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
all_views(element_id) click to toggle source

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
always() { || ... } click to toggle source
# 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
build() click to toggle source

Builds the layout and then returns self for chaining.

# File lib/motion-kit/helpers/tree_layout.rb, line 76
def build
  view
  self
end
build?()
Alias for: built?
built?() click to toggle source

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
Also aliased as: build?
child_layouts() click to toggle source
# File lib/motion-kit/helpers/tree_layout.rb, line 235
def child_layouts
  @child_layouts
end
create(element, element_id=nil, &block) click to toggle source

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
create_default_root_context() click to toggle source
# 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
first(element_id) click to toggle source
# File lib/motion-kit/helpers/tree_layout.rb, line 248
def first(element_id) ; get(element_id) ; end
forget(element_id) click to toggle source

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
forget_tree(element_id, view) click to toggle source

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
forget_view(element_id, view) click to toggle source
# 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
get(element_id) click to toggle source

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
get_view(element_id) click to toggle source

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
initial() { || ... } click to toggle source
# 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
initial?() click to toggle source
# File lib/motion-kit/helpers/tree_layout.rb, line 183
def initial?
  @layout_state == :initial
end
last(element_id) click to toggle source

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
last_view(element_id) click to toggle source

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
name_element(element, element_id) click to toggle source
# 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
nearest(element_id) click to toggle source

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
next(element_id) click to toggle source

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
nth(element_id, index) click to toggle source

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
nth_view(element_id, index) click to toggle source

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
prev(element_id) click to toggle source

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
reapply() { || ... } click to toggle source

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
reapply!() click to toggle source

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
reapply?() click to toggle source
# File lib/motion-kit/helpers/tree_layout.rb, line 153
def reapply?
  @layout_state == :reapply
end
reapply_blocks() click to toggle source

Only intended for private use

# File lib/motion-kit/helpers/tree_layout.rb, line 158
def reapply_blocks
  @reapply_blocks ||= []
end
remove(element_id) click to toggle source

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
remove_view(element_id, view) click to toggle source
# 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
root(element, element_id=nil, &block) click to toggle source

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
run_reapply_blocks() click to toggle source

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
view() click to toggle source

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

build_view() click to toggle source

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
initialize_element(elem, element_id) click to toggle source

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
layout() click to toggle source
# File lib/motion-kit/helpers/tree_layout.rb, line 535
def layout
end
style_and_context(element, element_id) { || ... } click to toggle source

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