motion-kit 0.0.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +839 -0
  3. data/lib/motion-kit-cocoa/cocoa_util.rb +59 -0
  4. data/lib/motion-kit-cocoa/constraints/constraint.rb +765 -0
  5. data/lib/motion-kit-cocoa/constraints/constraint_placeholder.rb +22 -0
  6. data/lib/motion-kit-cocoa/constraints/constraints_layout.rb +536 -0
  7. data/lib/motion-kit-cocoa/constraints/constraints_target.rb +52 -0
  8. data/lib/motion-kit-cocoa/layouts/cagradientlayer_layout.rb +13 -0
  9. data/lib/motion-kit-cocoa/layouts/calayer_layout.rb +27 -0
  10. data/lib/motion-kit-cocoa/layouts/sugarcube_compat.rb +38 -0
  11. data/lib/motion-kit-ios/dummy.rb +93 -0
  12. data/lib/motion-kit-ios/ios_util.rb +20 -0
  13. data/lib/motion-kit-ios/layouts/constraints_layout.rb +22 -0
  14. data/lib/motion-kit-ios/layouts/layout_device.rb +32 -0
  15. data/lib/motion-kit-ios/layouts/layout_orientation.rb +47 -0
  16. data/lib/motion-kit-ios/layouts/uibutton_layout.rb +52 -0
  17. data/lib/motion-kit-ios/layouts/uiview_layout.rb +45 -0
  18. data/lib/motion-kit-ios/layouts/uiview_layout_autoresizing.rb +75 -0
  19. data/lib/motion-kit-ios/layouts/uiview_layout_constraints.rb +36 -0
  20. data/lib/motion-kit-ios/layouts/uiview_layout_frame.rb +307 -0
  21. data/lib/motion-kit-ios/layouts/uiview_layout_gradient.rb +20 -0
  22. data/lib/motion-kit-osx/dummy.rb +89 -0
  23. data/lib/motion-kit-osx/layouts/constraints_layout.rb +42 -0
  24. data/lib/motion-kit-osx/layouts/nsmenu_extensions.rb +186 -0
  25. data/lib/motion-kit-osx/layouts/nsmenu_layout.rb +109 -0
  26. data/lib/motion-kit-osx/layouts/nsmenuitem_extensions.rb +45 -0
  27. data/lib/motion-kit-osx/layouts/nstablecolumn_layout.rb +12 -0
  28. data/lib/motion-kit-osx/layouts/nstableview_layout.rb +20 -0
  29. data/lib/motion-kit-osx/layouts/nsview_layout.rb +47 -0
  30. data/lib/motion-kit-osx/layouts/nsview_layout_autoresizing.rb +75 -0
  31. data/lib/motion-kit-osx/layouts/nsview_layout_constraints.rb +34 -0
  32. data/lib/motion-kit-osx/layouts/nsview_layout_frame.rb +375 -0
  33. data/lib/motion-kit-osx/layouts/nswindow_frame.rb +14 -0
  34. data/lib/motion-kit-osx/layouts/nswindow_layout.rb +77 -0
  35. data/lib/motion-kit-osx/osx_util.rb +16 -0
  36. data/lib/motion-kit.rb +33 -0
  37. data/lib/motion-kit/calculate.rb +263 -0
  38. data/lib/motion-kit/layouts/base_layout.rb +299 -0
  39. data/lib/motion-kit/layouts/base_layout_class_methods.rb +43 -0
  40. data/lib/motion-kit/layouts/parent.rb +68 -0
  41. data/lib/motion-kit/layouts/view_layout.rb +327 -0
  42. data/lib/motion-kit/motion-kit.rb +18 -0
  43. data/lib/motion-kit/object.rb +16 -0
  44. data/lib/motion-kit/util.rb +20 -0
  45. data/lib/motion-kit/version.rb +1 -1
  46. data/spec/ios/apply_styles_spec.rb +21 -0
  47. data/spec/ios/autoresizing_helper_spec.rb +224 -0
  48. data/spec/ios/calculate_spec.rb +322 -0
  49. data/spec/ios/calculator_spec.rb +31 -0
  50. data/spec/ios/constraints_helpers/attribute_lookup_spec.rb +27 -0
  51. data/spec/ios/constraints_helpers/axis_lookup_spec.rb +13 -0
  52. data/spec/ios/constraints_helpers/center_constraints_spec.rb +419 -0
  53. data/spec/ios/constraints_helpers/constraint_placeholder_spec.rb +72 -0
  54. data/spec/ios/constraints_helpers/priority_lookup_spec.rb +19 -0
  55. data/spec/ios/constraints_helpers/relationship_lookup_spec.rb +27 -0
  56. data/spec/ios/constraints_helpers/relative_corners_spec.rb +274 -0
  57. data/spec/ios/constraints_helpers/relative_location_spec.rb +111 -0
  58. data/spec/ios/constraints_helpers/simple_constraints_spec.rb +2763 -0
  59. data/spec/ios/constraints_helpers/size_constraints_spec.rb +422 -0
  60. data/spec/ios/constraints_helpers/view_lookup_constraints_spec.rb +93 -0
  61. data/spec/ios/create_layout_spec.rb +40 -0
  62. data/spec/ios/custom_layout_spec.rb +13 -0
  63. data/spec/ios/deferred_spec.rb +89 -0
  64. data/spec/ios/device_helpers_spec.rb +51 -0
  65. data/spec/ios/frame_helper_spec.rb +1150 -0
  66. data/spec/ios/layer_layout_spec.rb +36 -0
  67. data/spec/ios/layout_extensions_spec.rb +70 -0
  68. data/spec/ios/layout_spec.rb +74 -0
  69. data/spec/ios/layout_state_spec.rb +27 -0
  70. data/spec/ios/motionkit_util_spec.rb +102 -0
  71. data/spec/ios/objc_selectors_spec.rb +10 -0
  72. data/spec/ios/orientation_helper_specs.rb +67 -0
  73. data/spec/ios/parent_layout_spec.rb +19 -0
  74. data/spec/ios/parent_spec.rb +45 -0
  75. data/spec/ios/remove_layout_spec.rb +25 -0
  76. data/spec/ios/root_layout_spec.rb +53 -0
  77. data/spec/ios/setters_spec.rb +63 -0
  78. data/spec/ios/uibutton_layout_spec.rb +24 -0
  79. data/spec/ios/uitextfield_spec.rb +14 -0
  80. data/spec/ios/view_attr_spec.rb +25 -0
  81. data/spec/osx/autoresizing_helper_spec.rb +224 -0
  82. data/spec/osx/constraints_helper_spec.rb +0 -0
  83. data/spec/osx/constraints_helpers/orientation_lookup_spec.rb +13 -0
  84. data/spec/osx/constraints_helpers/simple_constraints_spec.rb +2095 -0
  85. data/spec/osx/constraints_helpers/size_constraints_spec.rb +362 -0
  86. data/spec/osx/create_menu_spec.rb +14 -0
  87. data/spec/osx/create_via_extensions_spec.rb +63 -0
  88. data/spec/osx/deferred_spec.rb +85 -0
  89. data/spec/osx/frame_helper_spec.rb +1881 -0
  90. data/spec/osx/menu_extensions_spec.rb +376 -0
  91. data/spec/osx/menu_layout_spec.rb +157 -0
  92. data/spec/osx/menu_spec.rb +70 -0
  93. data/spec/osx/root_menu_spec.rb +15 -0
  94. metadata +166 -14
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a17092b88b64e247ac1e7b87d1b8b5dcf71ada84
4
+ data.tar.gz: 20f75375fac394bb02b1a4ae35396d5f27124224
5
+ SHA512:
6
+ metadata.gz: 2fa4c416fd99a1b2bb06e58c1235442a8773a0546d0acfc22cd1db4c3e8a06e4d6645b1c8990979f0a3266ada6cc192a89a9d6874d693b0c48171c0dcaad74bf
7
+ data.tar.gz: 7ad4893a25e08b397bc2575a002d7dde744617896185828723e7e7f8e1122da525489c52e0ec11d9497a403de5650f8e1c2cf18c7f230fe7ef4d8182673c73d9
data/README.md ADDED
@@ -0,0 +1,839 @@
1
+ # MotionKit
2
+
3
+ *The RubyMotion layout and styling gem.*
4
+
5
+ 1. Crossplatform compatibility: iOS, OSX
6
+ 2. Simple, easy to learn DSL
7
+ 3. Crossframework compatibility:
8
+ - [UIKit][readmore-uikit]
9
+ - [ApplicationKit][readmore-applicationkit]
10
+ - [AutoLayout][readmore-autolayout]
11
+ - [Frame geometry][readmore-frames]
12
+ - [CoreAnimation][readmore-coreanimation]
13
+ - [NSMenu/NSMenuItem][readmore-nsmenu]
14
+ - [Joybox][readmore-joybox] *TODO*
15
+ - [SpriteKit][readmore-spritekit] *TODO*
16
+ 4. Non-polluting
17
+ 5. ProMotion/RMQ/SugarCube-compatible (kind of goes hand-in-hand with non-polluting)
18
+ 6. Styles and layouts are "just code" (not hash-based like in Teacup)
19
+ 7. Written by [the authors][authors] of [ProMotion][] and [Teacup][]
20
+
21
+ [authors]: CONTRIBUTORS.md
22
+ [Colin]: https://github.com/colinta
23
+ [Jamon]: https://github.com/jamonholmgren
24
+ [ProMotion]: https://github.com/clearsightstudio/ProMotion
25
+ [RMQ]: https://github.com/infinitered/rmq
26
+ [Teacup]: https://github.com/rubymotion/teacup
27
+
28
+ [readmore-uikit]: https://github.com/rubymotion/motion-kit/blob/master/READMORE.md#uikit
29
+ [readmore-applicationkit]: https://github.com/rubymotion/motion-kit/blob/master/READMORE.md#applicationkit
30
+ [readmore-joybox]: https://github.com/rubymotion/motion-kit/blob/master/READMORE.md#joybox
31
+ [readmore-spritekit]: https://github.com/rubymotion/motion-kit/blob/master/READMORE.md#spritekit
32
+ [readmore-coreanimation]: https://github.com/rubymotion/motion-kit/blob/master/READMORE.md#coreanimation
33
+ [readmore-frames]: https://github.com/rubymotion/motion-kit/blob/master/READMORE.md#frames
34
+ [readmore-autolayout]: https://github.com/rubymotion/motion-kit/blob/master/READMORE.md#autolayout
35
+ [readmore-nsmenu]: https://github.com/rubymotion/motion-kit/blob/master/READMORE.md#nsmenu
36
+
37
+
38
+ ## What happened to Teacup??
39
+
40
+ You can [read all about](#goodbye-teacup) why Colin decided that Teacup needed to
41
+ be replaced with a new project, rather than upgraded or refactored.
42
+
43
+
44
+ ## Usage
45
+
46
+ From your controller you will instantiate a `MotionKit::Layout` instance, and
47
+ request views from it. `layout.view` is the root view, and it's common to
48
+ assign this to `self.view` in your `loadView` method. You'll also want to hook
49
+ up your instance variables, using `layout.get(:id)` or using instance variables.
50
+
51
+ ```ruby
52
+ class LoginController < UIViewController
53
+ def loadView
54
+ @layout = LoginLayout.new
55
+ self.view = @layout.view
56
+
57
+ @button = @layout.get(:button) # This will be created in our layout (below)
58
+ @button = @layout.button # Alternatively you can use instance variables and accessor methods
59
+ end
60
+
61
+ def viewDidLoad
62
+ @button.on(:touch) { my_code } # Mix with some SugarCube for sweetness!
63
+ rmq(@button).on(:touch) { my_code } # and of course RMQ works just as well
64
+ end
65
+ end
66
+ ```
67
+
68
+
69
+ ### Lay out your subviews with a clean DSL
70
+
71
+ In a layout class, the `layout` method is expected to create the view hierarchy,
72
+ and it should also take care of frames and styling. You can apply styles here,
73
+ and it's handy to do so when you are creating a quick mock-up, or a very small
74
+ app. But in a real application, you'll want to include a Stylesheet module, so
75
+ your layout isn't cluttered with all your styling code.
76
+
77
+ Here's a layout that just puts a label and a button in the middle of the screen:
78
+
79
+ ```ruby
80
+ class SimpleLayout < MotionKit::Layout
81
+ # this is a special attr method that calls `layout` if the view hasn't been
82
+ # created yet. So you can call `layout.button` before `layout.view` and you
83
+ # won't get nil, and layout.view will be built.
84
+ view :button
85
+
86
+ def layout
87
+ add UILabel, :label
88
+
89
+ @button = add UIButton, :button
90
+ end
91
+
92
+ def label_style
93
+ text 'Hi there! Welcome to MotionKit'
94
+ font UIFont.fontWithName('Comic Sans', size: 24)
95
+ sizeToFit
96
+
97
+ # note: there are better ways to set the center, see the frame helpers below
98
+ center [CGRectGetMidX(superview.bounds), CGRectGetMidY(superview.bounds)]
99
+ text_alignment UITextAlignmentCenter
100
+ text_color UIColor.whiteColor
101
+
102
+ # if you prefer to use shorthands from another gem, you certainly can!
103
+ background_color rmq.color.white # from RMQ
104
+ background_color :white.uicolor # from SugarCube
105
+ end
106
+
107
+ def button_style
108
+ # this will call 'setTitle(forState:)' via a UIButton helper
109
+ title 'Press it!'
110
+ # this shorthand is much better! More about frame helpers below.
111
+ center ['50%', '50% + 50']
112
+ end
113
+
114
+ end
115
+ ```
116
+
117
+ Nice, that should be pretty easy to follow, right? M'kay, in this next, more
118
+ complicated layout we'll create a login page, with a 'Login' button and inputs
119
+ for username and password. In this example, I will assign the frame in the
120
+ `layout` method, instead of in the `_style` methods. This is purely an
121
+ aesthetic choice. Some people like to have their frame code in the layout
122
+ method, others like to put it in the _style methods. I point it out only as an
123
+ available feature.
124
+
125
+ ```ruby
126
+ class LoginLayout < MotionKit::Layout
127
+ # we write our `_style` methods in a module
128
+ include LoginStyles
129
+
130
+ def layout
131
+ # we know it's easy to add a subview, with a stylename...
132
+ add UIImageView, :logo
133
+
134
+ # inside a block you can set properties on that view
135
+ add UIImageView, :logo do
136
+ frame [[0, 0], [320, 568]]
137
+ end
138
+ # hardcoded dimensions!? there's got to be a better way...
139
+
140
+ # This frame argument will be handed to the 'MotionKit::Layout#frame'
141
+ # method, which can accept lots of shorthands. Let's use one to scale the
142
+ # imageview so that it fills the width, but keeps its aspect ratio.
143
+ add UIImageView, :logo do
144
+ frame [[0, 0], ['100%', :scale]]
145
+ end
146
+ # 'scale' uses sizeToFit and the other width/height property to keep the
147
+ # aspect ratio the same. Neat, huh?
148
+
149
+ add UIView, :button_container do
150
+ # Like I said, the frame method is very powerful, there are lots of
151
+ # ways it can help with laying out your rects, and it will even try to
152
+ # apply the correct autoresizingMask for you; the from_bottom method will
153
+ # set the UIAutoresizingMask to "FlexibleTop", and using '100%' in the
154
+ # width will ensure the frame stays the width of its parent.
155
+ frame from_bottom(height: 50, width: '100%')
156
+ frame from_bottom(h: 50, w: '100%') # is fine, too
157
+
158
+ # same as above; assumes full width
159
+ frame from_bottom(height: 50)
160
+
161
+ # similar to Teacup, views added inside a block are added to that
162
+ # container. You can reference the container with 'superview', but then
163
+ # you're working on the object directly, so no method translation (foo_bar
164
+ # => fooBar) will be done for you.
165
+ add UIButton, :login_button do
166
+ background_color superview.backgroundColor
167
+
168
+ # 'parent' is not instance of a view, it's a special object that
169
+ # acts like a placeholder for various values; if you want to assign
170
+ # *any* superview property, use 'superview' instead. 'parent' is mostly
171
+ # useful for setting the frame.
172
+ frame [[ 10, 5 ], [ 50, parent.height - 10 ]]
173
+ end
174
+ end
175
+
176
+ add UIView, :inputs do
177
+ frame x: 0, y: 0, width: '100%', height: '100% - 50'
178
+
179
+ # setting autoresizing_mask should handle rotation events; this overrides
180
+ # any automatic mask settings that occurred in 'frame'
181
+ autoresizing_mask :pin_to_top, :flexible_height, :flexible_width
182
+
183
+ # we'll use 'sizeToFit' to calculate the height
184
+ add UITextField, :username_input do
185
+ frame [[10, 10], ['100% - 10', :auto]]
186
+ end
187
+ add UITextField, :password_input do
188
+ frame below(:username_input, margin: 8)
189
+ end
190
+ end
191
+ end
192
+ end
193
+ ```
194
+
195
+
196
+ ### Styles are compiled, simple, and clean
197
+
198
+ In MotionKit, when you define a method that has the same name as a view
199
+ stylename with the suffix "_style", that method is called and is expected to
200
+ style that view.
201
+
202
+ ```ruby
203
+ class LoginLayout < MK::Layout
204
+
205
+ def layout
206
+ add UIImageView, :logo do
207
+ # this can be moved into `logo_style` below:
208
+ frame [[0, 0], ['100%', :scale]]
209
+ end
210
+ add UIView, :button_container
211
+ end
212
+
213
+ def logo_style
214
+ frame [[0, 0], ['100%', :scale]]
215
+ image UIImage.imageNamed('logo')
216
+ end
217
+
218
+ def button_container_style
219
+ background_color UIColor.clearColor
220
+ end
221
+
222
+ # In case you're curious, the MK::Layout#initialize method takes no arguments.
223
+ # Just be sure to call `super`
224
+ def initialize
225
+ super
226
+ # ...
227
+ end
228
+
229
+ end
230
+ ```
231
+
232
+ So as an additional code-cleanup step, why not put those methods in a module,
233
+ and include them in your layout! Sounds clean and organized to me! You can
234
+ include multiple stylesheets this way, just be careful around name collisions.
235
+
236
+ ```ruby
237
+ module LoginStyles
238
+
239
+ def login_button_style
240
+ background_color '#51A8E7'.uicolor
241
+ title 'Log In'
242
+ # `layer` returns a CALayer, which in turn becomes the new context inside
243
+ # this block!
244
+ layer do
245
+ corner_radius 7.0
246
+ shadow_color '#000000'.cgcolor
247
+ shadow_opacity 0.9
248
+ shadow_radius 2.0
249
+ shadow_offset [0, 0]
250
+ end
251
+ end
252
+
253
+ end
254
+
255
+ # back in our LoginLayout class
256
+ class LoginLayout
257
+ include LoginStyles
258
+
259
+ def layout
260
+ add UIButton, :login_button
261
+ # ...
262
+ end
263
+
264
+ end
265
+ ```
266
+
267
+
268
+ ### How do styles get applied?
269
+
270
+ If you've used RMQ's Stylers, you'll recognize a very similar pattern here. In
271
+ RMQ the 'style' methods are handed a 'Styler' instance, which wraps access to
272
+ the view. In MotionKit we make use of `method_missing` to call these methods
273
+ indirectly. That takes care of most methods related to styling, except those
274
+ that take multiple arguments. Those can get "helper" methods.
275
+
276
+ ```ruby
277
+ def login_label_style
278
+ text 'Press me' # this gets delegated to UILabel#text
279
+ end
280
+
281
+ # It's not hard to add extensions for common tasks, like setting the "normal"
282
+ # title on a UIButton
283
+ def login_button_style
284
+ title 'Press me'
285
+ # this gets delegated to UIButtonLayout#title(title), which in turn calls
286
+ # button.setTitle(title, forState: UIControlStateNormal)
287
+ end
288
+ ```
289
+
290
+ MotionKit offers shortcuts and mini-DSLs for frames, auto-layout, and
291
+ miscellaneous helpers. But if a method is not defined, it is sent to the view
292
+ after a little introspection. If you call a method like `title_color value`, MotionKit
293
+ will try to call:
294
+
295
+ - `setTitle_color(value)`
296
+ - `title_color=(value)`
297
+ - `title_color(value)`
298
+ - (try again, converting to camelCase)
299
+ - `setTitleColor(value)`
300
+ - `titleColor=(value)`
301
+ - `titleColor(value)`
302
+ - (failure:) `raise NoMethodError`
303
+
304
+ ```ruby
305
+ def login_button_style
306
+ background_color UIColor.clearColor # this gets converted to `self.target.backgroundColor = ...`
307
+ end
308
+ ```
309
+
310
+ You can easily add your own helpers to MotionKit's existing Layout classes. They
311
+ are all named consistenly, e.g. `MotionKit::UIViewLayout`, e.g.
312
+ `MotionKit::UILabelLayout`. Just open up these classes and hack away.
313
+
314
+ ```ruby
315
+ module MotionKit
316
+ # these helpers will only be applied to instances of UILabel and UILabel
317
+ # subclasses
318
+ class UILabelLayout
319
+
320
+ # style methods can accept any number of arguments, and a block. The current
321
+ # view should be referred to via the method `target`
322
+ def color(color)
323
+ target.textColor = color
324
+ end
325
+
326
+ # If a block is passed it is your responsibility to call `context(val,
327
+ # &block)`, if that is appropriate. I'll use `UIView#layer` as an example,
328
+ # but actually if you pass a block to a method that returns an object, that
329
+ # block will be called with that object as the context.
330
+ def layer(&block)
331
+ context(target.layer, &block)
332
+ end
333
+
334
+ # Sure, you can add flow-control mechanisms if that's your thing!
335
+ #
336
+ # You can use the block to conditionally call code; on iOS there are
337
+ # orientation helpers `portrait`, `landscape`, etc that apply styles based
338
+ # on the current orientation.
339
+ def sometimes(&block)
340
+ if rand > 0.5
341
+ yield
342
+ end
343
+ end
344
+
345
+ end
346
+ end
347
+ ```
348
+
349
+ For your own custom classes, you can provide a Layout class by calling the
350
+ `targets` method in your class body.
351
+
352
+ ```ruby
353
+ # Be sure to extend an existing Layout class, otherwise you'll lose a lot of
354
+ # functionality. Often this will be `MK::UIViewLayout` on iOS and
355
+ # `MK::NSViewLayout` on OS X.
356
+ class CustomLayout < MK::UIViewLayout
357
+ targets CustomView
358
+
359
+ def fore_color(value)
360
+ target.foregroundColor = value
361
+ end
362
+
363
+ end
364
+ ```
365
+
366
+
367
+ ### Frames
368
+
369
+ There are lots of frame helpers for NSView and UIView subclasses. It's cool
370
+ that you can set position and sizes as percents, but scroll down to see examples
371
+ of setting frames *based on any other view*. These are super useful! Most of
372
+ the ideas, method names, and some code came straight out of
373
+ [geomotion](https://github.com/clayallsopp/geomotion). It's not *quite as
374
+ powerful* as geomotion, but it's close!
375
+
376
+ One advantage over geomotion is that many of these frame helpers accept a view
377
+ or view name, so that you can place the view relative to that view.
378
+
379
+ ```ruby
380
+ # most direct way to set the frame, using pt values
381
+ frame [[0, 0], [320, 568]]
382
+
383
+ # using sizes relative to superview
384
+ frame [[5, 5], ['100% - 10pt', '100% - 10pt']]
385
+ # the 'pt' suffix is optional, and ignored. in the future we could add support
386
+ # for other suffixes - would that even be useful? probably not...
387
+
388
+ # other available methods:
389
+ origin [5, 5]
390
+ x 5 # aka left(..)
391
+ right 5 # right side of the view is 5px from the left side of the superview
392
+ bottom 5 # bottom of the view is 5px from the top of the superview
393
+ size ['100% - 10', '100% - 10']
394
+ width '100% - 10' # aka w(...)
395
+ height '100% - 10' # aka h(...)
396
+
397
+ size ['90%', '90%']
398
+ center ['50%', '50%']
399
+
400
+ ########
401
+ # +--------------------------------------------------+
402
+ # |from_top_left from_top from_top_right|
403
+ # | |
404
+ # |from_left from_center from_right|
405
+ # | |
406
+ # |from_bottom_left from_bottom from_bottom_right|
407
+ # +--------------------------------------------------+
408
+ ```
409
+
410
+ You can position the view *relative to other views*, either the superview or any
411
+ other view. You must pass the return value to `frame`.
412
+
413
+ ```ruby
414
+ # If you don't specify a view to base off of, the view is positioned relative to
415
+ # the superview:
416
+ frame from_bottom_right(size: [100, 100]) # 100x100 in the BR corner
417
+ frame from_bottom(size: ['100%', 32]) # full width, 32pt height
418
+ frame from_top_right(left: 5)
419
+
420
+ # But if you pass a view or symbol as the first arg, the position will be
421
+ # relative to that view
422
+ from_top_right(:info_container, left: 5)
423
+
424
+
425
+ ########
426
+ # above
427
+ # +---+
428
+ # left_of | | right_of
429
+ # (before) | | (after)
430
+ # +---+
431
+ # below
432
+
433
+ # these methods *require* another view.
434
+ frame above(:foo, up: 8)
435
+
436
+ frame above(:foo, up: 8)
437
+ frame before(:foo, left: 8)
438
+ frame relative_to(:foo, down: 5, right: 5)
439
+
440
+ # it's not common, but you can also pass a view to any of these methods
441
+ foo = self.get(:foo)
442
+ frame from_bottom_left(foo, up: 5, left: 5)
443
+ ```
444
+
445
+
446
+ ### Autoresizing mask
447
+
448
+ You can pass symbols like `autoresizing_mask :flexible_width`, or use
449
+ symbols that have more intuitive meaning than the usual
450
+ `UIViewAutoresizingFlexible*` constants. These work in iOS and OS X.
451
+
452
+ ```ruby
453
+ autoresizing_mask :flexible_right, :flexible_bottom, :flexible_width
454
+ autoresizing_mask :pin_to_left, :rigid_top # 'rigid' undoes a 'flexible' setting
455
+ autoresizing_mask :pin_to_bottom, :flexible_width
456
+ autoresizing_mask :fill_top
457
+
458
+ flexible_left: Sticks to the right side
459
+ flexible_width: Width varies with parent
460
+ flexible_right: Sticks to the left side
461
+ flexible_top: Sticks to the bottom
462
+ flexible_height: Height varies with parent
463
+ flexible_bottom: Sticks to the top
464
+
465
+ rigid_left: Left side stays constant (undoes :flexible_left)
466
+ rigid_width: Width stays constant (undoes :flexible_width)
467
+ rigid_right: Right side stays constant (undoes :flexible_right)
468
+ rigid_top: Top stays constant (undoes :flexible_top)
469
+ rigid_height: Height stays constant (undoes :flexible_height)
470
+ rigid_bottom: Bottom stays constant (undoes :flexible_bottom)
471
+
472
+ fill: The size increases with an increase in parent size
473
+ fill_top: Width varies with parent and view sticks to the top
474
+ fill_bottom: Width varies with parent and view sticks to the bottom
475
+ fill_left: Height varies with parent and view sticks to the left
476
+ fill_right: Height varies with parent and view sticks to the right
477
+
478
+ pin_to_top_left: View stays in top-left corner, size does not change.
479
+ pin_to_top: View stays in top-center, size does not change.
480
+ pin_to_top_right: View stays in top-right corner, size does not change.
481
+ pin_to_left: View stays centered on the left, size does not change.
482
+ pin_to_center: View stays centered, size does not change.
483
+ pin_to_right: View stays centered on the right, size does not change.
484
+ pin_to_bottom_left: View stays in bottom-left corner, size does not change.
485
+ pin_to_bottom: View stays in bottom-center, size does not change.
486
+ pin_to_bottom_right: View stays in bottom-left corner, size does not change.
487
+ ```
488
+
489
+
490
+ ### Constraints / Auto Layout
491
+
492
+ Inside a `constraints` block you can use similar helpers as above, but you'll
493
+ be using Cocoa's Auto Layout system instead. This is the recommended way to set
494
+ your frames, now that Apple is introducing multiple display sizes. But beware,
495
+ Auto Layout can be frustrating... :-/
496
+
497
+ One pain point in working with constraints is determining when to add them to
498
+ your views. We tried really hard to figure out a way to automatically add them,
499
+ but it's just an untenable problem (Teacup suffers from a similar conundrum).
500
+
501
+ Essentially, the problem comes down to this: you will often want to set
502
+ constraints that are related to the view controller's `view`, but those must be
503
+ created/set *after* `controller.view = @layout.view`. Without doing some crazy
504
+ method mangling on NS/UIView we just can't do this automatically
505
+
506
+ Long story short: If you need to create constraints that refer to the controller
507
+ view, you need to use a separate method that is called after the view hierarchy
508
+ is created.
509
+
510
+ ```ruby
511
+ class MainLayout < UIViewLayout
512
+
513
+ def layout
514
+ add UILabel, :label do
515
+ constraints do
516
+ x 0
517
+ width('100%')
518
+ end
519
+ end
520
+ end
521
+
522
+ def add_constraints(controller)
523
+ unless @layout_constraints_added
524
+ @layout_constraints_added = true
525
+ constraints(:label) do
526
+ top.equals(controller.topLayoutGuide)
527
+ end
528
+ end
529
+ end
530
+
531
+ end
532
+
533
+ class MainController < UIViewController
534
+
535
+ def loadView
536
+ @layout = MainLayout.new
537
+ self.view = @layout
538
+ end
539
+
540
+ # for the constraints to work reliably they should be added in this method:
541
+ def updateViewConstraints
542
+ @layout.add_constraints(self) # !!!
543
+ super
544
+ end
545
+
546
+ end
547
+ ```
548
+
549
+ OK, with that hack out of the way, on to the examples!
550
+
551
+ ```ruby
552
+ constraints do
553
+ top_left x: 5, y: 10
554
+ # the MotionKit::Constraint class has lots of aliases and "smart" methods,
555
+ # so you can write very literate code:
556
+ top_left.equals([5, 10])
557
+ top_left.is([5, 10])
558
+ top_left.is.equal_to(x: 5, y: 10)
559
+ top_left.is == { x: 5, y: 10 }
560
+ top_left.is >= { x: 5, y: 10 }
561
+ top_left.is <= { x: 5, y: 10 }
562
+
563
+ # this is all the same as setting these two constraints:
564
+ x 5 # aka `left 5`
565
+ y 10 # aka `top 10`
566
+
567
+ # You can have multiple constraints on the same property, and if the
568
+ # priorities are set appropriately you can easily have minimum margins,
569
+ # minimum widths, that kind of thing:
570
+ x.is.at_least(10).priority(:required)
571
+ x.is(15).priority(:low)
572
+ width.is.at_least(100).priority(:required)
573
+ width.is(150).priority(:low)
574
+
575
+ # using the `Constraint#is` method you can even use ==, <= and >=
576
+ x.is >= 10
577
+ x.is == 15
578
+
579
+ # setting the priority:
580
+ (x.is >= 10).priority(:required)
581
+ (x.is == 15).priority(:low)
582
+ end
583
+ ```
584
+
585
+ But of course with AutoLayout you set up *relationships* between views. Using
586
+ the element-id as a placeholder for a view works especially well here.
587
+
588
+ ```ruby
589
+ constraints do
590
+ top_left.equals x: 5, y:5
591
+ width.equals(:foo).minus(10) # searches for a view named :foo
592
+ height.equals(:foo).minus(10)
593
+ # that's repetitive, so just set 'size'
594
+ size.equals(:foo).minus(10)
595
+ size.equals(:foo).minus([10, 15]) # 10pt thinner, 15pt shorter
596
+ end
597
+ ```
598
+
599
+ Just like with frame helpers you can use the `:element_id` to refer to another
600
+ view, but get this: the view need not be created yet! This is because when you
601
+ setup a constraints block, it isn't resolved immediately; the symbols are
602
+ resolved at the end. This feature uses the `deferred` method behind the scenes
603
+ to accomplish this.
604
+
605
+ ```ruby
606
+ add UIView, :foo do
607
+ constraints do
608
+ width.equals(:bar).plus(10) # :bar has not been added yet!
609
+ end
610
+ end
611
+
612
+ add UIView, :bar do
613
+ constraints do
614
+ width.equals(:foo).minus(10)
615
+ width.equals(100).minus(10)
616
+ # believe it or not, this ^ code works! AutoLayout is a strange beast; it's
617
+ # not an "imperative" system, it solves a system of equations. In this
618
+ # case, :bar will have width 110, and :foo will have width 100, because
619
+ # those values solve these equations:
620
+ # foo.width = 100
621
+ # foo.width = bar.width - 10
622
+ # foo.width = bar.width + 10
623
+ # If you have constraints that conflict you'll get error messages or
624
+ # nonsensical values.
625
+
626
+ # There are helpers that act as placeholders for views, if you have multiple
627
+ # views with the same name:
628
+ # first, last, nth
629
+ width.equals(last(:foo))
630
+ width.equals(first(:foo))
631
+ width.equals(nth(:foo, 5))
632
+ end
633
+ end
634
+ ```
635
+
636
+ ### Frames
637
+
638
+ There are lots of frame helpers for NSView and UIView subclasses:
639
+
640
+ ```ruby
641
+ # most direct
642
+ frame [[0, 0], [320, 568]]
643
+ # using relative sizes (relative to superview)
644
+ frame [[5, 5], ['100% - 10', '100% - 10']]
645
+
646
+ # same, but using separate methods
647
+ origin [5, 5]
648
+ x 5
649
+ y 5
650
+ size ['100% - 10', '100% - 10']
651
+ width '100% - 10'
652
+ height '100% - 10'
653
+
654
+ size ['90%', '90%']
655
+ center ['50%', '50%']
656
+
657
+ # you can position the view *relative to other views*, either the superview or
658
+ # *any* other view.
659
+ from_bottom_right size: [100, 100] # 100x100pt in the BR corner
660
+ from_bottom size: ['100%', 32] # full width, 32pt height
661
+ from_top_right left: 5
662
+
663
+ # from_top_left from_top from_top_right
664
+ # from_left from_center from_right
665
+ # from_bottom_left from_bottom from_bottom_right
666
+
667
+ # these require another view
668
+ foo = self.get(:foo)
669
+ above foo, up: 8
670
+ # above
671
+ # before after
672
+ # left_of right_of
673
+ # below
674
+ relative_to foo, down: 5, right: 5
675
+ from_bottom_left foo, up: 5, left: 5
676
+ ```
677
+
678
+ ### Constraints / AutoLayout
679
+
680
+ Inside a `constraints` block you can use the same helpers as above, but you'll
681
+ be using AutoLayout instead!
682
+
683
+ ```ruby
684
+ constraints do
685
+ from_top_left x: 5, y: 5
686
+ end
687
+ ```
688
+
689
+ But of course with constraints you can setup *relationships* between views.
690
+
691
+ ```ruby
692
+ foo = self.get(:foo)
693
+ constraints do
694
+ from_top_left x: 5, y:5
695
+ from_top_left down: 5, right:5
696
+ width.equals(foo).minus(10)
697
+ height.equals(foo).minus(10)
698
+ # that's repetitive, so just set 'size'
699
+ size.equals(foo).minus(10)
700
+ size.equals(foo).minus([10, 15]) # 10pt thinner, 15pt shorter
701
+ end
702
+ ```
703
+
704
+
705
+ ### Some handy tricks
706
+
707
+ #### Orientation specific styles
708
+
709
+ These are available on iOS.
710
+
711
+ ```ruby
712
+ add UIView, :container do
713
+ portrait do
714
+ frame from_top(width: '100%', height: 100)
715
+ end
716
+ landscape do
717
+ frame from_top_left(width: 300, height: 100)
718
+ end
719
+ end
720
+ ```
721
+
722
+ #### Update views via 'initial', 'reapply', and 'deferred'
723
+
724
+ If you call 'layout.reapply!', your style methods will be called again (but
725
+ NOT the `layout` method). You very well might want to control what methods get
726
+ called on later invocations, or only on the *initial* layout.
727
+
728
+ This is more for being able to initialize values, or to handle orientation, than
729
+ anything else. There is not much performance increase/decrease if you just
730
+ reapply styles every time, but you might not want to have your frame or colors
731
+ reset, if you've done some animation.
732
+
733
+ ```ruby
734
+ def login_button_style
735
+ # only once, when the `layout` is created
736
+ initial do
737
+ end
738
+
739
+ # on later invocations
740
+ reapply do
741
+ end
742
+
743
+ # style every time
744
+ title 'Press Me'
745
+ end
746
+ ```
747
+
748
+ Or, you might need to set a frame or other property based on a view that hasn't
749
+ been created yet. In this case, you can use `deferred` to have a block of code
750
+ run after the current layout is completed.
751
+
752
+ ```ruby
753
+ def login_button_style
754
+ deferred do
755
+ frame below(last(:label), height: 20)
756
+ end
757
+ end
758
+ ```
759
+
760
+ #### Apply styles via module
761
+
762
+ ```ruby
763
+ module AppStyles
764
+
765
+ def rounded_button
766
+ layer do
767
+ corner_radius 7
768
+ masks_to_bounds true
769
+ end
770
+ end
771
+
772
+ end
773
+
774
+ class LoginLayout < MotionKit::Layout
775
+ include AppStyles
776
+
777
+ def layout
778
+ add button, :login_button
779
+ end
780
+
781
+ def login_button_style
782
+ self.rounded_button
783
+ title 'Login'
784
+ end
785
+
786
+ end
787
+ ```
788
+
789
+ # Contributing
790
+
791
+ We welcome your contributions! Please be sure to run the specs before you do,
792
+ and consider adding support for both iOS and OS X.
793
+
794
+ To run the specs for both platforms, you will need to run `rake spec` twice:
795
+
796
+ ```
797
+ > rake spec # runs iOS specs
798
+ > rake spec platform=osx # OS X specs
799
+ ```
800
+
801
+
802
+ # Goodbye Teacup
803
+ ###### by [colinta][Colin]
804
+
805
+ If you've worked with XIB/NIB files, you might know that while they can be
806
+ cumbersome to deal with, they have the great benefit of keeping your controllers
807
+ free of layout and styling concerns. [Teacup][] brought some of this benefit, in
808
+ the form of stylesheets, but you still built the layout in the body of your
809
+ controller file. This needed to be fixed.
810
+
811
+ Plus Teacup is a beast! Imported stylesheets, orientation change events,
812
+ auto-layout support. It's got a ton of features, but with that comes a lot of
813
+ complexity. This has led to an unfortunate situation - I'm the *only* person who
814
+ understands the code base! This was never the intention of Teacup. It started
815
+ out as, and was always meant to be, a community project, with contributions
816
+ coming from all of its users.
817
+
818
+ When [ProMotion][] and later [RMQ][] were released, they both included their own
819
+ styling mechanisms. Including Teacup as a dependency would have placed a huge
820
+ burden on their users, and they would have had to ensure compatibility. Since
821
+ Teacup does a lot of method swizzling on base classes, this is not a trivial
822
+ undertaking.
823
+
824
+ If you use RMQ or ProMotion already, you'll find that MotionKit fits right in.
825
+ We designed it to be something that can easily be brought into an existing
826
+ project, too; it does not extend any base classes, so it's completely opt-in.
827
+
828
+ Unlike Teacup, you won't have your styles reapplied due to orientation changes,
829
+ but it's *really* easy to set that up, as you'll see. Or, use AutoLayout (the
830
+ DSL is better than Teacup's, I think) and you'll get orientation changes for
831
+ free!
832
+
833
+ Big thanks to everyone who contributed on this project! I hope it serves you
834
+ as well as Teacup, and for even longer into the future.
835
+
836
+ Sincerely,
837
+
838
+ Colin T.A. Gray
839
+ Feb 13, 2014