teacup 1.3.4 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. data/Gemfile +1 -1
  2. data/Gemfile.lock +3 -3
  3. data/README.md +1172 -319
  4. data/Rakefile +8 -1
  5. data/app/app_delegate.rb +1 -1
  6. data/app/controllers/appearance_controller.rb +13 -0
  7. data/app/controllers/landscape_only_controller.rb +1 -1
  8. data/app/controllers/{first_controller.rb → main_controller.rb} +4 -3
  9. data/app/controllers/motion_layout_controller.rb +22 -0
  10. data/app/styles/appearance.rb +24 -0
  11. data/app/styles/main_styles.rb +8 -6
  12. data/app/views/custom_view.rb +1 -0
  13. data/lib/teacup/calculations.rb +2 -2
  14. data/lib/teacup/{z_core_extensions → core_extensions}/ca_layer.rb +0 -0
  15. data/lib/teacup/core_extensions/view_getters.rb +61 -0
  16. data/lib/teacup/handler.rb +14 -14
  17. data/lib/teacup/layout.rb +94 -17
  18. data/lib/teacup/stylesheet.rb +61 -26
  19. data/lib/teacup/stylesheet_extensions/transform.rb +88 -0
  20. data/lib/teacup/teacup_controller.rb +122 -0
  21. data/lib/teacup/teacup_util.rb +12 -7
  22. data/lib/teacup/teacup_view.rb +329 -0
  23. data/lib/teacup/version.rb +1 -1
  24. data/lib/teacup-ios/appearance.rb +96 -0
  25. data/lib/teacup-ios/core_extensions/teacup_handlers.rb +183 -0
  26. data/lib/teacup-ios/core_extensions/ui_view.rb +30 -0
  27. data/lib/teacup-ios/core_extensions/ui_view_controller.rb +110 -0
  28. data/lib/{dummy.rb → teacup-ios/dummy.rb} +2 -6
  29. data/lib/teacup-ios/handler.rb +23 -0
  30. data/lib/{teacup → teacup-ios}/style.rb +9 -10
  31. data/lib/teacup-ios/stylesheet_extensions/autoresize.rb +169 -0
  32. data/lib/{teacup/stylesheet_extensions/geometry.rb → teacup-ios/stylesheet_extensions/device.rb} +0 -0
  33. data/lib/teacup-osx/core_extensions/ns_view.rb +39 -0
  34. data/lib/teacup-osx/core_extensions/ns_view_controller.rb +21 -0
  35. data/lib/teacup-osx/core_extensions/ns_window.rb +39 -0
  36. data/lib/teacup-osx/core_extensions/ns_window_controller.rb +29 -0
  37. data/lib/{teacup/z_core_extensions/z_handlers.rb → teacup-osx/core_extensions/teacup_handlers.rb} +30 -47
  38. data/lib/teacup-osx/dummy.rb +80 -0
  39. data/lib/teacup-osx/handler.rb +16 -0
  40. data/lib/teacup-osx/style.rb +83 -0
  41. data/lib/teacup-osx/style_extensions/autoresize.rb +169 -0
  42. data/lib/teacup.rb +12 -11
  43. data/samples/Tweets/Gemfile +4 -0
  44. data/samples/Tweets/Gemfile.lock +16 -0
  45. data/samples/Tweets/README +7 -0
  46. data/samples/Tweets/Rakefile +9 -0
  47. data/samples/Tweets/app/app_delegate.rb +18 -0
  48. data/samples/Tweets/app/data_parser.rb +10 -0
  49. data/samples/Tweets/app/json_parser.rb +12 -0
  50. data/samples/Tweets/app/main_window.rb +99 -0
  51. data/samples/Tweets/app/menu.rb +108 -0
  52. data/samples/Tweets/app/stylesheet.rb +21 -0
  53. data/samples/Tweets/app/tweet.rb +11 -0
  54. data/samples/Tweets/resources/Credits.rtf +29 -0
  55. data/samples/Tweets/spec/main_spec.rb +9 -0
  56. data/samples/teacup-osx/.gitignore +1 -0
  57. data/samples/teacup-osx/Gemfile +4 -0
  58. data/samples/teacup-osx/Gemfile.lock +16 -0
  59. data/samples/teacup-osx/Rakefile +9 -0
  60. data/samples/teacup-osx/app/app_delegate.rb +23 -0
  61. data/samples/teacup-osx/app/controller.rb +11 -0
  62. data/samples/teacup-osx/app/menu.rb +108 -0
  63. data/samples/teacup-osx/app/window.rb +12 -0
  64. data/samples/teacup-osx/resources/Credits.rtf +29 -0
  65. data/samples/teacup-osx/resources/teacup.png +0 -0
  66. data/samples/teacup-osx/spec/main_spec.rb +9 -0
  67. data/spec/ios/appearance_spec.rb +18 -0
  68. data/spec/{calculations_spec.rb → ios/calculations_spec.rb} +0 -0
  69. data/spec/{constraints_spec.rb → ios/constraints_spec.rb} +0 -0
  70. data/spec/{custom_class_spec.rb → ios/custom_class_spec.rb} +0 -0
  71. data/spec/{gradient_spec.rb → ios/gradient_spec.rb} +1 -1
  72. data/spec/ios/layout_module_spec.rb +54 -0
  73. data/spec/ios/layout_spec.rb +50 -0
  74. data/spec/{main_spec.rb → ios/main_spec.rb} +52 -13
  75. data/spec/ios/motion_layout_spec.rb +44 -0
  76. data/spec/{present_modal_spec.rb → ios/present_modal_spec.rb} +0 -0
  77. data/spec/{style_spec.rb → ios/style_spec.rb} +1 -1
  78. data/spec/ios/stylesheet_extensions/autoresize_spec.rb +50 -0
  79. data/spec/{stylesheet_spec.rb → ios/stylesheet_spec.rb} +12 -0
  80. data/spec/{ui_view_getters_spec.rb → ios/ui_view_getters_spec.rb} +0 -0
  81. data/spec/{uiswitch_spec.rb → ios/uiswitch_spec.rb} +0 -0
  82. data/spec/{view_spec.rb → ios/view_spec.rb} +23 -2
  83. metadata +85 -35
  84. data/lib/teacup/stylesheet_extensions/autoresize.rb +0 -39
  85. data/lib/teacup/stylesheet_extensions/rotation.rb +0 -37
  86. data/lib/teacup/z_core_extensions/ui_view.rb +0 -262
  87. data/lib/teacup/z_core_extensions/ui_view_controller.rb +0 -263
  88. data/lib/teacup/z_core_extensions/ui_view_getters.rb +0 -58
data/README.md CHANGED
@@ -1,19 +1,23 @@
1
1
  Teacup
2
2
  ======
3
3
 
4
- A community-driven DSL for creating user interfaces on the iphone.
4
+ A community-driven DSL for creating user interfaces on iOS.
5
5
 
6
6
  [![Build Status](https://travis-ci.org/rubymotion/teacup.png)](https://travis-ci.org/rubymotion/teacup)
7
7
 
8
- Using teacup, you can easily create and style layouts while keeping your code
9
- dry. The goal is to offer a rubyesque (well, actually a rubymotion-esque) way
10
- to create interfaces programmatically.
8
+ Using Teacup, you can create and style layouts and keeping your code dry. The
9
+ goal is to offer a rubyesque (well, actually a rubymotion-esque) way to create
10
+ interfaces programmatically.
11
11
 
12
- **Check out a working sample app [here][Hai]!**
12
+ **Check out some sample apps!**
13
13
 
14
- [Hai]: https://github.com/rubymotion/teacup/tree/master/samples/Hai
14
+ * “[Hai][Hai]”
15
+ * “[AutoLayout][AutoLayout]”
16
+ * “[OnePage][OnePage]”
15
17
 
16
- #### Installation
18
+ [Hai]: https://github.com/rubymotion/teacup/tree/master/samples/Hai
19
+ [AutoLayout]: https://github.com/rubymotion/teacup/tree/master/samples/AutoLayout
20
+ [OnePage]: https://github.com/rubymotion/teacup/tree/master/samples/OnePage
17
21
 
18
22
  **Quick Install**
19
23
 
@@ -25,16 +29,6 @@ and in your Rakefile
25
29
  require 'teacup'
26
30
  ```
27
31
 
28
- **Better Install**
29
-
30
- However, it is recommended that you use [Bundler][] and [rvm][] to manage your
31
- gems on a per-project basis, using a gemset. See the
32
- [Installation wiki page][Installation] for more help on doing that.
33
-
34
- [Bundler]: http://gembundler.com/
35
- [rvm]: https://rvm.io/
36
- [Installation]: https://github.com/rubymotion/teacup/wiki/Installation
37
-
38
32
  #### 10 second primer
39
33
 
40
34
  1. Create a `UIViewController` subclass:
@@ -55,441 +49,1302 @@ gems on a per-project basis, using a gemset. See the
55
49
  stylesheet :main_screen
56
50
 
57
51
  layout do
58
- subview(UIButton, :finished_button)
52
+ subview(UIButton, :hi_button)
59
53
  end
60
54
  end
61
55
  ```
62
- 4. Create the stylesheet (Usually in app/styles/)
56
+ 4. Create the stylesheet (in `app/styles/` or somewhere near the controller)
63
57
 
64
58
  ```ruby
65
59
  Teacup::Stylesheet.new :main_screen do
66
- style :finished_button,
60
+ style :hi_button,
67
61
  origin: [10, 10],
68
62
  title: 'Hi!'
69
63
  end
70
64
  ```
71
65
 
72
- Teacup implements the `viewDidLoad` method and instantiates any views you
73
- declare in the `layout` block. Make sure to call `super` if you implement
74
- `viewDidLoad`, or you can use the "teacup-esque" `layoutDidLoad` method.
75
66
 
76
- #### Styling Without Implicitly Adding the View
77
- In the situation where you'd like to utilize the styles of Teacup without having your view
78
- automatically added in `viewDidLoad`. You can accomplish this by calling the `layout` method instead of the block.
67
+ Teacup
68
+ ------
69
+
70
+ Teacup's goal is to facilitate the creation and styling of your `UIViews`
71
+ hierarchy. Say "Goodbye!" to Xcode & XIB files!
72
+
73
+ Teacup is composed of two systems:
74
+
75
+ - Layouts
76
+ A DSL to create `UIViews` and to organize them in a hierarchy. You assign the
77
+ style name and style classes from these methods.
78
+
79
+ - Stylesheets
80
+ Store the "styles" that get applied to your views. The stylesheet DSL is
81
+ meant to resemble CSS, but is targeted at iOS, and so the precedence rules are
82
+ very different.
83
+
84
+ Teacup supports [Pixate][] and [NUI][], too, so you can use those systems for
85
+ styling and Teacup to manage your view hierarchy and apply auto-layout
86
+ constraints. Teacup can also integrate with the [motion-layout][] gem!
87
+
88
+ ### Table of Contents
89
+
90
+ * [Layouts](#layouts)
91
+ * [Stylesheets](#stylesheets)
92
+ * [Using and re-using styles in a Stylesheet](#using-and-re-using-styles-in-a-stylesheet)
93
+ * [Style via Stylename](#style-via-stylename)
94
+ * [Extending Styles](#extending-styles)
95
+ * [Style via UIView Class](#style-via-uiview-class)
96
+ * [Importing stylesheets](#importing-stylesheets)
97
+ * [Style via UIAppearance](#style-via-uiappearance)
98
+ * [More Teacup features](#more-teacup-features)
99
+ * [Styling View Properties](#styling-view-properties)
100
+ * [Orientation Styles](#orientation-styles)
101
+ * [UIView animation additions](#uiview-animation-additions)
102
+ * [Style Handlers](#style-handlers)
103
+ * [Frame Calculations](#frame-calculations)
104
+ * [Auto-Layout](#auto-layout)
105
+ * [Motion-Layout](#motion-layout)
106
+ * [Stylesheet extensions](#stylesheet-extensions)
107
+ * [Autoresizing Masks](#autoresizing-masks)
108
+ * [Device detection](#device-detection)
109
+ * [Rotation helpers](#rotation-helpers)
110
+ * [Showdown](#showdown)
111
+ * [The Nitty Gritty](#the-nitty-gritty)
112
+ * [Advanced Teacup Tricks](#advanced-teacup-tricks)
113
+ * [UITableViewCell](#uitableviewcell)
114
+ * [Sweettea](#sweettea)
115
+ * [Misc notes](#misc-notes)
116
+ * [The Dummy](#the-dummy)
117
+
118
+ Layouts
119
+ -------
120
+
121
+ The `Teacup::Layout` module is mixed into `UIViewController` and `UIView` so
122
+ that these two classes can take advantage of the view-hierarchy DSL.
123
+
124
+ You saw an example in the primer, using the `UIViewController` class method
125
+ `layout`. This is a helper function that stores the layout code. A more direct
126
+ example might look like this:
79
127
 
80
128
  ```ruby
81
- # Custom Navigation Title still styled by Teacup
82
- @custom_label = layout(UILabel, :custom_title)
83
- self.navigationItem.titleView = @custom_label
129
+ # controller example
130
+ class MyController < UIViewController
131
+
132
+ def viewDidLoad
133
+ # we will modify the controller's `view`, assigning it the stylename `:root`
134
+ layout(self.view, :root) do
135
+ # these subviews will be added to `self.view`
136
+ subview(UIToolbar, :toolbar)
137
+ subview(UIButton, :hi_button)
138
+ end
139
+ end
140
+
141
+ end
84
142
  ```
85
143
 
86
- Additionally, you can use the `style` method on a view to apply your styling inline (e.g. `@label.style(styles)`).
144
+ You can use very similar code in your `UIView` subclasses.
87
145
 
88
- #### Showdown
146
+ ```ruby
147
+ # view example
148
+ #
149
+ # if you use teacup in all your projects, you can bundle your custom views with
150
+ # their own stylesheets
151
+ def MyView < UIView
152
+
153
+ def initWithFrame(frame)
154
+ super.tap do
155
+ self.stylesheet = :my_stylesheet
156
+ subview(UIImageView, :image)
157
+ end
158
+ end
159
+
160
+ end
161
+ ```
89
162
 
90
- Cocoa
163
+ The `layout` and `subview` methods are the work horses of the Teacup view DSL.
164
+
165
+ * `layout(uiview|UIViewClass, stylename, style_classes, additional_styles, &block)`
166
+ - `uiview|UIViewClass` - You can layout an existing class or you can have
167
+ teacup create it for you (it just calls `new` on the class, nothing
168
+ special). This argument is required.
169
+ - `stylename` (`Symbol`) - This is the name of a style in your stylesheet. It
170
+ is optional
171
+ - `style_classes` (`[Symbol,...]`) - Other stylenames, they have lower
172
+ priority than the `stylename`.
173
+ - `additional_styles` (`Hash`) - You can pass other styles in here as well,
174
+ either to override or augment the settings from the `Stylesheet`. It is
175
+ common to use this feature to assign the `delegate` or `dataSource`.
176
+ - `&block` - See discussion below
177
+ - Returns the `uiview` that was created or passed to `layout`.
178
+ - only the `uiview` arg is required. You can pass any combination of
179
+ stylename, style_classes, and additional_styles (some, none, or all).
180
+ * `subview(uiview|UIViewClass, stylename, style_classes, additional_styles, &block)`
181
+ - Identical to `layout`, but adds the view to the current target
182
+
183
+ The reason it is so easy to define view hierarchies in Teacup is because the
184
+ `layout` and `subview` methods can be "nested" by passing a block.
91
185
 
92
186
  ```ruby
93
- class SomeController < UIViewController
187
+ subview(UIView, :container) do # create a UIView instance and give it the stylename :container
188
+ subview(UIView, :inputs) do # create another container
189
+ @email_input = subview(UITextField, :email_input)
190
+ @password_input = subview(UITextField, :password_input)
191
+ end
192
+ subview(UIButton.buttonWithType(UIButtonTypeRoundedRect), :submit_button)
193
+ end
194
+ ```
94
195
 
95
- def viewDidLoad
196
+ These methods are defined in the `Layout` module. And guess what!? It's easy
197
+ to add your *own view helpers*! I refer to this as a "partials" system, but
198
+ really it's just Ruby code (and isn't that the best system?).
96
199
 
97
- @field = UITextField.new
98
- @field.frame = [[10, 10], [200, 50]]
99
- @field.textColor = UIColor.redColor
100
- view.addSubview(@field)
200
+ ```ruby
201
+ # the methods you add here will be available in UIView, UIViewController, and
202
+ # any of your own classes that `include Teacup::Layout`
203
+ module Teacup::Layout
101
204
 
102
- @search = UITextField.new
103
- @search.frame = [[10, 70], [200, 50]]
104
- @search.placeholder = 'Find something...'
105
- @search.textColor = UIColor.redColor
106
- view.addSubview(@search)
205
+ # creates a button and assigns a default stylename
206
+ def button(*args, &block)
207
+ # apply a default stylename
208
+ args = [:button] if args.empty?
107
209
 
108
- true
210
+ # instantiate a button and give it a style class
211
+ subview(UIButton.buttonWithType(UIButtonTypeCustom), *args, &block)
109
212
  end
110
213
 
111
- # code to enable orientation changes
112
- def shouldAutorotateToInterfaceOrientation(orientation)
113
- if orientation == UIInterfaceOrientationPortraitUpsideDown
114
- return false
115
- end
116
- true
214
+ # creates a button with an icon image and label
215
+ def button_with_icon(icon, title)
216
+ label = UILabel.new
217
+ label.text = title
218
+ label.sizeToFit
219
+
220
+ image_view = UIImageView.new
221
+ image_view.image = icon
222
+ image_view.sizeToFit
223
+
224
+ button = UIButton.buttonWithType(UIButtonTypeCustom)
225
+ button.addSubview(image_view)
226
+ button.addSubview(label)
227
+
228
+ # code could go here to position the icon and label, or at could be handled
229
+ # by the stylesheet
230
+
231
+ subview(button)
117
232
  end
118
233
 
119
- # perform the frame changes depending on orientation
120
- def willAnimateRotationToInterfaceOrientation(orientation, duration:duration)
121
- case orientation
122
- when UIInterfaceOrientationLandscapeLeft, UIInterfaceOrientationLandscapeRight
123
- @field.frame = [[10, 10], [360, 50]]
124
- @search.frame = [[10, 70], [360, 50]]
125
- else
126
- @field.frame = [[10, 10], [200, 50]]
127
- @search.frame = [[10, 70], [200, 50]]
128
- end
234
+ end
235
+ ```
236
+ ###### example use of the helper methods
237
+ ```
238
+ class MyController < UIViewController
239
+
240
+ layout do
241
+ @button1 = button()
242
+ @button2 = button(:blue_button)
243
+ @button3 = button_with_icon(UIImage.imageNamed('email_icon'), 'Email')
129
244
  end
130
245
 
131
246
  end
132
247
  ```
133
248
 
134
- Teacup
249
+ The `UIViewController##layout` method that has been used so far is going to be
250
+ the first or second thing you add to a controller when you are building an app
251
+ with Teacup. It's method signature is
135
252
 
136
253
  ```ruby
137
- # Stylesheet
254
+ UIViewController.layout(stylename=nil, styles={}, &block)
255
+ ```
138
256
 
139
- Teacup::Stylesheet.new(:some_view) do
257
+ * `stylename` is the stylename you want applied to your controller's `self.view`
258
+ object.
259
+ * `styles` are *rarely* applied here, but one common use case is when you assign
260
+ a custom view in `loadView`, and you want to apply settings to it. I find it
261
+ cleaner to move this code into the body of the `layout` block, though.
262
+ * `&block` is the most important - it is the layout code that will be called
263
+ during `viewDidLoad`.
264
+
265
+ After the views have been added and styles have been applied Teacup calls the
266
+ `layoutDidLoad` method. If you need to perform some additional initialization
267
+ on your views, you should do it in this method. If you use the `layout` block
268
+ the styles have not yet been applied. Frames will not be set, text and titles
269
+ will be empty, and images will not have images. This all happens at the *end*
270
+ of the `layout` block.
140
271
 
141
- style :root,
142
- landscape: true # enable landscape rotation (otherwise only portrait is enabled)
143
- # this must be on the root-view, to indicate that this view is
144
- # capable of handling rotations
272
+ Stylesheets
273
+ -----------
145
274
 
146
- style :field,
147
- left: 10,
148
- top: 10,
149
- width: 200,
150
- height: 50,
151
- landscape: {
152
- width: 360 # make it wide in landscape view
153
- }
275
+ This is where you will store your styling-related code. Migrating code from your
276
+ controller or custom view into a stylesheet is very straightforward. The method
277
+ names map 1::1.
154
278
 
155
- style :search, extends: :field,
156
- left: 10,
157
- top: 70,
158
- placeholder: 'Find something...'
279
+ ```ruby
280
+ # classic Cocoa/UIKit
281
+ def viewDidLoad
282
+ self.view.backgroundColor = UIColor.grayColor
283
+ # ^.............^
284
+ end
159
285
 
160
- style UITextField, # Defining styles based on view class instead
161
- textColor: UIColor.redColor # of style name.
286
+ # in Teacup
287
+ def viewDidLoad
288
+ self.stylesheet = :main
289
+ self.view.stylename = :root
290
+ end
162
291
 
292
+ Teacup::Stylesheet.new :main do
293
+ style :root,
294
+ backgroundColor: UIColor.grayColor
295
+ # ^.............^
163
296
  end
297
+ ```
164
298
 
165
- # Controller
299
+ Nice! We turned three lines of code into nine! Well, obviously the benefits
300
+ come in when we have *lots* of style code, and when you need to do app-wide
301
+ styling.
166
302
 
167
- class SomeController < UIViewController
303
+ You can store stylesheets in any file. It is common to use `app/styles.rb` or
304
+ `app/styles/main.rb`, if you have more than a few of 'em. The
305
+ `Teacup::Stylesheet` constructor accepts a stylesheet name and a block, which
306
+ will contain your style declarations.
168
307
 
169
- # the stylesheet determines the placement and design of your views. You can
170
- # also implement a stylesheet method, or assign the stylesheet name to the
171
- # UIViewController later.
172
- stylesheet :some_view
308
+ ```ruby
309
+ Teacup::Stylesheet.new :main_menu do
310
+ style :ready_to_play_button,
311
+ backgroundColor: UIColor.blackColor,
312
+ frame: [[20, 300], [50, 20]]
313
+ end
173
314
 
174
- # think of this as a nib file that you are declaring in your UIViewController.
175
- # it is styled according to the :root styles, and can add and style subviews
176
- layout :root do
177
- subview(UITextField, :field)
178
- @search = subview(UITextField, :search)
179
- end
315
+ Teacup::Stylesheet[:main_menu] # returns this stylesheet
316
+ ```
317
+
318
+ Any method that accepts a single value can be assigned in a stylesheet. Please
319
+ don't abuse this by hiding application logic in your stylesheets - these are
320
+ meant for *design*, not behavior.
321
+
322
+ ### Using and re-using styles in a Stylesheet
180
323
 
181
- # you have to enable the auto-rotation stuff by implementing a
182
- # shouldAutorotateToInterfaceOrientation method
183
- def shouldAutorotateToInterfaceOrientation(orientation)
184
- # but don't worry, we made that painless, too!
185
- autorotateToOrientation(orientation)
324
+ - Styles are be applied via stylename (`style :label`) or class (`style UILabel`)
325
+ - Styles can extend other styles (`style :big_button, extends: :button`)
326
+ - A stylesheet can import other stylesheets (`import :app`)
327
+ - The special Appearance stylesheet can be used to apply styles to `UIAppearance`
328
+ (`Teacup::Appearance.new`)
329
+
330
+ Let's look at each in turn.
331
+
332
+ ### Style via Stylename
333
+
334
+ This is the most common way to apply a style.
335
+
336
+ ```ruby
337
+ class MainController < UIViewController
338
+
339
+ stylesheet :main # <= assigns the stylesheet named :main to this controller
340
+
341
+ layout do
342
+ subview(UILabel, :h1) # <= :h1 is the stylename
186
343
  end
187
344
 
345
+ end
346
+
347
+ Teacup::Stylesheet.new :main do # <= stylesheet name
348
+
349
+ style :h1, # <= style name
350
+ font: UIFont.systemFontOfSize(20) # <= and this style is applied
351
+
188
352
  end
189
353
  ```
190
354
 
191
- The orientation styling is really neat. I think you'll find that you will be
192
- more inspired to enable multiple orientations because the code is so much more
193
- painless.
355
+ When the stylesheet is applied (at the end of the `layout` block, when all the
356
+ views have been added), its `font` property will be assigned the value
357
+ `UIFont.systemFontOfSize(20)`.
194
358
 
195
- Stylesheets
196
- -----------
359
+ But we didn't assign any text!
197
360
 
198
- The basic format for a `style` is a name and a dictionary of "styles". These
199
- are usually just methods that get called on the target (a `UIView` or `CALayer`,
200
- most likely), but they can also perform introspection, using "handlers".
361
+ We can tackle this a couple ways. You can apply "last-minute" styles in the
362
+ `layout` and `subview` methods:
201
363
 
202
- Basics
203
- ======
364
+ ```ruby
365
+ layout do
366
+ subview(UILabel, :h1,
367
+ # the `subview` and `layout` methods can apply styles
368
+ text: "Omg, it's full of stars"
369
+ )
370
+ end
371
+ ```
204
372
 
205
- Create a stylesheet in any code file, usually `styles.rb` or `styles/main.rb`,
206
- if you have a ton of 'em. The `Teacup::Stylesheet` constructor accepts a
207
- stylesheet name and a block, which will contain your style declarations.
373
+ In this case though we just have static text, so you can assign the text using
374
+ the stylesheet:
208
375
 
209
376
  ```ruby
210
- Teacup::Stylesheet.new :main_menu do
211
- style :ready_to_play_button,
212
- backgroundColor: UIColor.blackColor,
213
- frame: [[20, 300], [50, 20]] # [[x, y], [w, h]]
377
+ Teacup::Stylesheet.new :main do
378
+
379
+ style :h1,
380
+ font: UIFont.systemFontOfSize(20)
381
+
382
+ style :main_header,
383
+ text: "Omg, it's full of stars",
384
+ font: UIFont.systemFontOfSize(20)
385
+
214
386
  end
215
387
  ```
216
388
 
217
- Any method that accepts a single value can be assigned here. Please don't abuse
218
- this by hiding application logic in your stylesheets - these are meant for
219
- *design*, not behavior. That said, if you're coding by yourself - go for it! ;)
389
+ ### Extending Styles
220
390
 
221
- Stylesheets
222
- ===========
391
+ Not very DRY though is it!? We have to use a new style (`:main_header`) because
392
+ not all our labels say "OMG", but we want to use our font from the `:h1` style.
393
+ We can tell the `:main_header` style that it `extends` the `:h1` style:
394
+
395
+ ```ruby
396
+ layout do
397
+ subview(UILabel, :main_header)
398
+ end
399
+
400
+ Teacup::Stylesheet.new :main do
401
+
402
+ style :h1,
403
+ font: UIFont.systemFontOfSize(20)
404
+
405
+ style :main_header, extends: :h1,
406
+ text: "Omg, it's full of stars"
407
+
408
+ end
409
+ ```
410
+
411
+ A common style when writing stylesheets is to use instance variables to store
412
+ settings you want to tweak.
413
+
414
+ ```ruby
415
+ Teacup::Stylesheet.new :main do
416
+ @hi_font = UIFont.systemFontOfSize(20)
223
417
 
224
- The `Teacup::Stylesheet` class has methods that create a micro-DSL in the
225
- context of a `style` declaration. You can view the source (most of them are
226
- very short methods) in `lib/teacup/stylesheet_extensions/*.rb`.
418
+ style :h1,
419
+ font: @hi_font
420
+ style :main_header, extends: :h1,
421
+ text: "Omg, it's full of stars"
422
+ end
423
+ ```
424
+
425
+ ### Style via UIView Class
426
+
427
+ If you need to apply styles to *all* instances of a `UIView` subclass, you can
428
+ do so by applying styles to a class name instead of a symbol. This feature is
429
+ handy at times when you might otherwise use `UIAppearance` (which teacup also
430
+ supports!).
227
431
 
228
432
  ```ruby
229
- # autoresizingMask
230
- style :spinner,
231
- center: [320, 460],
232
- autoresizingMask: flexible_left|flexible_right|flexible_top|flexible_bottom
433
+ Teacup::Stylesheet.new :app do
233
434
 
435
+ style UILabel,
436
+ font: UIFont.systemFontOfSize(20)
234
437
 
235
- # device-specific geometries - app_size, screen_size, and device/device? methods
236
- style :root,
237
- origin: [0, 0],
238
- size: app_size # doesn't include the status bar - screen_size does
438
+ style UITableView,
439
+ backgroundColor: UIColor.blackColor
239
440
 
240
- if device? iPhone
241
- style :image, image: UIImage.imageNamed "my iphone image"
242
- elsif device? iPad
243
- style :image, image: UIImage.imageNamed "my ipad image"
244
441
  end
442
+ ```
443
+
444
+ ### Importing stylesheets
445
+
446
+ We've touched on the ability to write styles, extend styles, and apply styles to
447
+ a class. Now we can introduce another feature that is even more useful for
448
+ applying styles to your entire app: import a stylesheet.
449
+
450
+ When you import a stylesheet, you receive all of its `style`s *and* you gain
451
+ access to its instance variables. This way you can define colors and margins and
452
+ such in a "parent" stylesheet.
453
+
454
+ ```ruby
455
+ Teacup::Stylesheet.new :app do
456
+
457
+ @header_color = UIColor.colorWithRed(7/255.0, green:16/255.0, blue:95/255.0, alpha: 1)
458
+ @background_color = UIColor.colorWithRed(216/255.0, green:226/255.0, blue:189/255.0, alpha: 1)
459
+
460
+ style :root,
461
+ backgroundColor: @background_color
462
+
463
+ style :header,
464
+ textColor: @header_color
245
465
 
246
- # the device method can be OR'd with the devices. The return value is always a
247
- # number, so make sure to compare to 0
248
- if device|iPhone > 0
249
- # ...
250
466
  end
251
- # so yeah, you might as well use `device? iPhone`, in my opinion.
252
467
 
468
+ Teacup::Stylesheet.new :main do
469
+ import :app
253
470
 
254
- # rotations - use the identity and pi methods
255
- style :button,
256
- layer: {
257
- transform: spin identity, 2*pi
258
- }
471
+ style :subheader, extends: :header # <= the :header style is imported from the :app stylesheet
472
+
473
+ style :button,
474
+ titleColor: @header_color # <= @header_color is imported, too
475
+ end
259
476
  ```
260
477
 
261
- Orientations
262
- ============
478
+ ### Style via UIAppearance
263
479
 
264
- Teacup stylesheets can be given orientation hashes. The supported orientations
265
- are:
480
+ And lastly, the `UIAppearance protocol` is supported by creating an instance of
481
+ `Teacup::Appearance`. There is debatable benefit to using [UIAppearance][],
482
+ because it will apply styles to views that are outside your control, like the
483
+ camera/image pickers and email/message controllers. Using `import` instead will
484
+ not apply styles to those views.
485
+
486
+ But, it does come in handy sometimes... so here it is!
487
+
488
+ ```ruby
489
+ Teacup::Appearance.new do
266
490
 
267
- - `portrait` - upright or upside down
268
- - `upside_up`
269
- - `upside_down`
270
- - `landscape` - on either side
271
- - `landscape_left` - "left" refers to the home button, e.g. the button is on the left.
272
- - `landscape_right` - home button is on the right
491
+ # UINavigationBar.appearance.setTintColor(UIColor.blackColor)
492
+ style UINavigationBar,
493
+ tintColor: UIColor.blackColor
273
494
 
274
- An example should suffice:
495
+ # UINavigationBar.appearanceWhenContainedIn(UINavigationBar, nil).setTintColor(UIColor.blackColor)
496
+ style UIBarButtonItem, when_contained_in: UINavigationBar,
497
+ tintColor: UIColor.blackColor
498
+
499
+ # UINavigationBar.appearanceWhenContainedIn(UIToolbar, UIPopoverController, nil).setTintColor(UIColor.blackColor)
500
+ style UIBarButtonItem, when_contained_in: [UIToolbar, UIPopoverController],
501
+ tintColor: UIColor.blackColor
502
+
503
+ end
504
+ ```
505
+
506
+ That block is called using the `UIApplicationDidFinishLaunchingNotification`,
507
+ but that notification is not called until the *end* of the
508
+ `application(application,didFinishLaunchingWithOptions:launchOptions)` method.
509
+ This is sometimes after your views have been created, and so they will not be
510
+ styled. If that is the case, call `Teacup::Appearance.apply` before creating
511
+ your `rootViewController`.
512
+
513
+ ### Now go use Teacup!
514
+
515
+ You have enough information *right now* to go play with Teacup. Check out the
516
+ example apps, write your own, whatever. But read on to hear about why Teacup is
517
+ more than just writing `layouts` and applying styles.
518
+
519
+ Teacup as a utility
520
+ -------------------
521
+
522
+ When you are prototyping an app it is useful to bang out a bunch of code
523
+ quickly, and here are some ways that Teacup might help.
524
+
525
+ You can use all the methods above without having to rely on the entirety of
526
+ Teacup's layout and stylesheet systems. By that I mean *any* time you are
527
+ creating a view hierarchy don't be shy about using Teacup to do it.
528
+
529
+ `UIView` has the `style` method, which can be used to group a bunch of
530
+ customizations anywhere in your code. You don't *have* to pull out a stylesheet
531
+ to do it.
275
532
 
276
533
  ```ruby
277
- style :ready_to_play_button,
278
- portrait: {
279
- frame: [[20, 300], [50, 20]]
280
- },
281
- landscape: {
282
- frame: [[60, 300], [50, 20]] # button moves over 40 pixels because of the wider screen
283
- }
534
+ # Custom Navigation Title created and styled by Teacup
535
+ self.navigationItem.titleView = layout(UILabel,
536
+ text:'Title',
537
+ font: UIFont.systemFontOfSize(12),
538
+ )
539
+
540
+ # Customize contentView in a UITableViewCell dataSource method
541
+ def tableView(table_view, cellForRowAtIndexPath:index_path)
542
+ cell_identifier = 'MyController - cell'
543
+ cell = table_view.dequeueReusableCellWithIdentifier(cell_identifier)
544
+
545
+ unless cell
546
+ cell = UITableViewCell.alloc.initWithStyle(UITableViewCellStyleDefault,
547
+ reuseIdentifier: cell_identifier)
548
+ layout(cell.contentView) do
549
+ subview(UIImageView, :image)
550
+ end
551
+ end
552
+
553
+ return cell
554
+ end
555
+
556
+ # Use the `style` method on a view to apply your styling. This is a one-shot
557
+ # styling.
558
+ @label.style(textColor: UIColor.blueColor, text: 'Blue Label')
284
559
  ```
285
560
 
286
- That code is repetive, though, let's shorten it up by using precedence and some
287
- aliases for setting the `frame`:
561
+ More Teacup features
562
+ --------------------
563
+
564
+ There are a few (OK, a bunch) more features that Teacup provides that deserve
565
+ discussion:
566
+
567
+ - Styling View Properties
568
+ - Orientation Styles
569
+ - UIView Additions
570
+ - Style Handlers
571
+ - Frame Calculations
572
+ - Auto-Layout & [Motion-Layout][motion-layout]
573
+ - Stylesheet Extensions
574
+
575
+ ### Styling View Properties
576
+
577
+ Styling a UIView is fun, but a UIView is often composed of many objects, like
578
+ the `layer`, or maybe an `imageView` or `textLabel` and so on. You can style
579
+ those, too!
288
580
 
289
581
  ```ruby
290
- style :ready_to_play_button,
291
- top: 300,
292
- width: 50,
293
- height: 20,
294
- portrait: {
295
- left: 20
582
+ # UITableViewCells have a contentView, a backgroundView, imageView, textLabel,
583
+ # detailTextLabel, and a layer! whew!
584
+ style :tablecell,
585
+ layer: { # style the layer!
586
+ shadowRadius: 3
296
587
  },
297
- landscape: {
298
- left: 60
588
+ backgroundView: { # style the background!
589
+ backgroundColor: UIColor.blackColor
590
+ },
591
+ imageView: { # style the imageView!
592
+ contentMode: UIViewContentModeScaleAspectFill
299
593
  }
300
594
  ```
301
595
 
302
- Styles declared in an orientation hash will override the "generic" styles
303
- declared directly above it, so the above could also be written as:
596
+ ### Orientation Styles
597
+
598
+ There's more to stylesheets than just translating `UIView` setters. Teacup can
599
+ also apply orientation-specific styles. These are applied when the view is
600
+ created (using the current device orientation) and when a rotation occurs.
304
601
 
305
602
  ```ruby
306
- style :ready_to_play_button,
307
- top: 300,
308
- width: 50,
309
- height: 20,
310
- left: 20
311
- landscape: {
312
- left: 60 # overrides left: 20
313
- }
603
+ Teacup::Stylesheet.new :main do
604
+
605
+ # this label hides when the orientation is landscape (left or right)
606
+ style :label,
607
+ landscape: {
608
+ hidden: true
609
+ },
610
+ portrait: {
611
+ hidden: false
612
+ }
613
+
614
+ end
314
615
  ```
315
616
 
316
- Handlers
317
- ========
617
+ Combine these styles with [Frame Calculations][calculations] to have you view
618
+ frame recalculated automatically.
619
+
620
+ ### UIView animation additions
621
+
622
+ We've already seen the Teacup related properties:
623
+
624
+ - `stylename`, the primary style name
625
+ - `style_classes`, secondary style names
626
+ - `style`, apply styles directly
627
+
628
+ Each of these has a corresponding method that you can use to facilitate
629
+ animations.
630
+
631
+ - `animate_to_stylename(stylename)`
632
+ - `animate_to_styles(style_classes)`
633
+ - `animate_to_style(properties)`
634
+
635
+ ### Style Handlers
636
+
637
+ *This feature is used extensively by [sweettea][] to make a more intuitive
638
+ stylesheet DSL*
318
639
 
319
- Above, we saw that we can assign `view.frame.x` by using the `left` property.
320
- There *is* no `UIView#left` method, so this must be handled somewhere special...
640
+ Teacup is, by itself, pretty useful, but it really does little more than map
641
+ Hash keys to `UIView` setters. That's great, because it keeps the system easy
642
+ to understand. But there are some methods in UIKit that take more than one
643
+ argument, or could benefit from some shorthands.
321
644
 
322
- Not **that** special, it turns out. This used to be an internal translation,
323
- but the list of "translations" was getting out of hand, and we realized that we
324
- could break this out into a new feature. **Handlers**. Here is the `handler`
325
- for the `left` property:
645
+ This is where Teacup's style handlers come in. They are matched against a
646
+ `UIView` subclass and one or more stylenames, and they are used to apply that
647
+ style when you use it in your stylesheet.
326
648
 
327
649
  ```ruby
328
- Teacup.handler UIView, :left { |view, x|
329
- f = view.frame
330
- f.origin.x = x
331
- view.frame = f
332
- }
650
+ # this handler adds a `:title` handler to the UIButton class (and subclasses).
651
+ Teacup.handler UIButton, :title do |target, title|
652
+ target.setTitle(title, forState: UIControlStateNormal)
653
+ end
654
+
655
+ # ...
656
+ subview(UIButton,
657
+ title: 'This is the title' # <= this will end up being passed to the handler above
658
+ )
659
+
660
+ layout(UINavigationItem,
661
+ title: 'This is the title' # <= but this will not! the handler above is restricted to UIButton subclasses
662
+ )
333
663
  ```
334
664
 
335
- How about setting the title of a `UIButton`?
665
+ [Other built-in handlers][other-handlers] are defined in `z_handlers.rb`.
666
+ Another useful one is the ability to make view the same size as its parent, and
667
+ located at the origin.
336
668
 
337
669
  ```ruby
338
- Teacup.handler UIButton, :title { |target, title|
339
- target.setTitle(title, forState: UIControlStateNormal)
340
- }
670
+ style :container,
671
+ frame: :full # => [[0, 0], superview.frame.size]
341
672
  ```
342
673
 
343
- You can also make aliases for long method names, or to shorten a style you use a
344
- lot. You can alias two ways - give multiple names in a `Teacup.handler` method,
345
- or use `Teacup.alias`.
674
+ ### Frame Calculations
675
+
676
+ *These are super cool, just don't forget your autoresizingMasks*
677
+
678
+ When positioning views you will often have situations where you want to have a
679
+ view centered, or 8 pixels to the right of center, or full width/height. All of
680
+ these relationships can be described using the `Teacup.calculate` method, which
681
+ is called automatically in any method that modifies the `frame` or `center`.
682
+
683
+ frame, origin, size
684
+ top/y, left/x, right, bottom, width, height
685
+ center_x/middle_x, center_y/middle_y, center
346
686
 
347
687
  ```ruby
348
- # the actual `left` handler offers an alias `x`
349
- Teacup, handler UIView, :left, :x { |view, x|
350
- f = view.frame
351
- f.origin.x = x
352
- view.frame = f
353
- }
688
+ Teacup::Stylesheet.new :main do
689
+
690
+ style :button,
691
+ left: 8, top: 8, # easy enough!
692
+ width: '100% - 16', # woah! (O_o)
693
+ height: 22
694
+
695
+ style :top_half,
696
+ frame: [[0, 0], ['100%', '50%']]
697
+ style :bottom_half,
698
+ frame: [[0, '50%'], ['100%', '50%']]
354
699
 
355
- # but I speak japanese, and I want it to be called "hidari" instead, and I want
356
- # top to be "ue".
357
- Teacup.alias UIView, :hidari => :left, :ue => :top
700
+ end
358
701
  ```
359
702
 
360
- extends:
361
- =======
703
+ When this code executes, the string `'100% - 16'` is translated into the formula
704
+ `1.00 * target.superview.frame.size.width - 16`. If the property is related to
705
+ the height or y-position, it will be calculated based on the height.
362
706
 
363
- You might have a view where all the buttons or text fields have similar colors
364
- or font. You can have them extend a common style declaration.
707
+ The frame calculations must be a string of the form `/[0-9]+% [+-] [0-9]+/`. If
708
+ you need more "math-y-ness" than that, you can construct strings using interpolation.
365
709
 
366
710
  ```ruby
711
+ margin = 8
712
+
367
713
  style :button,
368
- font: UIFont.systemFontOfSize(20)
714
+ left: margin, top: margin,
715
+ width: "100% - #{margin * 2}",
716
+ height: 22
717
+
718
+ # just for fun, let's see what it would take to add a margin between these two views.
719
+ style :top_half,
720
+ frame: [[0, 0], ['100%', "50% - #{margin / 2}"]]
721
+ style :bottom_half,
722
+ frame: [[0, "50% + #{margin / 2}"], ['100%', "50% - #{margin / 2}"]]
723
+ ```
369
724
 
370
- style :ok_button, extends: :button,
371
- title: "OK"
725
+ One more example: The real power of the frame calculations comes when you
726
+ remember to set springs and struts. You can have a view "pinned" to the bottom
727
+ if you remember to set the `autoresizingMask`.
372
728
 
373
- style :cancel_button, extends: :button,
374
- title: "Cancel"
729
+ ```ruby
730
+ Teacup::Stylesheet.new :main do
731
+
732
+ style :button,
733
+ # fixed width / height
734
+ height: 22, width: 200,
735
+ center_x: '50%',
736
+ top: '100% - 30', # includes an 8px margin from the bottom
737
+ autoresizingMask: (UIViewAutoresizingFlexibleLeftMargin |
738
+ UIViewAutoresizingFlexibleRightMargin |
739
+ UIViewAutoresizingFlexibleTopMargin)
740
+ # see the autoresizing extension below for an even better way to write this.
741
+ end
375
742
  ```
376
743
 
377
- Precedence is important. We said that "orientation" overrides "generic", but
378
- that is only at the local `style` declaration level. If you declare a property
379
- in `style` that is set in an orientation hash in an extended style, *your*
380
- property will win.
744
+ ### Auto-Layout
745
+
746
+ *This is another much bigger topic than it is given space for here*
747
+
748
+ Teacup includes an Auto-Layout constraint DSL that you can use in your
749
+ stylesheets. These methods are added to the `Stylesheet` class, so unless you
750
+ are in the context of a stylesheet, you will have to create your constraints in
751
+ longhand (you can still use the `Teacup::Constraint` class to help you!).
752
+
753
+ I won't sugar-coat it: Auto-Layout is hard. Much harder than using frames and
754
+ springs and struts. And honestly, I recommend you try using the
755
+ `Teacup.calculate` features mentioned above, they will take you far.
756
+
757
+ But at the end of the day, once you really understand the auto-layout system
758
+ that Apple released in iOS 6, you can build your UIs to be responsive to
759
+ different devices, orientations, and sizes. UIs built with auto-layout do not
760
+ usually need to adjust anything during a rotation. The constraints take *care*
761
+ of it all. It's impressive.
762
+
763
+ Here's a quick example that creates this shape. The edges are bound to the
764
+ superview's frame.
765
+
766
+ +-----+----------------+
767
+ | | |
768
+ | A | B |
769
+ | | +-----| <\
770
+ | | | C | |_ 50% of B's height, minus 10 pixels
771
+ +-----+----------+-----+ </
772
+ ^--+--^ ^--+--^
773
+ |_fixed (100) |_fixed (100)
381
774
 
382
775
  ```ruby
383
- style :button,
384
- portrait: {
385
- width: 40
386
- },
387
- landscape: {
388
- width: 45
389
- }
776
+ Teacup::Stylesheet.new do
777
+ style :A,
778
+ constraints: [
779
+ # these first three are all fixed, so super easy
780
+ constrain_left(0),
781
+ constrain_width(100),
782
+ constrain_top(0),
783
+ # here we go, here's a real constraint
784
+ constrain(:bottom).equals(:superview, :bottom),
785
+ ]
786
+
787
+ style :B,
788
+ constraints: [
789
+ # B.left == A.right
790
+ constrain(:left).equals(:A, :right),
791
+ # B.height == A.height
792
+ constrain(:height).equals(:A, :height),
793
+ constrain(:right).equals(:superview, :right),
794
+ ]
795
+
796
+ style :C, # <= this looks like a very grumpy style :C
797
+ constraints: [
798
+ constrain_width(100),
799
+ # pin to bottom-right corner
800
+ constrain(:right).equals(:superview, :right),
801
+ constrain(:bottom).equals(:superview, :bottom),
802
+ # 50% B.height - 10
803
+ constrain(:height).equals(:B, :height).times(0.5).minus(10),
804
+ ]
805
+
806
+ end
807
+ ```
808
+
809
+ Writing views this way will either make your brain hurt, or make the math-nerd
810
+ in you chuckle with glee. In this example you could go completely with just
811
+ frame calculation formulas and springs and struts. Your frame code would still
812
+ be cluttered, just cluttered in a different way.
813
+
814
+ ### Motion-Layout[motion-layout]
390
815
 
391
- style :ok_button, extends: :button,
392
- title: "OK",
393
- width: 40 # width will always be 40, even in landscape
816
+ If you are using [Nick Quaranto][qrush]'s [motion-layout][] gem, you can use it from within
817
+ any class that includes `Teacup::Layout`. Then benefit is that the Teacup
818
+ stylenames assigned to your views will be used in the dictionary that the
819
+ ASCII-based system relies on.
820
+
821
+ ```ruby
822
+ layout do
823
+ subview(UIView, :view_a)
824
+ subview(UIView, :view_b)
825
+ subview(UIView, :view_c)
826
+
827
+ # if you need to apply these to a different view, or if you want to assign
828
+ # different names to use in the ASCII strings
829
+ # auto(layout_view=self.view, layout_subviews={}, &layout_block)
830
+
831
+ auto do
832
+ metrics 'margin' => 20
833
+ vertical "|-[view_a]-margin-[view_b]-margin-[view_c]-|"
834
+ horizontal "|-margin-[view_a]-margin-|"
835
+ horizontal "|-margin-[view_b]-margin-|"
836
+ horizontal "|-margin-[view_c]-margin-|"
837
+ end
838
+ end
394
839
 
395
- style :cancel_button, extends: :button,
396
- title: "Cancel"
397
- # width will be 40 or 45, depending on orientation
398
840
  ```
399
841
 
400
- import
401
- ======
842
+ ### Stylesheet extensions
402
843
 
403
- Each `UIView` or `UIViewController` can have only one stylesheet attached to it.
404
- If you want to break up a stylesheet into multiple sheets, you will use `import`
405
- to do it.
844
+ Auto-Layout is just one Stylesheet extension, there are a few others. And if
845
+ you want to write your own, just open up the `Teacup::Stylesheet` class and
846
+ start adding methods.
847
+
848
+ #### Autoresizing Masks
849
+
850
+ If you've used the SugarCube `uiautoresizingmask` methods, you'll recognize
851
+ these. They are handy, and hopefully intuitive, shorthands for common springs
852
+ and struts.
853
+
854
+ In previous versions of Teacup these were available without needing the
855
+ `autoresize` prefix. The old methods are still available, but deprecated.
406
856
 
407
857
  ```ruby
408
- Teacup::Stylesheet.new :base do
409
- style :button,
410
- font: UIFont.systemFontOfSize(20)
858
+ # keeps the width and height in proportion to the parent view
859
+ style :container,
860
+ autoresizingMask: autoresize.flexible_width | autoresize.flexible_height
861
+
862
+ # the same, but using block syntax
863
+ style :container,
864
+ autoresizingMask: autoresize { flexible_width | flexible_height }
865
+
866
+ # the same again, using a shorthand
867
+ style :container,
868
+ autoresizingMask: autoresize.fill
869
+ ```
870
+
871
+ The autoresize methods are grouped into four categories: `flexible, fill, fixed,
872
+ and float`. The flexible methods correspond 1::1 with the `UIViewAutoresizing*`
873
+ constants.
874
+
875
+ The `fill` methods (`fill,fill_top,fill_bottom,fill_left,fill_right`) will
876
+ stretch the width, or height, or both. The location specifies where the view is
877
+ pinned, so `fill_top` will stretch the width and bottom margin, but keep it the
878
+ same distance from the top (not necessarily *at* the top, but a fixed distance).
879
+ `fill_right` will pin it to the right side, stretch the height, and have a
880
+ flexible left margin.
881
+
882
+ The `fixed` methods pin the view to one of nine locations:
883
+
884
+ top_left | top_middle | top_right
885
+ ------------+---------------+-------------
886
+ middle_left | middle | middle_right
887
+ ------------+---------------+-------------
888
+ bottom_left | bottom_middle | bottom_right
889
+
890
+ The `float` methods fill in the last gap, when you don't want your view pinned
891
+ to any corner, and you don't want it to change size.
892
+
893
+ float_horizontal | float_vertical == fixed_middle
894
+
895
+ #### Device detection
896
+
897
+ Because the stylesheets are defined in a block, you can perform tests for device
898
+ and screen size before setting styles. For instance, on an ipad you might want
899
+ to have a larger margin than on the iphone.
900
+
901
+ The `Stylesheet` device methods will help you create these conditions:
902
+
903
+ ```ruby
904
+ Teacup::Stylesheet.new do
905
+ if device_is? iPhone
906
+ margin = 8
907
+ elsif device_is? iPad
908
+ margin = 20
909
+ end
910
+
911
+ style :container,
912
+ frame: [[margin, margin], ["100% - #{margin * 2}", "100% * #{margin * 2}"]]
411
913
  end
914
+ ```
412
915
 
413
- Teacup::Stylesheet.new :main do
414
- import :base
916
+ Multiple calls to `style` will *add* those styles, not replace. So this code
917
+ works just fine:
415
918
 
416
- style :ok_button, extends: :button,
417
- top: 10
919
+ ```ruby
920
+ Teacup::Stylesheet.new do
921
+ style :logo,
922
+ origin: [0, 0]
923
+
924
+ if device_is? iPhone
925
+ style :logo, image: UIImage.imageNamed "small logo"
926
+ elsif device_is? iPad
927
+ style :logo, image: UIImage.imageNamed "big logo"
928
+ end
418
929
  end
930
+ ```
931
+
932
+ #### Rotation helpers
419
933
 
420
- Teacup::Stylesheet.new :register do
421
- import :base
934
+ Because you can animate changes to the stylename or style_classes, you can make
935
+ it pretty easy to apply rotation effects to a `UIView` or `CALayer`. The
936
+ `style_classes` property is especially useful for this purpose.
937
+
938
+ ```ruby
939
+ style :container,
940
+ frame: :full
941
+
942
+ # UIView transforms
943
+
944
+ style :rotated,
945
+ transform: transform_view.rotate(pi / 2) # pi and transform_view are methods on Stylesheet
946
+
947
+ style :not_rotated,
948
+ transform: transform_view.rotate(0)
949
+
950
+ # CALayer transforms
951
+
952
+ style :rotated,
953
+ layer: { transform: transform_layer.rotate(pi / 2) }
954
+
955
+ style :not_rotated,
956
+ layer: { transform: transform_layer.rotate(0) }
957
+ ```
958
+
959
+ These work even better when used with the [geomotion][] methods that extend
960
+ `CGAffineTransform` and `CATransform3D`.
961
+
962
+ ```ruby
963
+ style :goofy,
964
+ transform: CGAffineTransform.rotate(pi / 2).translate(100, 0).scale(2)
965
+ style :regular,
966
+ transform: CGAffineTransform.identity
967
+
968
+ # CALayer uses CATransform3D objects
969
+ style :regular,
970
+ layer: {
971
+ transform: CATransform3D.rotate(pi / 2)
972
+ }
973
+ ```
974
+
975
+ ### Showdown
976
+
977
+ As a recap, here is a translation of traditional Cocoa code done using Teacup.
978
+
979
+ No cool tricks here, just some plain ol' Cocoa.
980
+
981
+ ```ruby
982
+ #
983
+ # Traditional Cocoa
984
+ #
985
+ class SomeController < UIViewController
986
+
987
+ def viewDidLoad
988
+ @field = UITextField.new
989
+ @field.frame = [[10, 10], [200, 50]]
990
+ @field.textColor = UIColor.redColor
991
+ view.addSubview(@field)
992
+
993
+ @search = UITextField.new
994
+ @search.frame = [[10, 70], [200, 50]]
995
+ @search.placeholder = 'Find something...'
996
+ @search.textColor = UIColor.redColor
997
+ view.addSubview(@search)
998
+ end
999
+
1000
+ # perform the frame changes depending on orientation
1001
+ def willAnimateRotationToInterfaceOrientation(orientation, duration:duration)
1002
+ case orientation
1003
+ when UIInterfaceOrientationLandscapeLeft, UIInterfaceOrientationLandscapeRight
1004
+ @field.frame = [[10, 10], [360, 50]]
1005
+ @search.frame = [[10, 70], [360, 50]]
1006
+ else
1007
+ @field.frame = [[10, 10], [200, 50]]
1008
+ @search.frame = [[10, 70], [200, 50]]
1009
+ end
1010
+ end
1011
+
1012
+ end
1013
+
1014
+ #
1015
+ # Teacup
1016
+ #
1017
+
1018
+ class SomeController < UIViewController
1019
+
1020
+ stylesheet :some_view
1021
+
1022
+ layout :root do
1023
+ subview(UITextField, :field)
1024
+ @search = subview(UITextField, :search)
1025
+ end
1026
+
1027
+ end
1028
+
1029
+ Teacup::Stylesheet.new(:some_view) do
1030
+
1031
+ style :root, # enable landscape rotation (otherwise only portrait is enabled)
1032
+ landscape: true # this must be on the root-view, to indicate that this view is
1033
+ # capable of handling rotations
1034
+
1035
+ style :field,
1036
+ left: 10,
1037
+ top: 10,
1038
+ width: 200,
1039
+ height: 50,
1040
+ landscape: {
1041
+ width: 360 # make it wide in landscape view
1042
+ }
1043
+
1044
+ style :search, extends: :field,
1045
+ left: 10,
1046
+ top: 70,
1047
+ placeholder: 'Find something...'
1048
+
1049
+ style UITextField, # Defining styles based on view class instead
1050
+ textColor: UIColor.redColor # of style name.
422
1051
 
423
- style :submit_button, extends: :button,
424
- top: 50
425
1052
  end
426
1053
  ```
427
1054
 
428
- UIView classes
429
- ==============
1055
+ The Nitty Gritty
1056
+ ----------------
1057
+
1058
+ #### Regarding Style Precedence
1059
+
1060
+ You need to be careful when extending styles and using orientation styles
1061
+ because the precedence rules take some getting used to. The goal is that you
1062
+ can have all your style code in the stylesheets. But you also need to be able
1063
+ to animate your views, and rotating the device should not go reseting
1064
+ everything.
1065
+
1066
+ So here's what happens.
1067
+
1068
+ When your controller is loaded, `viewDidLoad` is called, and that's where Teacup
1069
+ creates the view hierarchy and applies the styles. It is at the *end* of the
1070
+ method that the styles are applied - not until all the views have been added.
1071
+ The current device orientation will be used so that orientation-specific styles
1072
+ will be applied.
1073
+
1074
+ Now Teacup goes quiet for a while. Your app chugs along... until the user
1075
+ rotates the device.
1076
+
1077
+ If you have orientation-specific styles, they will get applied. But the
1078
+ *original* styles (the "generic" styles) **will not**.
1079
+
1080
+ However, there's a way around *that, too.* If you call `restyle!` on a
1081
+ `UIView`, that will reapply all the original stylesheet styles - orientation
1082
+ *and* generic styles.
1083
+
1084
+ With me so far? Orientation styles are reapplied whenever the device is
1085
+ rotated. But generic styles are only applied in `viewDidLoad` and when
1086
+ `restyle!` is called explicitly.
1087
+
1088
+ How does the `:extends` property affect things?
1089
+
1090
+ If your stylesheet defines orientation-specific styles and "generic" styles, the
1091
+ orientation-specific styles win. But if you *extend* a style that has
1092
+ orientation-specific styles, your local generic styles will win.
1093
+
1094
+ The more "local" styles always win - and that applies to styles that you add
1095
+ using the `subview/layout` methods, too. The only time it doesn't really apply
1096
+ is if you apply styles using `UIView#style` or `UIView#apply_stylename`. Those
1097
+ are one-shot (they can get overwritten when `restyle!` is called).
430
1098
 
431
- You can style entire classes of `UIView`s! These get merged in last (lowest
432
- precedence), but they are a great way to get site-wide styles. Put these in a
433
- base stylesheet, and import that stylesheet everywhere. The Apple
434
- `UIAppearance` protocol/classes do this same thing, but in much more code ;-)
1099
+ There are also times when you either want (or must) override (or add to) the
1100
+ stylesheet styles. For instance, if you want to assign the `delegate` or
1101
+ `dataSource` properties, this cannot be done from a `Stylesheet`. But that's
1102
+ okay, because we have a chance to add these styles in the `subview` and `layout`
1103
+ methods.
435
1104
 
436
1105
  ```ruby
437
- Teacup::Stylesheet.new :base do
438
- style UIButton,
439
- font: UIFont.systemFontOfSize(20)
1106
+ layout do
1107
+ subview(UITableView, delegate: self)
440
1108
  end
1109
+ ```
441
1110
 
442
- Teacup::Stylesheet.new :main do
443
- style :ok_button, # no need to use extends
444
- top: 10
1111
+ Styles applied here are one-shot. It is the exact same as assigning the
1112
+ `stylename` and `style_classes` and then calling `style`. Because the
1113
+ stylesheet is not necessarily applied immediately, these styles could be
1114
+ overwritten before they take effect.
1115
+
1116
+ ```ruby
1117
+ layout do
1118
+ table_view = subview(UITableView, :tableview, delegate: self,
1119
+ font: UIFont.boldSystemFontOfSize(10) # the stylesheet could override this during rotation
1120
+ )
445
1121
  end
446
1122
 
447
- Teacup::Stylesheet.new :register do
448
- style :submit_button, # no need to use extends
449
- top: 50
1123
+ def layoutDidLoad
1124
+ table_view.apply_stylename(:tableview_init) # this will only get applied once
450
1125
  end
451
1126
  ```
452
1127
 
453
- Precedence, and Style
454
- ==========
1128
+ The idea here is that the closer the style setting is to where the view is
1129
+ instantiated, the higher the precedence.
455
1130
 
456
- 1. Within a `style` declaration, orientation-specific properties override generic properties
457
- 2. Imported properties will be merged in, but no values from (1) will be overridden
458
- 3. Extended styles will be merged in, but no values from (1) or (2) will be overridden
459
- 4. Styles applied to an ancestor will be merged in, but no values from (1) or (2) or (3) will be overridden
1131
+ More examples!
460
1132
 
461
- These rules are maintained in the `Style` class. It starts by creating a Hash
462
- based on the `style` declaration, then merges the orientation styles, if
463
- applicable (orientation styles override). Next it merges imports, then
464
- extends, and finally the class ancestors. At each "merge" except orientation
465
- merges, it is a recursive-soft-merge, meaning that Hashes will be merged, but
466
- existing keys will be left alone.
1133
+ ```ruby
1134
+ class MyController < UIViewController
1135
+ stylyesheet :my_sheet
1136
+ layout do
1137
+ subview(UILabel, :label, text: 'overrides')
1138
+ end
1139
+ end
1140
+ Teacup::Stylesheet.new :my_sheet do
1141
+ style :generic_label,
1142
+ text: 'portrait',
1143
+ # these get applied initially, but after being rotated they will not get
1144
+ # applied again
1145
+ font: UIFont.boldSystemFontOfSize(10),
1146
+ textColor: UIColor.grayColor,
1147
+ landscape: {
1148
+ font: UIFont.boldSystemFontOfSize(12),
1149
+ textColor: UIColor.whiteColor,
1150
+ } # this style should add a `portrait` setting that restores the font and color
1151
+
1152
+ style :label, extends: :generic_label,
1153
+ font: UIFont.systemFontOfSize(10), # this will override all the font settings
1154
+ end
1155
+ ```
467
1156
 
468
- Unless you are going to be hacking on teacup, you do not need to understand the
469
- nitty-gritty - just remember the precedence rules.
1157
+ Advanced Teacup Tricks
1158
+ ----------------------
470
1159
 
471
- Development
472
- -----------
1160
+ There are times when you might wish teacup "just worked", but please remember:
1161
+ Teacup is not a "blessed" framework built by Apple engineers. We have access to
1162
+ the same APIs that you do. That said, here are some use-cases where you can most
1163
+ definitely *use* teacup, but you'll need to do a little more leg work.
473
1164
 
474
- *Current version*: v0.2.0 (or see `lib/teacup/version.rb`)
1165
+ ### Trust your parent view - by using springs and struts
475
1166
 
476
- *Last milestone*: Release layout and stylesheet DSL to the world.
1167
+ *...not autolayout*
477
1168
 
478
- *Next milestone*: Provide default styles, that mimic Interface Builder's object library
1169
+ It's been mentioned a few times in this document that Teacup will create & style
1170
+ views in the `viewDidLoad` method. That means that the `superview` property of
1171
+ the controller's view will, necessarily, *not* be set yet. `viewDidLoad` is
1172
+ called after the view is instantiated (in `loadView`), and it hasn't been added
1173
+ as a subview yet.
479
1174
 
480
- **Changelog v0.2.0:**
1175
+ Auto-Layout is based on the relationship between two views - often a container
1176
+ and child view. It's an amazing system, but if that parent view *isn't
1177
+ available*, well, you're not gonna have much success.
481
1178
 
482
- - Stylesheets are no longer constants. Instead, they can be fetched by name: `Teacup::Stylesheet[:iphone]`
483
- - Stylesheets can be assigned by calling the `stylesheet :stylesheet_name` inside a view controller.
484
- - Ability to style based on view class.
485
- - Support for orientation-based styles.
1179
+ In the case of a UIViewController your "container" is the `self.view` property,
1180
+ which by default has sensible springs setup so that it stretches to fill the
1181
+ superview. It's not until you go messing with the `self.view` property, or are
1182
+ not in the context of a `UIViewController` that things get hairy.
486
1183
 
1184
+ If this is the case, you should get some pretty obvious warning messages,
1185
+ something along the lines of `Could not find :superview`.
487
1186
 
488
- teacup, being a community project, moves in "spurts" of decision making and
489
- coding. We will announce when we are in "proposal mode". That's a good time to
490
- jump into the project and offer suggestions for its future.
1187
+ ### UITableViewCell
491
1188
 
492
- And we're usually hanging out over at the `#teacuprb` channel on `irc.freenode.org`.
1189
+ If you are using your controller as your tableview dataSource this is not a big
1190
+ deal, the `subview` and `layout` methods continue to work as you expect them to.
1191
+
1192
+ I don't know about you, but I often write helper classes for tableviews that
1193
+ appear on many screens in an app. You should not shy away from adding teacup's
1194
+ `Layout` module to these helper classes.
1195
+
1196
+ ```ruby
1197
+ class TableHelper
1198
+ include Teacup::Layout
1199
+
1200
+ stylesheet :table_helper
1201
+
1202
+ def tableView(table_view, cellForRowAtIndexPath:index_path)
1203
+ cell_identifier = 'MyController - cell'
1204
+ cell = table_view.dequeueReusableCellWithIdentifier(cell_identifier)
1205
+
1206
+ unless cell
1207
+ cell = UITableViewCell.alloc.initWithStyle(UITableViewCellStyleDefault
1208
+ reuseIdentifier: cell_identifier)
1209
+
1210
+ layout(cell.contentView) do
1211
+ subview(UIImageView, :image)
1212
+ end
1213
+ # cell.contentView and all child classes will "inherit" the :table_helper stylesheet
1214
+ end
1215
+
1216
+ return cell
1217
+ end
1218
+
1219
+ end
1220
+ ```
1221
+
1222
+ ### [Sweettea][]
1223
+
1224
+ *SugarCube + Teacup = Sweettea*
1225
+
1226
+ SugarCube was born of a desire to make Teacup stylesheets more readable, less
1227
+ cluttered with Apple's verbose method names and constants. Sweettea takes this
1228
+ a step further, by implementing a wealth of Teacup handlers that translate
1229
+ Symbols to constants and provide useful shorthands.
1230
+
1231
+ ```ruby
1232
+ style :button,
1233
+ normal: { image: 'button-white' },
1234
+ highlighted: { image: 'button-white-pressed' },
1235
+ title: 'Submit',
1236
+ shadow: {
1237
+ opacity: 0.5,
1238
+ radius: 3,
1239
+ offset: [3, 3],
1240
+ color: :black,
1241
+ },
1242
+ font: 'Comic Sans'
1243
+
1244
+ style :label,
1245
+ font: :bold,
1246
+ alignment: :center,
1247
+ color: :slateblue
1248
+ ```
1249
+
1250
+ Sweettea also offers some convenient styles that you can extend in your base
1251
+ class. You might want to either specify the Sweettea version you are using in
1252
+ your Gemfile, or copy the stylesheet so that changes to Sweettea don't affect
1253
+ your project. Once that projet is at 1.0 you can rely on the styles not
1254
+ changing.
1255
+
1256
+ ```ruby
1257
+ # buttons! :tan_button, :black_button, :green_button, :orange_button,
1258
+ # :blue_button, :white_button, :gray_button
1259
+ style :submit_button, extends: :white_button
1260
+
1261
+ # label sets more sensible defaults than a "raw" UILabel (like clear background)
1262
+ style :header, extends: :label
1263
+
1264
+ # inputs! these are not styled, they just setup keyboard and autocomplete
1265
+ # settings
1266
+ # :name_input, :ascii_input, :email_input, :url_input, :number_input,
1267
+ # :phone_input, :secure_input
1268
+ style :login_input, extends: :email_input
1269
+ style :password_input, extends: :secure_input
1270
+ ```
1271
+
1272
+ Misc notes
1273
+ ----------
1274
+
1275
+ Multiple calls to `style` with the same stylename combines styles, it doesn't
1276
+ replace the styles.
1277
+
1278
+ ------
1279
+
1280
+ Styles are not necessarily applied immediately. They are applied at the end of
1281
+ the outermost `layout/subview` method, including the `UIViewController##layout`
1282
+ block. If you call `stylename=` or `stylesheet=` *outside* a `layout/subview`
1283
+ block, your view will be restyled immediately.
1284
+
1285
+ ------
1286
+
1287
+ Restyling a view calls `restyle!` on all child views, all the way down the tree.
1288
+ Much care has been taken to call this method sparingly within Teacup.
1289
+
1290
+ ------
1291
+
1292
+ Any styles that you apply in a `layout/subview` method are *not* retained, they
1293
+ are applied immediately, and so the stylesheet can (and usually do) override
1294
+ those styles if there is a conflict. Only styles stored in a stylesheet are
1295
+ reapplied (during rotation or in `restyle!`).
1296
+
1297
+ ------
1298
+
1299
+ Stylesheets should not be modified once they are created - they cache styles by
1300
+ name (per orientation).
1301
+
1302
+ ------
1303
+
1304
+ You can add and remove a `style_class` using `add_style_class` and
1305
+ `remove_style_class`, which will call `restyle!` for you if `style_classes`
1306
+ array was changed.
1307
+
1308
+ ------
1309
+
1310
+ If you need to do frame calculations outside of the stylesheet code, you should
1311
+ do so in the `layoutDidLoad` method. This is not necessary, though! It is
1312
+ usually cleaner to do the frame calculations in stylesheets, either using
1313
+ [geomotion][], frame calculations, or auto-layout.
1314
+
1315
+ ------
1316
+
1317
+ Within a `subview/layout` block views are added to the last object in
1318
+ `Layout#superview_chain`. Views are pushed and popped from this array in the
1319
+ `Layout#layout` method, starting with the `top_level_view`. If you include
1320
+ `Teacup::Layout` on your own class, you do not *have* to implement
1321
+ `top_level_view` unless you want to use the `subview` method to add classes to a
1322
+ "default" target.
1323
+
1324
+ ------
1325
+
1326
+ When `UIView` goes looking for its `stylesheet` it does so by going up the
1327
+ responder chain. That means that if you define the stylesheet on a parent view
1328
+ or controller, all the child views will use that same stylesheet by default. It
1329
+ also means you can assign a stylesheet to a child view without worrying what the
1330
+ parent view's stylesheet is.
1331
+
1332
+ Caveat! If you implement a class that includes `Teacup::Layout`, you can assign
1333
+ it a `stylesheet`. *That* stylesheet will be used by views created using
1334
+ `layout` or `subview` even though your class is probably not part of the
1335
+ responder chain. Saying that `UIView` inherits its `stylesheet` from the
1336
+ responder chain is not accurate; it actually uses `teacup_responder`, which
1337
+ defaults to `nextResponder`, but it is assigned to whatever object calls the
1338
+ `layout` method on the view.
1339
+
1340
+ ------
1341
+
1342
+ If you use `Teacup::Appearance` but it is not styling the first screen of your
1343
+ app (but, strangely, *does* style all other screens), try calling
1344
+ `Teacup::Appearance.apply` before creating you create the `rootViewController`
1345
+ (in your `AppDelegate`)..
1346
+
1347
+ ------
493
1348
 
494
1349
  The Dummy
495
1350
  ---------
@@ -500,30 +1355,28 @@ If you get an error that looks like this:
500
1355
  precompiled. Make sure you properly link with the framework or library that
501
1356
  defines this message.
502
1357
 
503
- You need to add your method to dummy.rb. This is a compiler issue, nothing we
504
- can do about it except build up a huge dummy.rb file that has just about every
505
- method that you would want to style.
1358
+ You probably need to add your method to [dummy.rb][]. This is a compiler issue,
1359
+ nothing we can do about it except build up a huge dummy.rb file that has just
1360
+ about every method that you would want to style.
506
1361
 
507
- Example
508
- =======
1362
+ # Teacup is a Community Project!
509
1363
 
510
- The error above was from trying to style the
511
- `UIActivityIndicatorView#hidesWhenStopped` property. Make a new `Dummy` class,
512
- subclass the class *where the method is defined* (not a subclass, please) and
513
- add that method to a `dummy` method. You should assign it `nil` so that the
514
- method signature looks right. And you should mark it private, too.
1364
+ Teacup was born out of the #rubymotion irc chatroom in the early days of
1365
+ RubyMotion. Its design, direction, and priorities are all up for discussion!
515
1366
 
516
- ```ruby
517
- class DummyActivityIndicatorView < UIActivityIndicatorView
518
- private
519
- def dummy
520
- setHidesWhenStopped(nil)
521
- end
522
- end
523
- ```
1367
+ I'm [Colin T.A. Gray][colinta], the maintainer of the Teacup project. I hope this
1368
+ tool helps you build great apps!
524
1369
 
525
- Bugs
526
- ----
1370
+ [advanced]: https://github.com/rubymotion/teacup/#advanced-teacup-tricks
1371
+ [calculations]: https://github.com/rubymotion/teacup/#frame-calculations
1372
+ [dummy.rb]: https://github.com/rubymotion/teacup/tree/master/lib/dummy.rb
1373
+ [other-handlers]: https://github.com/rubymotion/teacup/tree/master/lib/teacup/z_core_extensions/z_handlers.rb
527
1374
 
528
- Please report any bugs you find with our source at the
529
- [Issues](https://github.com/rubymotion/teacup/issues) page.
1375
+ [Pixate]: http://www.pixate.com
1376
+ [NUI]: https://github.com/tombenner/nui
1377
+ [geomotion]: https://github.com/clayallsopp/geomotion
1378
+ [UIAppearance]: http://developer.apple.com/library/ios/#documentation/uikit/reference/UIAppearance_Protocol/Reference/Reference.html#//apple_ref/occ/intf/UIAppearance
1379
+ [motion-layout]: https://github.com/qrush/motion-layout
1380
+ [sweettea]: https://github.com/colinta/sweettea
1381
+ [qrush]: https://github.com/qrush
1382
+ [colinta]: https://github.com/colinta