The RubyMotion layout and styling gem. Follow @MotionKit on twitter for updates and commit notifications
The RubyMotion layout and styling gem. Follow @MotionKit on twitter for updates and commit notifications
The RubyMotion layout and styling gem.
You can read all about why Colin decided that Teacup needed to be replaced with a new project, rather than upgraded or refactored.
If you need to update your app to use MotionKit, see READMORE.md for an example of migrating stylesheets, styles, and constraints.
In your Gemfile
gem 'motion-kit'
From your controller you will instantiate a MotionKit::Layout instance, and
request views from it. layout.view is the root view, and it's common to
assign this to self.view in your loadView method. You'll also want to hook
up your instance variables, using layout.get(:id) or using instance variables.
…
In MotionKit, it is easy to add views on the fly using the same API as used during layout.
def add_button style, button_title
context get(:inputs) do #Two very useful methods for accessing/modifying previously added views
add UIButton, :dynamic_button do
title button_title
constraints do # if using autolayout
...
end
end
end
end
During layout, z-order is determined by the sequence in which views are added to the hierarchy. You can control this dynamically by supplying :behind, :in_front_of, or :z_index options (:z_index not supported in OS X)
add UIImageView, :highlight_square, behind: get(:dynamic_button)
add UIImageView, :x_marks_the_spot, in_front_of: @selected_label
add UILabel, :subterranian_marker, z_index: 4 #becomes the 4th view in the subview hierarchy
In MotionKit, when you define a method that has the same name as a view stylename with the suffix "_style", that method is called and is expected to style that view.
class LoginLayout ChildLayout
Calling get(:child_id).view will return the view associated with that
layout.
If you need to use a custom root view, you can use the root method from within
the layout method. When you create or assign the root view this way, you must
assign subviews and styles inside a block that you pass to root.
def layout
root(SomeOtherViewclass) do
add UILabel
end
end
You can also pass in a root view to your layout, like this:
def loadView
@layout = MyLayout.new(root: self.view).build
end
Make sure to call .build; otherwise, the layout will be returned but the view not built.
In this case, if you want to style the root view, just refer to it in your layout:
def layout
root :my_root_view do
# ...
end
end
def my_root_view_style
background_color UIColor.grayColor
end
This is especially useful with collection views, table views, and table cells, where you can assign a root view explicitly:
return MyCellLayout.new(root: cell).build
Keep in mind that MotionKit will not retain a strong reference when you provide a root view, so retain it yourself to prevent it from being deallocated.
If you've used RMQ's Stylers, you'll recognize a very similar pattern here. In
RMQ the 'style' methods are handed a 'Styler' instance, which wraps access to
the view. In MotionKit we make use of method_missing to call these methods
indirectly. That takes care of most methods related to styling, but you might
want to write some "helper" methods so that your styling code is more concise.
Some examples are included in the MotionKit core, but the SweetKit gem has
many more. If you are writing helpers for UIKit or AppKit, please consider
adding them to SweetKit, so we can all share in the productivity boost! :smiley:
def login_label_style
text 'Press me' # this gets delegated to UILabel#text
end
# It's not hard to add extensions for common tasks, like setting the "normal"
# title on a UIButton
def login_button_style
title 'Press me'
# this gets delegated to UIButtonHelpers#title(title), which in turn calls
# button.setTitle(title, forState: UIControlStateNormal)
# See uibutton_helpers.rb for implementation.
end
MotionKit offers shortcuts and mini-DSLs for frames, auto-layout, and
miscellaneous helpers. But if a method is not defined, it is sent to the view
after a little introspection. If you call a method like title_color value, MotionKit
will try to call:
setTitle_color(value)title_color=(value)title_color(value)setTitleColor(value)titleColor=(value)titleColor(value)raise NoMethodError def login_button_style
background_color UIColor.clearColor # this gets converted to `self.target.backgroundColor = ...`
end
Introspection and method_missing add a little overhead to your code, but in our benchmarking it is insignificant and undetectable. Let us know if you find any performance issues.
You can easily add your own helpers to MotionKit. They
should all be named consistenly, e.g. MotionKit::UIViewHelpers,
MotionKit::UILabelHelpers, etc. You just need to specify the "target class" that
your helper class is meant to work with. Each class can only have one helper
class.
module MotionKit
# these helpers will only be applied to instances of UILabel and UILabel
# subclasses
class UILabelHelpers 0.5
yield
end
end
end
end
For your own custom classes, or when you want to write
helper methods for a built-in class, you will need to write a class that
"targets" that class. This will be a subclass of MK::UIViewHelpers; it looks
and feels like a MK::Layout subclass, but these classes are used to extend
the MotionKit DSL, and should not be instantiated or used to build layouts.
Again, to be clear: you should be subclassing MK::Layout when you build your
controller layouts, and you should write a subclass of MK::UIViewHelpers only
when you are adding extensions to the MotionKit DSL.
# Be sure to extend an existing Helpers class, otherwise you'll lose a lot of
# functionality. Often this will be `MK::UIViewHelpers` on iOS and
# `MK::NSViewHelpers` on OS X.
class CustomViewHelpers = { x: 5, y: 10 }
top_left.is =
x.is >= 10
x.is == 15
# setting the priority:
(x.is >= 10).priority(:required)
(x.is == 15).priority(:low)
# setting the identifier
x.equals(15).identifier('foo')
end
But of course with AutoLayout you set up relationships between views. Using the element-id as a placeholder for a view works especially well here.
…
Just like with frame helpers you can use the :element_id to refer to another
view, but get this: the view need not be created yet! This is because when you
setup a constraints block, it isn't resolved immediately; the symbols are
resolved at the end. This feature uses the deferred method behind the scenes
to accomplish this.
…
One common use case is to use a child layout to create many instances of the
same layout that repeat, for instance a "row" of content. In this case you will
probably have many views with the same id, and you will not know the index of
the container view that you want to add constraints to. In this situation, use
the nearest, prev or next method to find a container, sibling, or
child view.
prev and next are easy; they just search for a sibling view. No
superviews or subviews are searched.
nearest will search child views, siblings, and superviews, in that order. The
"distance" is calculated as such:
See the AutoLayout sample app for an example of this usage.
items.each do |item|
add UIView, :row do
add UIImageView, :avatar
add UILabel, :title
end
end
def title_style
constraints do
# center the view vertically
center.equals(nearest(:row))
# and place it to the right of the :avatar
left.equals(nearest(:avatar), :right).plus(8)
right.equals(nearest(:row)).minus(8)
end
end
One pain point in working with constraints is determining when to add them to your views. We tried really hard to figure out a way to automatically add them, but it's just an untenable problem (Teacup suffers from a similar conundrum).
Essentially, the problem comes down to this: you will often want to set
constraints that are related to the view controller's view, but those must be
created/set after controller.view = @layout.view. Without doing some crazy
method mangling on NS/UIView we just can't do this automatically
Long story short: If you need to create constraints that refer to the controller view, you need to use a separate method that is called after the view hierarchy is created.
class MainLayout do
self.view.layoutIfNeeded # applies the constraint change
end, completion: nil)
You can also activate/deactivate constraints selectively, and animate the transitions between them.
class MyLayout do
self.view.layoutIfNeeded
end)
end
def hide_button
@bottom_constraint.deactivate
@top_constraint.activate
UIView.animateWithDuration(0.3, animations: -> do
self.view.layoutIfNeeded
end)
end
end
gem install motion-kit-events
Adds on :event and trigger :event methods to MK::Layout objects. These
can be used to send events from the Layout to your controller, further
simplifying the controller code (and usually making it more testable). See the
MotionKit::Events documentation for more information.
gem install motion-kit-templates
Adds project templates, for use with motion create.
motion create foo --template=mk-ios
motion create foo --template=mk-osx
These are available on iOS.
add UIView, :container do
portrait do
fram
No open issues yet, or sync has not completed.