Class: RMX::Layout

Inherits:
Object
  • Object
show all
Defined in:
lib/motion/layout.rb

Constant Summary collapse

ATTRIBUTE_LOOKUP =
{
  "left" => NSLayoutAttributeLeft,
  "right" => NSLayoutAttributeRight,
  "top" => NSLayoutAttributeTop,
  "bottom" => NSLayoutAttributeBottom,
  "leading" => NSLayoutAttributeLeading,
  "trailing" => NSLayoutAttributeTrailing,
  "width" => NSLayoutAttributeWidth,
  "height" => NSLayoutAttributeHeight,
  "centerX" => NSLayoutAttributeCenterX,
  "centerY" => NSLayoutAttributeCenterY,
  "baseline" => NSLayoutAttributeBaseline,
  nil => NSLayoutAttributeNotAnAttribute
}
ATTRIBUTE_LOOKUP_INVERSE =
ATTRIBUTE_LOOKUP.invert
{
  "<=" => NSLayoutRelationLessThanOrEqual,
  "==" => NSLayoutRelationEqual,
  ">=" => NSLayoutRelationGreaterThanOrEqual
}
RELATED_BY_LOOKUP.invert
PRIORITY_LOOKUP =
{
  "max" => UILayoutPriorityRequired, # = 1000
  "required" => UILayoutPriorityRequired, # = 1000
  "high" => UILayoutPriorityDefaultHigh, # = 750
  "med" => 500,
  "low" => UILayoutPriorityDefaultLow, # = 250
  "fit" => UILayoutPriorityFittingSizeLevel # = 50
}
PRIORITY_LOOKUP_INVERSE =
PRIORITY_LOOKUP.invert
AXIS_LOOKUP =
{
  "h" => UILayoutConstraintAxisHorizontal,
  "v" => UILayoutConstraintAxisVertical
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(&block) ⇒ Layout

Example: RMX::Layout.new do |layout|

...

end



54
55
56
57
58
59
60
61
62
# File 'lib/motion/layout.rb', line 54

def initialize(&block)
  @block_owner = block.owner if block
  @visible_items = []
  @constraints = {}
  unless block.nil?
    block.call(self)
    block = nil
  end
end

Instance Attribute Details

#visible_itemsObject

keeps track of views that are not #hidden? as constraints are built, so the special ‘last_visible` view name can be used in equations. exposed for advanced layout needs.



48
49
50
# File 'lib/motion/layout.rb', line 48

def visible_items
  @visible_items
end

Class Method Details

.describe_constraint(subviews, constraint) ⇒ Object

transforms an NSLayoutConstraint into a string. this string is for debugging and produces a verbose translation. its not meant to be copied directly as an equation. pass the subviews hash just as you would to Layout#subviews=, followed by the NSLayoutConstraint



320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/motion/layout.rb', line 320

def self.describe_constraint(subviews, constraint)
  subviews_inverse = subviews.invert
  item = subviews_inverse[constraint.firstItem] || "view"
  item_attribute = ATTRIBUTE_LOOKUP_INVERSE[constraint.firstAttribute]
  related_by = RELATED_BY_LOOKUP_INVERSE[constraint.relation]
  to_item = subviews_inverse[constraint.secondItem] || "view"
  to_item_attribute = ATTRIBUTE_LOOKUP_INVERSE[constraint.secondAttribute]
  multiplier = constraint.multiplier
  constant = constraint.constant
  priority = PRIORITY_LOOKUP_INVERSE[constraint.priority] || constraint.priority
  "#{constraint.description}\n#{item}.#{item_attribute} #{related_by} #{to_item}.#{to_item_attribute} * #{multiplier} + #{constant} @ #{priority}"
end

.describe_view(subviews, view) ⇒ Object

transforms a view’s NSLayoutConstraints into strings. pass the subviews hash just as you would to Layout#subviews=, followed by the view to describe



336
337
338
339
340
# File 'lib/motion/layout.rb', line 336

def self.describe_view(subviews, view)
  view.constraints.map do |constraint|
    describe_constraint(subviews, constraint)
  end.join("\n")
end

Instance Method Details

#clear!Object



79
80
81
# File 'lib/motion/layout.rb', line 79

def clear!
  @view.removeConstraints(@view.constraints)
end

#describe_constraint(constraint) ⇒ Object



309
310
311
# File 'lib/motion/layout.rb', line 309

def describe_constraint(constraint)
  self.class.describe_constraint(@subviews, constraint)
end

#describe_viewObject



313
314
315
# File 'lib/motion/layout.rb', line 313

def describe_view
  self.class.describe_view(@subviews, @view)
end

#eq(str, remove = false) ⇒ Object

Constraints are of the form “view1.attr1 <relation> view2.attr2 * multiplier + constant @ priority” processes one equation string



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/motion/layout.rb', line 123

def eq(str, remove=false)
  parts = str.split("#", 2).first.split(" ").select { |x| !x.empty? }
  return if parts.empty?

  item = nil
  item_attribute = nil
  related_by = nil
  to_item = nil
  to_item_attribute = nil
  multiplier = 1.0
  constant = 0.0
  
  debug = parts.delete("?")

  # first part should always be view1.attr1
  part = parts.shift
  item, item_attribute = part.split(".", 2)

  # second part should always be relation
  related_by = parts.shift

  # now things get more complicated

  # look for priority
  if idx = parts.index("@")
    priority = parts[idx + 1]
    parts.delete_at(idx)
    parts.delete_at(idx)
  end

  # look for negative or positive constant
  if idx = parts.index("-")
    constant = "-#{parts[idx + 1]}"
    parts.delete_at(idx)
    parts.delete_at(idx)
  elsif idx = parts.index("+")
    constant = parts[idx + 1]
    parts.delete_at(idx)
    parts.delete_at(idx)
  end

  # look for multiplier
  if idx = parts.index("*")
    multiplier = parts[idx + 1]
    parts.delete_at(idx)
    parts.delete_at(idx)
  end

  # now we need to_item, to_item_attribute

  if part = parts.shift
    # if part includes a . it could be either view2.attr2 or a float like 10.5
    l, r = part.split(".", 2)
    if !r || (r && r =~ /\d/)
      # assume a solo constant was on the right side
      constant = part
    else
      # assume view2.attr2
      to_item, to_item_attribute = l, r
    end
  end

  # if we dont have to_item and the item_attribute is something that requires a to_item, then
  # assume superview
  if !to_item
    unless item_attribute == "height" || item_attribute == "width"
      to_item = "view"
      to_item_attribute = item_attribute
    end
  end

  # normalize

  if item == "last_visible"
    item = @visible_items.first || "view"
  end

  res_item = view_for_item(item)
  res_constant = Float(PRIORITY_LOOKUP[constant] || constant)

  if res_item
    case item_attribute
    when "resistH"
      return res_item.setContentCompressionResistancePriority(res_constant, forAxis:UILayoutConstraintAxisHorizontal)
    when "resistV"
      return res_item.setContentCompressionResistancePriority(res_constant, forAxis:UILayoutConstraintAxisVertical)
    when "hugH"
      return res_item.setContentHuggingPriority(res_constant, forAxis:UILayoutConstraintAxisHorizontal)
    when "hugV"
      return res_item.setContentHuggingPriority(res_constant, forAxis:UILayoutConstraintAxisVertical)
    end
  end

  if to_item == "last_visible"
    to_item = @visible_items.detect { |x| x != item } || "view"
  end

  res_item_attribute = ATTRIBUTE_LOOKUP[item_attribute]
  res_related_by = RELATED_BY_LOOKUP[related_by]
  res_to_item = to_item ? view_for_item(to_item) : nil
  res_to_item_attribute = ATTRIBUTE_LOOKUP[to_item_attribute]
  res_multiplier = Float(multiplier)
  res_priority = priority ? Integer(PRIORITY_LOOKUP[priority] || priority) : nil

  errors = []
  errors.push("Invalid view1: #{item}") unless res_item
  errors.push("Invalid attr1: #{item_attribute}") unless res_item_attribute
  errors.push("Invalid relatedBy: #{related_by}") unless res_related_by
  errors.push("Invalid view2: #{to_item}") if to_item && !res_to_item
  errors.push("Invalid attr2: #{to_item_attribute}") unless res_to_item_attribute

  internal_ident = "#{item}.#{item_attribute} #{related_by} #{to_item}.#{to_item_attribute} * #{multiplier} @ #{priority}"

  if errors.size > 0 || debug
    p "======================== constraint debug ========================"
    p "given:"
    p "  #{str}"
    p "interpreted:"
    p "  item:                #{item}"
    p "  item_attribute:      #{item_attribute}"
    p "  related_by:          #{related_by}"
    p "  to_item:             #{to_item}"
    p "  to_item_attribute:   #{to_item_attribute}"
    p "  multiplier:          #{multiplier}"
    p "  constant:            #{constant}"
    p "  priority:            #{priority || "required"}"
    p "  internal_ident:      #{internal_ident}"
  end

  if errors.size > 0
    raise(errors.join(", "))
  end

  unless res_item.hidden?
    @visible_items.unshift(item)
  end

  if remove
    if constraint = @constraints[internal_ident]
      if debug
        p "status:"
        p "  existing (for removal)"
      end
      @view.removeConstraint(constraint)
    else
      raise "RMX::Layout could not find constraint to remove for internal_ident: `#{internal_ident}` (note: this is an internal representation of the constraint, not the exact string given).  Make sure the constraint was created first."
    end
  elsif constraint = @constraints[internal_ident]
    if debug
      p "status:"
      p "  existing (for modification)"
    end
    constraint.constant = res_constant
  else
    constraint = NSLayoutConstraint.constraintWithItem(res_item,
       attribute:res_item_attribute,
       relatedBy:res_related_by,
          toItem:res_to_item,
       attribute:res_to_item_attribute,
      multiplier:res_multiplier,
        constant:res_constant)
    if debug
      p "status:"
      p "  created"
    end
    @constraints[internal_ident] = constraint
    if res_priority
      constraint.priority = res_priority
    end
    @view.addConstraint(constraint)
  end

  if debug
    p "implemented:"
    p "  #{constraint.description}"
  end

  constraint
end

#eqs(str) ⇒ Object

takes a string one or more equations separated by newlines and processes each. returns an array of constraints



115
116
117
118
119
# File 'lib/motion/layout.rb', line 115

def eqs(str)
  str.split("\n").map(&:strip).select { |x| !x.empty? }.map do |line|
    eq(line)
  end.compact
end

#remove(constraint) ⇒ Object



83
84
85
86
87
88
89
90
# File 'lib/motion/layout.rb', line 83

def remove(constraint)
  constraints = [ constraint ].flatten.compact
  @view.removeConstraints(constraints)
  @constraints.keys.each do |key|
    @constraints.delete(key) if constraints.include?(@constraints.fetch(key))
  end
  true
end

#reopenObject

reopens the RMX::Layout instance for additional processing, ex:

@layout.reopen do |layout|
  ...
end

note: you would need to store your instance somewhere on creation to be able to reopen it later, ex:

@layout = RMX::Layout.new do |layout|
  ...
end


72
73
74
75
76
77
# File 'lib/motion/layout.rb', line 72

def reopen
  if block_given?
    yield self
  end
  self
end

#subviews(subviews) ⇒ Object



100
101
102
103
104
105
106
107
# File 'lib/motion/layout.rb', line 100

def subviews(subviews)
  @subviews = subviews
  @subviews.values.each do |subview|
    subview.translatesAutoresizingMaskIntoConstraints = false
    @view.addSubview(subview)
  end
  @subviews
end

#subviews=(views) ⇒ Object



109
110
111
# File 'lib/motion/layout.rb', line 109

def subviews=(views)
  subviews(views)
end

#view(view) ⇒ Object



92
93
94
# File 'lib/motion/layout.rb', line 92

def view(view)
  @view = view
end

#view=(v) ⇒ Object



96
97
98
# File 'lib/motion/layout.rb', line 96

def view=(v)
  view(v)
end

#xeq(str) ⇒ Object

removes the constraint matching equation string. constant is not considered. if no matching constraint is found, it will raise an exception.



305
306
307
# File 'lib/motion/layout.rb', line 305

def xeq(str)
  eq(str, true)
end