motion-kit 0.0.1 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +839 -0
- data/lib/motion-kit-cocoa/cocoa_util.rb +59 -0
- data/lib/motion-kit-cocoa/constraints/constraint.rb +765 -0
- data/lib/motion-kit-cocoa/constraints/constraint_placeholder.rb +22 -0
- data/lib/motion-kit-cocoa/constraints/constraints_layout.rb +536 -0
- data/lib/motion-kit-cocoa/constraints/constraints_target.rb +52 -0
- data/lib/motion-kit-cocoa/layouts/cagradientlayer_layout.rb +13 -0
- data/lib/motion-kit-cocoa/layouts/calayer_layout.rb +27 -0
- data/lib/motion-kit-cocoa/layouts/sugarcube_compat.rb +38 -0
- data/lib/motion-kit-ios/dummy.rb +93 -0
- data/lib/motion-kit-ios/ios_util.rb +20 -0
- data/lib/motion-kit-ios/layouts/constraints_layout.rb +22 -0
- data/lib/motion-kit-ios/layouts/layout_device.rb +32 -0
- data/lib/motion-kit-ios/layouts/layout_orientation.rb +47 -0
- data/lib/motion-kit-ios/layouts/uibutton_layout.rb +52 -0
- data/lib/motion-kit-ios/layouts/uiview_layout.rb +45 -0
- data/lib/motion-kit-ios/layouts/uiview_layout_autoresizing.rb +75 -0
- data/lib/motion-kit-ios/layouts/uiview_layout_constraints.rb +36 -0
- data/lib/motion-kit-ios/layouts/uiview_layout_frame.rb +307 -0
- data/lib/motion-kit-ios/layouts/uiview_layout_gradient.rb +20 -0
- data/lib/motion-kit-osx/dummy.rb +89 -0
- data/lib/motion-kit-osx/layouts/constraints_layout.rb +42 -0
- data/lib/motion-kit-osx/layouts/nsmenu_extensions.rb +186 -0
- data/lib/motion-kit-osx/layouts/nsmenu_layout.rb +109 -0
- data/lib/motion-kit-osx/layouts/nsmenuitem_extensions.rb +45 -0
- data/lib/motion-kit-osx/layouts/nstablecolumn_layout.rb +12 -0
- data/lib/motion-kit-osx/layouts/nstableview_layout.rb +20 -0
- data/lib/motion-kit-osx/layouts/nsview_layout.rb +47 -0
- data/lib/motion-kit-osx/layouts/nsview_layout_autoresizing.rb +75 -0
- data/lib/motion-kit-osx/layouts/nsview_layout_constraints.rb +34 -0
- data/lib/motion-kit-osx/layouts/nsview_layout_frame.rb +375 -0
- data/lib/motion-kit-osx/layouts/nswindow_frame.rb +14 -0
- data/lib/motion-kit-osx/layouts/nswindow_layout.rb +77 -0
- data/lib/motion-kit-osx/osx_util.rb +16 -0
- data/lib/motion-kit.rb +33 -0
- data/lib/motion-kit/calculate.rb +263 -0
- data/lib/motion-kit/layouts/base_layout.rb +299 -0
- data/lib/motion-kit/layouts/base_layout_class_methods.rb +43 -0
- data/lib/motion-kit/layouts/parent.rb +68 -0
- data/lib/motion-kit/layouts/view_layout.rb +327 -0
- data/lib/motion-kit/motion-kit.rb +18 -0
- data/lib/motion-kit/object.rb +16 -0
- data/lib/motion-kit/util.rb +20 -0
- data/lib/motion-kit/version.rb +1 -1
- data/spec/ios/apply_styles_spec.rb +21 -0
- data/spec/ios/autoresizing_helper_spec.rb +224 -0
- data/spec/ios/calculate_spec.rb +322 -0
- data/spec/ios/calculator_spec.rb +31 -0
- data/spec/ios/constraints_helpers/attribute_lookup_spec.rb +27 -0
- data/spec/ios/constraints_helpers/axis_lookup_spec.rb +13 -0
- data/spec/ios/constraints_helpers/center_constraints_spec.rb +419 -0
- data/spec/ios/constraints_helpers/constraint_placeholder_spec.rb +72 -0
- data/spec/ios/constraints_helpers/priority_lookup_spec.rb +19 -0
- data/spec/ios/constraints_helpers/relationship_lookup_spec.rb +27 -0
- data/spec/ios/constraints_helpers/relative_corners_spec.rb +274 -0
- data/spec/ios/constraints_helpers/relative_location_spec.rb +111 -0
- data/spec/ios/constraints_helpers/simple_constraints_spec.rb +2763 -0
- data/spec/ios/constraints_helpers/size_constraints_spec.rb +422 -0
- data/spec/ios/constraints_helpers/view_lookup_constraints_spec.rb +93 -0
- data/spec/ios/create_layout_spec.rb +40 -0
- data/spec/ios/custom_layout_spec.rb +13 -0
- data/spec/ios/deferred_spec.rb +89 -0
- data/spec/ios/device_helpers_spec.rb +51 -0
- data/spec/ios/frame_helper_spec.rb +1150 -0
- data/spec/ios/layer_layout_spec.rb +36 -0
- data/spec/ios/layout_extensions_spec.rb +70 -0
- data/spec/ios/layout_spec.rb +74 -0
- data/spec/ios/layout_state_spec.rb +27 -0
- data/spec/ios/motionkit_util_spec.rb +102 -0
- data/spec/ios/objc_selectors_spec.rb +10 -0
- data/spec/ios/orientation_helper_specs.rb +67 -0
- data/spec/ios/parent_layout_spec.rb +19 -0
- data/spec/ios/parent_spec.rb +45 -0
- data/spec/ios/remove_layout_spec.rb +25 -0
- data/spec/ios/root_layout_spec.rb +53 -0
- data/spec/ios/setters_spec.rb +63 -0
- data/spec/ios/uibutton_layout_spec.rb +24 -0
- data/spec/ios/uitextfield_spec.rb +14 -0
- data/spec/ios/view_attr_spec.rb +25 -0
- data/spec/osx/autoresizing_helper_spec.rb +224 -0
- data/spec/osx/constraints_helper_spec.rb +0 -0
- data/spec/osx/constraints_helpers/orientation_lookup_spec.rb +13 -0
- data/spec/osx/constraints_helpers/simple_constraints_spec.rb +2095 -0
- data/spec/osx/constraints_helpers/size_constraints_spec.rb +362 -0
- data/spec/osx/create_menu_spec.rb +14 -0
- data/spec/osx/create_via_extensions_spec.rb +63 -0
- data/spec/osx/deferred_spec.rb +85 -0
- data/spec/osx/frame_helper_spec.rb +1881 -0
- data/spec/osx/menu_extensions_spec.rb +376 -0
- data/spec/osx/menu_layout_spec.rb +157 -0
- data/spec/osx/menu_spec.rb +70 -0
- data/spec/osx/root_menu_spec.rb +15 -0
- 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
|