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?, #iphone47?, #iphone4?, #iphone55?, #iphone?, #is_parent_layout?, #method_missing, #objc_version, #orientation?, #orientation_block, #parent_layout, #retina?, #ruby_version, #run_deferred, #set_parent_layout, #target, #tv?, #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
65
# File 'lib/motion-kit/helpers/tree_layout.rb', line 60

def initialize(args={})
  super
  @child_layouts = []
  @reapply_blocks = []
  @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, options = {}, &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.



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# 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) ⇒ Object

Returns all the elements with a given element_id



281
282
283
284
285
286
# 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) ⇒ Object

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



290
291
292
293
294
295
296
# 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(&block) ⇒ Object

Raises:

  • (ArgumentError)


187
188
189
190
191
192
193
194
195
196
# 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

#buildObject

Builds the layout and then returns self for chaining.



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

def build
  view
  self
end

#built?Boolean Also known as: build?

Checks if the layout has been built yet or not.

Returns:

  • (Boolean)


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

def built?
  !@view.nil?
end

#child_layoutsObject



235
236
237
# File 'lib/motion-kit/helpers/tree_layout.rb', line 235

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.



129
130
131
132
133
134
# 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_contextObject



489
490
491
492
493
494
495
496
497
498
499
# 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) ⇒ Object



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

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.



450
451
452
453
454
455
456
457
458
459
460
# 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) ⇒ Object

returns the root view that was removed, if any



475
476
477
478
479
480
481
482
483
484
485
486
487
# 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) ⇒ Object



462
463
464
465
466
467
468
469
470
471
472
# 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) ⇒ 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.



242
243
244
245
246
247
# 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) ⇒ Object

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



252
253
254
255
256
257
258
# 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(&block) ⇒ Object

Raises:

  • (ArgumentError)


198
199
200
201
202
203
204
205
206
# 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?Boolean

Returns:

  • (Boolean)


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

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.



263
264
265
266
267
268
# 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) ⇒ Object

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



272
273
274
275
276
277
278
# 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) ⇒ Object



208
209
210
211
212
# 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, 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.


395
396
397
# File 'lib/motion-kit/helpers/tree_layout.rb', line 395

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



315
316
317
# 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) ⇒ Object

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



300
301
302
# 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) ⇒ Object

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



306
307
308
309
310
311
312
# 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, from: from_view) ⇒ Object

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



350
351
352
# File 'lib/motion-kit/helpers/tree_layout.rb', line 350

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

#reapply(&block) ⇒ Object

Blocks passed to ‘reapply` are only run when `reapply!` is called.

Raises:

  • (ArgumentError)


163
164
165
166
167
168
169
170
171
172
173
174
# 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!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.



139
140
141
142
143
144
145
146
147
148
149
150
151
# 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?Boolean

Returns:

  • (Boolean)


153
154
155
# File 'lib/motion-kit/helpers/tree_layout.rb', line 153

def reapply?
  @layout_state == :reapply
end

#reapply_blocksObject

Only intended for private use



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

def reapply_blocks
  @reapply_blocks ||= []
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.



422
423
424
425
426
427
428
429
430
431
432
433
# 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) ⇒ Object



435
436
437
438
439
440
441
442
443
444
445
446
# 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) ⇒ 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.



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
126
# 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_blocksObject

Only intended for private use



177
178
179
180
181
# 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

#viewObject

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



68
69
70
71
72
73
# 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