Class: MotionKit::TreeLayout

Inherits:
BaseLayout show all
Defined in:
lib/motion-kit/helpers/tree_layout.rb

Overview

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.

Direct Known Subclasses

CALayerHelpers, Layout, MenuLayout, WindowLayout

Instance Attribute Summary

Attributes inherited from BaseLayout

#parent

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from BaseLayout

#add_deferred_block, #apply, #apply_with_context, #apply_with_target, #context, #deferred, #deferred_blocks, delegate_method_fix, #has_context?, #ipad?, #iphone35?, #iphone4?, #iphone?, #is_parent_layout?, #method_missing, #objc_version, #orientation?, #parent_layout, #retina?, #ruby_version, #run_deferred, #set_parent_layout, #target, #v

Methods included from BaseLayoutClassMethods

#layout_for, #memoize, #target_klasses, #targets

Constructor Details

#initialize(args = {}) ⇒ TreeLayout

Returns a new instance of TreeLayout.



60
61
62
63
64
# File 'lib/motion-kit/helpers/tree_layout.rb', line 60

def initialize(args={})
  super
  @child_layouts = []
  @elements = {}
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method in the class MotionKit::BaseLayout

Class Method Details

.view(*names) ⇒ Object

This is an ‘attr_reader`-like method that also calls `build_view` if the assigned to ivars in your `layout` method.

You can also set multiple views in a single line.

Examples:

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. = add UIButton, :button
  end

end
class MyLayout < MK::Layout
  view :label, :login_button
end


38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 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

Instance Method Details

#add(element, element_id = nil, &block) ⇒ Object

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.



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/motion-kit/helpers/tree_layout.rb', line 194

def add(element, element_id=nil, &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)
  style_and_context(element, element_id, &block)

  element
end

#all(element_id) ⇒ Object

Returns all the elements with a given element_id



257
258
259
260
261
262
# File 'lib/motion-kit/helpers/tree_layout.rb', line 257

def all(element_id)
  unless is_parent_layout?
    return parent_layout.all(element_id)
  end
  @elements[element_id] || []
end

#all_views(element_id) ⇒ Object

Just like ‘all`, but if `all` returns a Layout, this method returns the layout’s view.



266
267
268
269
270
271
272
# File 'lib/motion-kit/helpers/tree_layout.rb', line 266

def all_views(element_id)
  element = all(element_id)
  if element.is_a?(Layout)
    element = element.view
  end
  element
end

#buildObject

Builds the layout and then returns self for chaining.



75
76
77
78
# File 'lib/motion-kit/helpers/tree_layout.rb', line 75

def build
  view
  self
end

#built?Boolean Also known as: build?

Checks if the layout has been built yet or not.

Returns:

  • (Boolean)


81
82
83
# File 'lib/motion-kit/helpers/tree_layout.rb', line 81

def built?
  !@view.nil?
end

#child_layoutsObject



211
212
213
# File 'lib/motion-kit/helpers/tree_layout.rb', line 211

def child_layouts
  @child_layouts
end

#create(element, element_id = nil, &block) ⇒ Object

instantiates a view, possibly running a ‘layout block’ to add child views.



128
129
130
131
132
133
# File 'lib/motion-kit/helpers/tree_layout.rb', line 128

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_contextObject



425
426
427
428
429
430
431
432
433
434
435
# File 'lib/motion-kit/helpers/tree_layout.rb', line 425

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) ⇒ Object



224
# File 'lib/motion-kit/helpers/tree_layout.rb', line 224

def first(element_id) ; get(element_id) ; end

#forget(element_id) ⇒ Object

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.



413
414
415
416
417
418
419
420
421
422
423
# File 'lib/motion-kit/helpers/tree_layout.rb', line 413

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

#get(element_id) ⇒ Object

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.



218
219
220
221
222
223
# File 'lib/motion-kit/helpers/tree_layout.rb', line 218

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) ⇒ Object

Just like ‘get`, but if `get` returns a Layout, this method returns the layout’s view.



228
229
230
231
232
233
234
# File 'lib/motion-kit/helpers/tree_layout.rb', line 228

def get_view(element_id)
  element = get(element_id)
  if element.is_a?(Layout)
    element = element.view
  end
  element
end

#initial(&block) ⇒ Object

Raises:

  • (ArgumentError)


175
176
177
178
179
180
181
182
# File 'lib/motion-kit/helpers/tree_layout.rb', line 175

def initial(&block)
  raise ArgumentError.new('Block required') unless block

  if initial?
    yield
  end
  return self
end

#initial?Boolean

Returns:

  • (Boolean)


171
172
173
# File 'lib/motion-kit/helpers/tree_layout.rb', line 171

def initial?
  @layout_state == :initial
end

#last(element_id) ⇒ Object

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.



239
240
241
242
243
244
# File 'lib/motion-kit/helpers/tree_layout.rb', line 239

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) ⇒ Object

Just like ‘last`, but if `last` returns a Layout, this method returns the layout’s view.



248
249
250
251
252
253
254
# File 'lib/motion-kit/helpers/tree_layout.rb', line 248

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) ⇒ Object



184
185
186
187
188
# File 'lib/motion-kit/helpers/tree_layout.rb', line 184

def name_element(element, element_id)
  element.motion_kit_id = element_id
  @elements[element_id] ||= []
  @elements[element_id] << element
end

#nearest(element_id, from: from_view) ⇒ Object

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.


371
372
373
# File 'lib/motion-kit/helpers/tree_layout.rb', line 371

def nearest(element_id)
  nearest(element_id, from: target)
end

#next(element_id, from: from_view) ⇒ Object

Search for a sibling: the next sibling that has the given id



291
292
293
# File 'lib/motion-kit/helpers/tree_layout.rb', line 291

def next(element_id)
  self.next(element_id, from: target)
end

#nth(element_id, index) ⇒ Object

Returns the ‘N’th element with a given element_id, where “‘N’th” is passed in as ‘index`



276
277
278
# File 'lib/motion-kit/helpers/tree_layout.rb', line 276

def nth(element_id, index)
  self.all(element_id)[index]
end

#nth_view(element_id, index) ⇒ Object

Just like ‘nth`, but if `nth` returns a Layout, this method returns the layout’s view.



282
283
284
285
286
287
288
# File 'lib/motion-kit/helpers/tree_layout.rb', line 282

def nth_view(element_id, index)
  element = nth(element_id)
  if element.is_a?(Layout)
    element = element.view
  end
  element
end

#prev(element_id, from: from_view) ⇒ Object

Search for a sibling: the previous sibling that has the given id



326
327
328
# File 'lib/motion-kit/helpers/tree_layout.rb', line 326

def prev(element_id)
  prev(element_id, from: target)
end

#reapply(&block) ⇒ Object

Calls the style method of all objects in the view hierarchy

Raises:

  • (ArgumentError)


162
163
164
165
166
167
168
169
# File 'lib/motion-kit/helpers/tree_layout.rb', line 162

def reapply(&block)
  raise ArgumentError.new('Block required') unless block

  if reapply?
    yield
  end
  return self
end

#reapply!Object

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.



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/motion-kit/helpers/tree_layout.rb', line 138

def reapply!
  root ||= self.view
  @layout_state = :reapply

  @elements.each do |element_id, elements|
    elements.each do |element|
      style_and_context(element, element_id)
    end
  end

  @child_layouts.each do |child_layout|
    child_layout.reapply!
  end

  @layout_state = :initial

  return self
end

#reapply?Boolean

Returns:

  • (Boolean)


157
158
159
# File 'lib/motion-kit/helpers/tree_layout.rb', line 157

def reapply?
  @layout_state == :reapply
end

#remove(element_id) ⇒ Object

Removes a view (or several with the same name) from the hierarchy and forgets it entirely. Returns the views that were removed.



398
399
400
401
402
403
404
405
406
407
408
409
# File 'lib/motion-kit/helpers/tree_layout.rb', line 398

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

#root(element, element_id = nil, &block) ⇒ Object

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.



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/motion-kit/helpers/tree_layout.rb', line 94

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

#viewObject

The main view. This method builds the layout and returns the root view.



67
68
69
70
71
72
# File 'lib/motion-kit/helpers/tree_layout.rb', line 67

def view
  unless is_parent_layout?
    return parent_layout.view
  end
  @view ||= build_view
end