Teacup
A community-driven DSL for creating user interfaces on iOS and OS X.
Using Teacup, you can create and style layouts and keeping your code dry. The
goal is to offer a rubyesque (well, actually a rubymotion-esque) way to create
interfaces programmatically.
A note about Teacup and MotionKit
Read a letter
from Colin T.A. Gray regarding the future of Teacup and introducing
MotionKit, its successor.
Check out some sample apps!
Quick Install
> gem install teacup
and in your Rakefile
require 'teacup'
10 second primer, iOS
-
Create a UIViewController
subclass:
class MyController < UIViewController
-
Assign a stylesheet name:
class MyController < UIViewController
stylesheet :main_screen
-
Create a layout:
class MyController < UIViewController
stylesheet :main_screen
def teacup_layout
subview(UIButton, :hi_button)
end
end
-
Create the stylesheet (in app/styles/
or somewhere near the controller)
Teacup::Stylesheet.new :main_screen do
style :hi_button,
origin: [10, 10],
title: 'Hi!'
end
10 second primer, OS X
Pretty much the same! Note that on OS X, view coordinates are based on having
the origin in the bottom-left corner, not the upper-left like it is on every
other GUI system ever. :-|
You should use the TeacupWindowController
parent class instead of NSWindowController
-
Create a TeacupWindowController
subclass.
class MyController < TeacupWindowController
-
Assign a stylesheet name:
class MyController < TeacupWindowController
stylesheet :main_window
-
Create a layout:
class MyController < TeacupWindowController
stylesheet :main_window
def teacup_layout
subview(NSButton, :hi_button)
end
end
-
Create the stylesheet (in app/styles/
or somewhere near the controller)
Teacup::Stylesheet.new :main_window do
style :hi_button,
origin: [10, 10],
title: 'Hi!'
end
Teacup
Teacup's goal is to facilitate the creation and styling of your view hierarchy.
Say "Goodbye!" to Xcode & XIB files!
Teacup is composed of two systems:
-
Layouts
A DSL to create Views and to organize them in a hierarchy. You assign the
style name and style classes from these methods.
-
Stylesheets
Store the "styles" that get applied to your views. The stylesheet DSL is
meant to resemble CSS, but is targeted at iOS, and so the precedence rules are
very different.
Teacup supports Pixate and NUI, too, so you can use those systems for
styling and Teacup to manage your view hierarchy and apply auto-layout
constraints. Teacup can also integrate with the motion-layout gem!
Changes in 3.0
There is one significant change in version 3.0. In every version of Teacup
prior (from 0.2.0 to 2.3.0) the controller layout was usually created by calling
a class method called layout
. It was discovered, embarrassingly late, that
this system is causing memory leaks. To fix it we had to remove this feature
altogether. So if you are looking at old Teacup examples, you will see this
block syntax that is no longer offered. It is easy to update to 3.0, though:
class MyController < UIViewController
layout(:root_stylename) do
end
end
class MyController < UIViewController
def teacup_layout
root(:root_stylename, { background: UIColor.blueColor })
end
layout(:root_stylename, {})
end
Table of Contents
Layouts
The Teacup::Layout
module is mixed into UIViewController
and UIView
on
iOS, and NSWindowController
, NSViewController
, and NSView
on OS X. These
classes can take advantage of the view-hierarchy DSL.
You saw an example in the primer, using the
UIViewController
/NSWindowController
class method layout
and the
teacup_layout
method. You could just as easily use Teacup's DSL to create
your views from a loadView
method, for instance you might want to use a
custom view subclass as your root view. An example might look like this:
class MyController < UIViewController
def loadView
self.view = layout(FancyView, :root) do
subview(UIToolbar, :toolbar)
subview(UIButton, :hi_button)
end
end
end
You can use very similar code in your view subclasses.
def MyView < UIView
def initWithFrame(frame)
super.tap do
self.stylesheet = :my_stylesheet
subview(UIImageView, :image)
end
end
end
The layout
and subview
methods are the work horses of the Teacup view DSL.
layout(view|ViewClass, stylename, style_classes, additional_styles, &block)
view|ViewClass
- You can layout an existing class or you can have Teacup
create it for you (it just calls new
on the class, nothing special). This
argument is required.stylename
(Symbol
) - This is the name of a style in your stylesheet. It
is optionalstyle_classes
([Symbol,...]
) - Other stylenames, they have lower
priority than the stylename
.additional_styles
(Hash
) - You can pass other styles in here as well,
either to override or augment the settings from the Stylesheet
. It is
common to use this feature to assign the delegate
or dataSource
.&block
- See discussion below- Returns the
view
that was created or passed to layout
. - only the
view
arg is required. You can pass any combination of
stylename, style_classes, and additional_styles (some, none, or all).
subview(view|UIViewClass, stylename, style_classes, additional_styles, &block)
- Identical to
layout
, but adds the view to the current target
The reason it is so easy to define view hierarchies in Teacup is because the
layout
and subview
methods can be "nested" by passing a block.
subview(UIView, :container) do
subview(UIView, :inputs) do
@email_input = subview(UITextField, :email_input)
@password_input = subview(UITextField, :password_input)
end
subview(UIButton.buttonWithType(UIButtonTypeRoundedRect), :submit_button)
end
These methods are defined in the Layout
module. And guess what!? It's easy
to add your own view helpers! I refer to this as a "partials" system, but
really it's just Ruby code (and isn't that the best system?).
module Teacup::Layout
def button(*args, &block)
args = [:button] if args.empty?
subview(UIButton.buttonWithType(UIButtonTypeCustom), *args, &block)
end
def button_with_icon(icon, title)
label = UILabel.new
label.text = title
label.sizeToFit
image_view = UIImageView.new
image_view.image = icon
image_view.sizeToFit
button = UIButton.buttonWithType(UIButtonTypeCustom)
button.addSubview(image_view)
button.addSubview(label)
subview(button)
end
end
example use of the helper methods
class MyController < UIViewController
def teacup_layout
@button1 = button()
@button2 = button(:blue_button)
@button3 = button_with_icon(UIImage.imageNamed('email_icon'), 'Email')
end
end
The Controller#teacup_layout
method is going to be the first or second thing
you add to a controller when you are building an app with Teacup. Inside you
will add subviews using subview
or you can create a view using the layout
method (subview
delegates most of its work to layout
)
After the views have been added and styles have been applied Teacup calls the
layoutDidLoad
method. If you need to perform some additional initialization
on your views, you can do it in this method.
Stylesheets
This is where you will store your styling-related code. Migrating code from your
controller or custom view into a stylesheet is very straightforward. The method
names map 1::1.
def viewDidLoad
self.view.backgroundColor = UIColor.grayColor
end
def viewDidLoad
self.stylesheet = :main
self.view.stylename = :root
end
Teacup::Stylesheet.new :main do
style :root,
backgroundColor: UIColor.grayColor
end
Nice! We turned three lines of code into nine! Well, obviously the benefits
come in when we have lots of style code, and when you need to do app-wide
styling.
You can store stylesheets in any file. It is common to use app/styles.rb
or
app/styles/main.rb
, if you have more than a few of 'em. The
Teacup::Stylesheet
constructor accepts a stylesheet name and a block, which
will contain your style declarations.
Teacup::Stylesheet.new :main_menu do
style :ready_to_play_button,
backgroundColor: UIColor.blackColor,
frame: [[20, 300], [50, 20]]
end
Teacup::Stylesheet[:main_menu]
Any method that accepts a single value can be assigned in a stylesheet. Please
don't abuse this by hiding application logic in your stylesheets - these are
meant for design, not behavior.
Limelight syntax
If you want to use a shorter syntax, you can use the "Limelight" inspired
syntax:
Teacup::Stylesheet.new :main_menu do
ready_to_play_button do
backgroundColor UIColor.blackColor
frame [[20, 300], [50, 20]]
end
end
This creates the same style hashes as style :ready_to_play_button
, so you have
access to all the features that are available in the "traditional" syntax.
Using and re-using styles in a Stylesheet
- Styles are be applied via stylename (
style :label
) or class (style UILabel
) - Styles can extend other styles (
style :big_button, extends: :button
) - A stylesheet can import other stylesheets (
import :app
) - The special Appearance stylesheet can be used to apply styles to
UIAppearance
(Teacup::Appearance.new
)
Let's look at each in turn.
Style via Stylename
This is the most common way to apply a style.
class MainController < UIViewController
stylesheet :main
def teacup_layout
subview(UILabel, :h1)
end
end
Teacup::Stylesheet.new :main do
style :h1,
font: UIFont.systemFontOfSize(20)
end
When the stylesheet is applied (at the end of the layout
block, when all the
views have been added), its font
property will be assigned the value
UIFont.systemFontOfSize(20)
.
But we didn't assign any text!
We can tackle this a couple ways. You can apply "last-minute" styles in the
layout
and subview
methods:
def teacup_layout
subview(UILabel, :h1,
text: "Omg, it's full of stars"
)
end
In this case though we just have static text, so you can assign the text using
the stylesheet:
Teacup::Stylesheet.new :main do
style :h1,
font: UIFont.systemFontOfSize(20)
style :main_header,
text: "Omg, it's full of stars",
font: UIFont.systemFontOfSize(20)
end
Extending Styles
Not very DRY though is it!? We have to use a new style (:main_header
) because
not all our labels say "OMG", but we want to use our font from the :h1
style.
We can tell the :main_header
style that it extends
the :h1
style:
def teacup_layout
subview(UILabel, :main_header)
end
Teacup::Stylesheet.new :main do
style :h1,
font: UIFont.systemFontOfSize(20)
style :main_header, extends: :h1,
text: "Omg, it's full of stars"
end
A common style when writing stylesheets is to use variables to store settings
you want to re-use.
Teacup::Stylesheet.new :main do
h1_font = UIFont.systemFontOfSize(20)
style :h1,
font: h1_font
style :main_header, extends: :h1,
text: "Omg, it's full of stars"
end
And you're not limited to one class that you can extend, it accepts an array
Teacup::Stylesheet.new :main do
h1_font = UIFont.systemFontOfSize(20)
style :h1,
font: h1_font
style :label,
textColor: UIColor.black
style :main_header, extends: [:h1, :label],
text: "Omg, it's full of stars"
end
Style via View Class
If you need to apply styles to all instances of a UIView
/NSView
subclass,
you can do so by applying styles to a class name instead of a symbol. This
feature is handy at times when you might otherwise use UIAppearance
(which
teacup also supports!).
Teacup::Stylesheet.new :app do
style UILabel,
font: UIFont.systemFontOfSize(20)
style UITableView,
backgroundColor: UIColor.blackColor
end
Importing stylesheets
We've touched on the ability to write styles, extend styles, and apply styles to
a class. Now we can introduce another feature that is even more useful for
applying styles to your entire app: import a stylesheet.
When you import a stylesheet, you receive all of its style
s and you gain
access to its instance variables. This way you can define colors and margins and
such in a "parent" stylesheet.
Teacup::Stylesheet.new :app do
@header_color = UIColor.colorWithRed(7/255.0, green:16/255.0, blue:95/255.0, alpha: 1)
@background_color = UIColor.colorWithRed(216/255.0, green:226/255.0, blue:189/255.0, alpha: 1)
style :root,
backgroundColor: @background_color
style :header,
textColor: @header_color
end
Teacup::Stylesheet.new :main do
import :app
style :subheader, extends: :header
style :button,
titleColor: @header_color
end
Style via UIAppearance
iOS only
And lastly, the UIAppearance protocol
is supported by creating an instance of
Teacup::Appearance
. There is debatable benefit to using UIAppearance,
because it will apply styles to views that are outside your control, like the
camera/image pickers and email/message controllers.
But, it does come in handy sometimes... so here it is!
Teacup::Appearance.new do
style UINavigationBar,
barTintColor: UIColor.blackColor,
titleTextAttributes: {
UITextAttributeFont => UIFont.fontWithName('Trebuchet MS', size:24),
UITextAttributeTextShadowColor => UIColor.colorWithWhite(0.0, alpha:0.4),
UITextAttributeTextColor => UIColor.whiteColor
}
style UIBarButtonItem, when_contained_in: UINavigationBar,
tintColor: UIColor.blackColor
style UIBarButtonItem, when_contained_in: [UIToolbar, UIPopoverController],
tintColor: UIColor.blackColor
end
In your AppDelegate you need to call Teacup::Appearance.apply
. It will get
called automatically using the UIApplicationDidFinishLaunchingNotification
,
but that notification is triggered after the method
AppDelegate#didFinishLaunching(withOptions:)
is called.
app_delegate.rb
class AppDelegate
def didFinishLaunching(application, withOptions:options)
Teacup::Appearance.apply
@window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
ctlr = MainController.new
@window.rootViewController = UINavigationController.alloc.initWithRootController(ctlr)
@window.makeKeyAndVisible
true
end
end
That block is called using the UIApplicationDidFinishLaunchingNotification
,
but that notification is not called until the end of the
application(application,didFinishLaunchingWithOptions:launchOptions)
method.
This is sometimes after your views have been created, and so they will not be
styled. If that is the case, call Teacup::Appearance.apply
before creating
your rootViewController
.
Now go use Teacup!
You have enough information right now to go play with Teacup. Check out the
example apps, write your own, whatever. But read on to hear about why Teacup is
more than just writing layouts
and applying styles.
You should also be sure to read the Misc notes section at the
bottom. These aren't very well organized, but the information is important. You
can learn about features any time, but learning about the "gotcha"s sooner
rather than later could save you a lot of time!
Teacup as a utility
When you are prototyping an app it is useful to bang out a bunch of code
quickly, and here are some ways that Teacup might help.
You can use all the methods above without having to rely on the entirety of
Teacup's layout and stylesheet systems. By that I mean any time you are
creating a view hierarchy don't be shy about using Teacup to do it.
UIView
and NSView
have the style
method, which can be used to group a
bunch of customizations anywhere in your code. You don't have to pull out a
stylesheet to do it.
self.navigationItem.titleView = layout(UILabel,
text:'Title',
font: UIFont.systemFontOfSize(12),
)
def tableView(table_view, cellForRowAtIndexPath:index_path)
cell_identifier = 'MyController - cell'
cell = table_view.dequeueReusableCellWithIdentifier(cell_identifier)
unless cell
cell = UITableViewCell.alloc.initWithStyle(UITableViewCellStyleDefault,
reuseIdentifier: cell_identifier)
layout(cell.contentView) do
subview(UIImageView, :image)
end
end
return cell
end
@label.style(textColor: UIColor.blueColor, text: 'Blue Label')
UITableViews
Teacup is designed to be used in coordination with the controller life cycle,
but there are other life cycles that need to be considered as well.
UITableViews maintain a "queue" of cells that can be reused, and they need to be
restyled when the cell is created and re-used.
The solution is to apply the styles and layout constraints inside the
tableView:willDisplayCell:forRowAtIndexPath:
delegate method. In your
delegate, if you include the Teacup::TableViewDelegate
module, you'll get this
behavior for free, and if you override this method, you can call super
to have
the Teacup method run.
class TableViewController < UITableViewController
include Teacup::TableViewDelegate
stylesheet :table
def tableView(table_view, cellForRowAtIndexPath:index_path)
cell = table_view.dequeueReusableCellWithIdentifier('cell id')
layout(cell.contentView, :root) do
cell.title_label = subview(UILabel, :title_label, :text => "title #{index_path.row}")
cell.details_label = subview(UILabel, :details_label, :text => "details #{index_path.row}")
cell.other_label = subview(UILabel, :other_label, :text => "other #{index_path.row}")
end
return cell
end
end
Constraints and styles get applied before the view appears, even if the cell is
reused later.
More Teacup features
There are a few (OK, a bunch) more features that Teacup provides that deserve
discussion:
- Styling View Properties
- Orientation Styles
- View Class Additions
- Style Handlers
- Frame Calculations
- Auto-Layout & Motion-Layout
- Stylesheet Extensions
Styling View Properties
Styling a UIView is fun, but a UIView is often composed of many objects, like
the layer
, or maybe an imageView
or textLabel
and so on. You can style
those, too!
style :tablecell,
layer: {
shadowRadius: 3
},
backgroundView: {
backgroundColor: UIColor.blackColor
},
imageView: {
contentMode: UIViewContentModeScaleAspectFill
}
Orientation Styles
iOS only
There's more to stylesheets than just translating UIView
setters. Teacup can
also apply orientation-specific styles. These are applied when the view is
created (using the current device orientation) and when a rotation occurs.
Teacup::Stylesheet.new :main do
style :label,
landscape: {
hidden: true
},
portrait: {
hidden: false
}
end
Combine these styles with Frame Calculations to have you view
frame recalculated automatically.
Animation additions
We've already seen the Teacup related properties:
stylename
, the primary style namestyle_classes
, secondary style namesstyle
, apply styles directly
Each of these has a corresponding method that you can use to facilitate
animations.
animate_to_stylename(stylename)
animate_to_styles(style_classes)
animate_to_style(properties)
On OS X you have to use the view.animator
property to perform animations.
This is supported, but it's kind of "hacky".
Style Handlers
This feature is used extensively by sweettea to make a more intuitive
stylesheet DSL
Teacup is, by itself, pretty useful, but it really does little more than map
Hash keys to UIView
setters. That's great, because it keeps the system easy
to understand. But there are some methods in UIKit that take more than one
argument, or could benefit from some shorthands.
This is where Teacup's style handlers come in. They are matched against a
UIView
subclass and one or more stylenames, and they are used to apply that
style when you use it in your stylesheet.
Teacup.handler UIButton, :title do |target, title|
target.setTitle(title, forState: UIControlStateNormal)
end
subview(UIButton,
title: 'This is the title'
)
layout(UINavigationItem,
title: 'This is the title'
)
Other built-in handlers are defined in z_handlers.rb
.
Another useful one is the ability to make view the same size as its parent, and
located at the origin.
style :container,
frame: :full
Frame Calculations
These are super cool, just don't forget your autoresizingMasks
When positioning views you will often have situations where you want to have a
view centered, or 8 pixels to the right of center, or full width/height. All of
these relationships can be described using the Teacup.calculate
method, which
is called automatically in any method that modifies the frame
or center
.
frame, origin, size
top/y, left/x, right, bottom, width, height
center_x/middle_x, center_y/middle_y, center
Teacup::Stylesheet.new :main do
style :button,
left: 8, top: 8,
width: '100% - 16',
height: 22
style :top_half,
frame: [[0, 0], ['100%', '50%']]
style :bottom_half,
frame: [[0, '50%'], ['100%', '50%']]
end
When this code executes, the string '100% - 16'
is translated into the formula
1.00 * target.superview.frame.size.width - 16
. If the property is related to
the height or y-position, it will be calculated based on the height.
The frame calculations must be a string of the form /[0-9]+% [+-] [0-9]+/
. If
you need more "math-y-ness" than that, you can construct strings using interpolation.
margin = 8
style :button,
left: margin, top: margin,
width: "100% - #{margin * 2}",
height: 22
style :top_half,
frame: [[0, 0], ['100%', "50% - #{margin / 2}"]]
style :bottom_half,
frame: [[0, "50% + #{margin / 2}"], ['100%', "50% - #{margin / 2}"]]
One more example: The real power of the frame calculations comes when you
remember to set springs and struts. You can have a view "pinned" to the bottom
if you remember to set the autoresizingMask
.
Teacup::Stylesheet.new :main do
style :button,
height: 22, width: 200,
center_x: '50%',
top: '100% - 30',
autoresizingMask: (UIViewAutoresizingFlexibleLeftMargin |
UIViewAutoresizingFlexibleRightMargin |
UIViewAutoresizingFlexibleTopMargin)
end
Auto-Layout
This is another much bigger topic than it is given space for here
Teacup includes an Auto-Layout constraint DSL that you can use in your
stylesheets. These methods are added to the Stylesheet
class, so unless you
are in the context of a stylesheet, you will have to create your constraints in
longhand (you can still use the Teacup::Constraint
class to help you!).
I won't sugar-coat it: Auto-Layout is hard. Much harder than using frames and
springs and struts. And honestly, I recommend you try using the
Teacup.calculate
features mentioned above, they will take you far.
But at the end of the day, once you really understand the auto-layout system
that Apple released in iOS 6, you can build your UIs to be responsive to
different devices, orientations, and sizes. UIs built with auto-layout not
usually need to adjust anything during a rotation. The constraints take care
of it all. It's impressive.
Here's a quick example that creates this shape. The edges are bound to the
superview's frame.
+-----+----------------+
| | |
| A | B |
| | +-----| <\
| | | C | |_ 50% of B's height, minus 10 pixels
+-----+----------+-----+ </
^--+--^ ^--+--^
|_fixed (100) |_fixed (100)
Teacup::Stylesheet.new do
style :A,
constraints: [
constrain_left(0),
constrain_width(100),
constrain_top(0),
constrain(:bottom).equals(:superview, :bottom),
]
style :B,
constraints: [
constrain(:left).equals(:A, :right),
constrain(:height).equals(:A, :height),
constrain(:right).equals(:superview, :right),
]
style :C,
constraints: [
constrain_width(100),
constrain(:right).equals(:superview, :right),
constrain(:bottom).equals(:superview, :bottom),
constrain(:height).equals(:B, :height).times(0.5).minus(10),
]
end
Writing views this way will either make your brain hurt, or make the math-nerd
in you chuckle with glee. In this example you could go completely with just
frame calculation formulas and springs and struts. Your frame code would still
be cluttered, just cluttered in a different way.
If you need to reset the list of constraints managed by Teacup, you can call
reset_constraints
before you add the new styles to a UIView. This can be
useful when you need to define a new set of layout constraints for a dynamic
set of views.
This works on OS X and iOS, and you don't have to go changing the idea of "top"
and "bottom" even though OS X uses reversed frames.
Motion-Layout
If you are using Nick Quaranto's motion-layout gem, you can use it
from within any class that includes Teacup::Layout
. Then benefit is that the
Teacup stylenames assigned to your views will be used in the dictionary that the
ASCII-based system relies on.
def teacup_layout
subview(UIView, :view_a)
subview(UIView, :view_b)
subview(UIView, :view_c)
auto do
metrics 'margin' => 20
vertical "|-[view_a]-margin-[view_b]-margin-[view_c]-|"
horizontal "|-margin-[view_a]-margin-|"
horizontal "|-margin-[view_b]-margin-|"
horizontal "|-margin-[view_c]-margin-|"
end
end
Stylesheet extensions
Auto-Layout is just one Stylesheet extension, there are a few others. And if
you want to write your own, just open up the Teacup::Stylesheet
class and
start adding methods.
Autoresizing Masks
If you've used the SugarCube uiautoresizingmask
methods, you'll recognize
these. They are handy, and hopefully intuitive, shorthands for common springs
and struts.
In previous versions of Teacup these were available without needing the
autoresize
prefix. The old methods are still available, but deprecated.
style :container,
autoresizingMask: autoresize.flexible_width | autoresize.flexible_height
# the same, but using block syntax
style :container,
autoresizingMask: autoresize { flexible_width | flexible_height }
style :container,
autoresizingMask: autoresize.fill
The autoresize methods are grouped into four categories: flexible, fill, fixed, and float
. The flexible methods correspond 1::1 with the UIViewAutoresizing*
constants.
The fill
methods (fill,fill_top,fill_bottom,fill_left,fill_right
) will
stretch the width, or height, or both. The location specifies where the view is
pinned, so fill_top
will stretch the width and bottom margin, but keep it the
same distance from the top (not necessarily at the top, but a fixed distance).
fill_right
will pin it to the right side, stretch the height, and have a
flexible left margin.
The fixed
methods pin the view to one of nine locations:
top_left | top_middle | top_right
------------+---------------+-------------
middle_left | middle | middle_right
------------+---------------+-------------
bottom_left | bottom_middle | bottom_right
e.g. fixed_top_left, fixed_middle, fixed_bottom_right
The float
methods fill in the last gap, when you don't want your view pinned
to any corner, and you don't want it to change size.
# incidentally:
float_horizontal | float_vertical == fixed_middle
Device detection
iOS only
Because the stylesheets are defined in a block, you can perform tests for device
and screen size before setting styles. For instance, on an ipad you might want
to have a larger margin than on the iphone.
The Stylesheet
device methods will help you create these conditions:
Teacup::Stylesheet.new do
if device_is? iPhone
margin = 8
elsif device_is? iPad
margin = 20
end
style :container,
frame: [[margin, margin], ["100% - #{margin * 2}", "100% * #{margin * 2}"]]
end
Multiple calls to style
will add those styles, not replace. So this code
works just fine:
Teacup::Stylesheet.new do
style :logo,
origin: [0, 0]
if device_is? iPhone
style :logo, image: UIImage.imageNamed "small logo"
elsif device_is? iPad
style :logo, image: UIImage.imageNamed "big logo"
end
end
Rotation helpers
iOS only
Because you can animate changes to the stylename or style_classes, you can make
it pretty easy to apply rotation effects to a UIView
or CALayer
. The
style_classes
property is especially useful for this purpose.
style :container,
frame: :full
style :rotated,
transform: transform_view.rotate(pi / 2)
style :not_rotated,
transform: transform_view.rotate(0)
style :rotated,
layer: { transform: transform_layer.rotate(pi / 2) }
style :not_rotated,
layer: { transform: transform_layer.rotate(0) }
These work even better when used with the geomotion methods that extend
CGAffineTransform
and CATransform3D
.
style :goofy,
transform: CGAffineTransform.rotate(pi / 2).translate(100, 0).scale(2)
style :regular,
transform: CGAffineTransform.identity
style :regular,
layer: {
transform: CATransform3D.rotate(pi / 2)
}
Showdown
As a recap, here is a translation of traditional Cocoa code done using Teacup.
No cool tricks here, just some plain ol' Cocoa.
class SomeController < UIViewController
def viewDidLoad
@field = UITextField.new
@field.frame = [[10, 10], [200, 50]]
@field.textColor = UIColor.redColor
view.addSubview(@field)
@search = UITextField.new
@search.frame = [[10, 70], [200, 50]]
@search.placeholder = 'Find something...'
@search.textColor = UIColor.redColor
view.addSubview(@search)
end
def willAnimateRotationToInterfaceOrientation(orientation, duration:duration)
case orientation
when UIInterfaceOrientationLandscapeLeft, UIInterfaceOrientationLandscapeRight
@field.frame = [[10, 10], [360, 50]]
@search.frame = [[10, 70], [360, 50]]
else
@field.frame = [[10, 10], [200, 50]]
@search.frame = [[10, 70], [200, 50]]
end
end
end
class SomeController < UIViewController
stylesheet :some_view
def teacup_layout
root(:root)
subview(UITextField, :field)
@search = subview(UITextField, :search)
end
end
Teacup::Stylesheet.new(:some_view) do
style :root,
landscape: true
style :field,
left: 10,
top: 10,
width: 200,
height: 50,
landscape: {
width: 360
}
style :search, extends: :field,
left: 10,
top: 70,
placeholder: 'Find something...'
style UITextField,
textColor: UIColor.redColor
end
The Nitty Gritty
Regarding Style Precedence
You need to be careful when extending styles and using orientation styles
because the precedence rules take some getting used to. The goal is that you
can have all your style code in the stylesheets. But you also need to be able
to animate your views, and rotating the device should not go reseting
everything.
So here's what happens.
When your controller is loaded, viewDidLoad
is called, and that's where Teacup
creates the view hierarchy and applies the styles. It is at the end of the
method that the styles are applied - not until all the views have been added.
The current device orientation will be used so that orientation-specific styles
will be applied.
Now Teacup goes quiet for a while. Your app chugs along... until the user
rotates the device.
If you have orientation-specific styles, they will get applied. But the
original styles (the "generic" styles) will not.
However, there's a way around that, too. If you call restyle!
on a
UIView
, that will reapply all the original stylesheet styles - orientation
and generic styles.
With me so far? Orientation styles are reapplied whenever the device is
rotated. But generic styles are only applied in viewDidLoad
and when
restyle!
is called explicitly.
How does the :extends
property affect things?
If your stylesheet defines orientation-specific styles and "generic" styles, the
orientation-specific styles win. But if you extend a style that has
orientation-specific styles, your local generic styles will win.
The more "local" styles always win - and that applies to styles that you add
using the subview/layout
methods, too. The only time it doesn't really apply
is if you apply styles using UIView#style
or UIView#apply_stylename
. Those
are one-shot (they can get overwritten when restyle!
is called).
There are also times when you either want (or must) override (or add to) the
stylesheet styles. For instance, if you want to assign the delegate
or
dataSource
properties, this cannot be done from a Stylesheet
. But that's
okay, because we have a chance to add these styles in the subview
and layout
methods.
def teacup_layout
subview(UITableView, delegate: self)
end
Styles applied here are one-shot. It is the exact same as assigning the
stylename
and style_classes
and then calling style
. Because the
stylesheet is not necessarily applied immediately, these styles could be
overwritten before they take effect.
def teacup_layout
table_view = subview(UITableView, :tableview, delegate: self,
font: UIFont.boldSystemFontOfSize(10)
)
end
def layoutDidLoad
table_view.apply_stylename(:tableview_init)
end
The idea here is that the closer the style setting is to where the view is
instantiated, the higher the precedence.
More examples!
class MyController < UIViewController
stylesheet :my_sheet
def teacup_layout
subview(UILabel, :label, text: 'overrides')
end
end
Teacup::Stylesheet.new :my_sheet do
style :generic_label,
text: 'portrait',
font: UIFont.boldSystemFontOfSize(10),
textColor: UIColor.grayColor,
landscape: {
font: UIFont.boldSystemFontOfSize(12),
textColor: UIColor.whiteColor,
}
style :label, extends: :generic_label,
font: UIFont.systemFontOfSize(10),
end
Advanced Teacup Tricks
There are times when you might wish teacup "just worked", but please remember:
Teacup is not a "blessed" framework built by Apple engineers. We have access to
the same APIs that you do. That said, here are some use-cases where you can most
definitely use teacup, but you'll need to do a little more leg work.
Trust your parent view - by using springs and struts
...not autolayout
It's been mentioned a few times in this document that Teacup will create & style
views in the viewDidLoad
method. That means that the superview
property of
the controller's view will, necessarily, not be set yet. viewDidLoad
is
called after the view is instantiated (in loadView
), and it hasn't been added
as a subview yet.
Auto-Layout is based on the relationship between two views - often a container
and child view. It's an amazing system, but if that parent view isn't
available, well, you're not gonna have much success.
In the case of a UIViewController your "container" is the self.view
property,
which by default has sensible springs setup so that it stretches to fill the
superview. It's not until you go messing with the self.view
property, or are
not in the context of a UIViewController
that things get hairy.
If this is the case, you should get some pretty obvious warning messages,
something along the lines of Could not find :superview
.
Including Teacup::Layout
on arbitrary classes
I don't know about you, but I often write helper classes for tableviews that
appear on many screens in an app. You should not shy away from adding teacup's
Layout
module to these helper classes.
If you are using your controller as your table view dataSource, the subview
and layout
methods continue to work as you expect them to. This is for the
case when you are using a helper class as the dataSource and/or delegate.
class TableHelper
include Teacup::TableViewDelegate
include Teacup::Layout
stylesheet :table_helper
def tableView(table_view, cellForRowAtIndexPath:index_path)
cell_identifier = 'MyController - cell'
cell = table_view.dequeueReusableCellWithIdentifier(cell_identifier)
unless cell
cell = UITableViewCell.alloc.initWithStyle(UITableViewCellStyleDefault
reuseIdentifier: cell_identifier)
layout(cell.contentView) do
subview(UIImageView, :image)
end
end
return cell
end
end
Calculating values using lambda
All styles can accept a proc, and before the style is applied the proc is called
and you can calculate a value at runtime. There are times when this is simply
convenient, and there are times when it is necessary.
Convenient: when a value is based on some other view's value
style :some_view,
backgroundColor: ->(view) { view.backgroundColor = view.superview.someOtherColor }
Required: when you need to instantiate an object, like a view
style :some_view,
leftView: ->(view) { view.leftView || UILabel.alloc.initWithFrame(...) }
SugarCube + Teacup = Sweettea
SugarCube was born of a desire to make Teacup stylesheets more readable, less
cluttered with Apple's verbose method names and constants. Sweettea takes this
a step further, by implementing a wealth of Teacup handlers that translate
Symbols to constants and provide useful shorthands.
style :button,
normal: { image: 'button-white' },
highlighted: { image: 'button-white-pressed' },
title: 'Submit',
shadow: {
opacity: 0.5,
radius: 3,
offset: [3, 3],
color: :black,
},
font: 'Comic Sans'
style :label,
font: :bold,
alignment: :center,
color: :slateblue
Sweettea also offers some convenient styles that you can extend in your base
class. You might want to either specify the Sweettea version you are using in
your Gemfile, or copy the stylesheet so that changes to Sweettea don't affect
your project. Once that projet is at 1.0 you can rely on the styles not
changing.
style :submit_button, extends: :white_button
style :header, extends: :label
style :login_input, extends: :email_input
style :password_input, extends: :secure_input
Misc notes
Multiple calls to style
with the same stylename combines styles, it doesn't
replace the styles.
Styles are not necessarily applied immediately. They are applied at the end of
the outermost layout/subview
method, including the UIViewController##layout
block. If you call stylename=
or stylesheet=
outside a layout/subview
block, your view will be restyled immediately.
Restyling a view calls restyle!
on all child views, all the way down the tree.
Much care has been taken to call this method sparingly within Teacup.
Any styles that you apply in a layout/subview
method are not retained, they
are applied immediately, and so the stylesheet can (and usually do) override
those styles if there is a conflict. Only styles stored in a stylesheet are
reapplied (during rotation or in restyle!
).
Stylesheets should not be modified once they are created - they cache styles by
name (it is a per orientation cache).
You can add and remove a style_class
using add_style_class
and
remove_style_class
, which will call restyle!
for you if style_classes
array was changed.
If you need to do frame calculations outside of the stylesheet code, you should
do so in the layoutDidLoad
method. This is not necessary, though! It is
usually cleaner to do the frame calculations in stylesheets, either using
geomotion, frame calculations, or auto-layout.
Within a subview/layout
block views are added to the last object in
Layout#superview_chain
. Views are pushed and popped from this array in the
Layout#layout
method, starting with the top_level_view
. If you include
Teacup::Layout
on your own class, you do not have to implement
top_level_view
unless you want to use the subview
method to add classes to a
"default" target.
When UIView
goes looking for its stylesheet
it does so by going up the
responder chain. That means that if you define the stylesheet on a parent view
or controller, all the child views will use that same stylesheet by default. It
also means you can assign a stylesheet to a child view without worrying what the
parent view's stylesheet is.
Caveat! If you implement a class that includes Teacup::Layout
, you can assign
it a stylesheet
. That stylesheet will be used by views created using
layout
or subview
even though your class is probably not part of the
responder chain. Saying that UIView
inherits its stylesheet
from the
responder chain is not accurate; it actually uses teacup_responder
, which
defaults to nextResponder
, but it is assigned to whatever object calls the
layout
method on the view.
If you use Teacup::Appearance
but it is not styling the first screen of your
app (but, strangely, does style all other screens), try calling
Teacup::Appearance.apply
before creating you create the rootViewController
(in your AppDelegate
)..
The Dummy
If you get an error that looks like this:
Objective-C stub for message `setHidesWhenStopped:' type `v@:c' not
precompiled. Make sure you properly link with the framework or library that
defines this message.
You probably need to add your method to dummy.rb. This is a compiler issue,
nothing we can do about it except build up a huge dummy.rb file that has just
about every method that you would want to style. There is a [dummy.rb file for iOS][],
and one for OS X.
If you need to add this method to your project, please give back to the
community by forking teacup and adding this method to the dummy.rb file.
It's easy! Create a subclass, define a method called dummy
, and call the "not
precompiled" message inside it. That will trigger the compiler to include this
method signature.
For instance, lets say you are styling a UIPickerView
and you get the error:
Objective-C stub for message `setShowsSelectionIndicator:' type ...
You would open up dummy.rb and add the following code:
class DummyPickerView < UIPickerView
private
def dummy
setShowsSelectionIndicator(nil)
end
end
Recompile your project, and you should be good to go!
Teacup was born out of the #rubymotion irc chatroom in the early days of
RubyMotion. Its design, direction, and priorities are all up for discussion!
I'm Colin T.A. Gray, the maintainer of the Teacup project. I hope this
tool helps you build great apps!