The buzz cut

Ramblings from the barbershop

Ruby Motion Layout Options

| Comments

If you’re new to Ruby Motion, you’ll likely bump into lots of tutorials with completely different syntax. That’s because the Ruby Motion ecosystem hasn’t fully matured yet. There’s yet to be agreed upon on best practices, or a concensus on which gems to use.

This isn’t Rails where we could argue between Devise vs Authlogic, or CarrierWave vs Paperclip. There are handfuls of options for each category in the Ruby Motion ecosystem. Libraries are coming out quickly, and becoming dated quickly. A blog post from 6-12 months ago can contain dated recommendations (as this one will be soon enough).

For this blog post, I’ll zero in on one of those categories: creating layouts and styling your views. I’ll walk you through a handful of options. From, IMHO, worst to best.

All my examples below have been implemented in a simple Ruby Motion iOS app using ProMotion. For those who don’t know ProMotion, it’s a gem that “abstracts a ton of boilerplate UIViewController, UINavigationController, and other iOS code into a simple, Ruby-like DSL.” For the code examples below, the most important thing to know is that what are called controllers in native iOS development, are called screens in ProMotion.

Interface Builder

When I first started working with Ruby Motion, I had no idea how to programmatically create layouts, so I started with the easiest option: Xcode’s interface builder.

Interface builder “makes it simple to design a full user interface without writing any code. Simply drag and drop windows, buttons, text fields, and other objects onto the design canvas to create a functioning Mac, iPhone, or iPad user interface.”

The layouts you create in interface builder are saved as .xib files, which are XML files that ultimately get compiled to .nib files. Lucky for us, RubyMotion has no problem loading and compiling these files.

I created the following screen in interface builder, then saved it as root_view.xib in the resources directory of my Ruby Motion app.

Interface Builder

To load/compile the .xib into my app, I did the following:

app/screens/root_screen.rb
1
2
3
4
5
6
7
8
class RootScreen < PM::Screen
  title "Home"

  def on_load
    views = NSBundle.mainBundle.loadNibNamed "root_view", owner:self, options:nil
    self.view = @views[0]
  end
end

The result of that code is a rather ugly, but perfectly working iOS screen:

Interface Builder

That’s all fine and dandy, but now I want to click that button and have it change the “Hello world” text to “Goodbye world.” So, my next step is to access my label and button from the .nib.

This is where things start to get a little weird. Back in interface builder, I have to tag each element. You can find the ‘tag’ field in the Attributes Inspector. Oh, and tags can only be integers, because Apple hates you.

So, after tagging my label with 1 and my button with 2, I can refer to those items in my code like this:

app/screens/root_screen.rb
1
2
3
4
5
6
7
8
9
10
11
class RootScreen < PM::Screen
  title "Home"

  def on_load
    views = NSBundle.mainBundle.loadNibNamed "RootView", owner:self, options:nil
    self.view = views[0]

    @label = view.viewWithTag 1
    @button = view.viewWithTag 2
  end
end

Now the @button instance variable will contain an instance of UIButton. To add a tap event to @button, I use the iOS addTarget method:

app/screens/root_screen.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class RootScreen < PM::Screen
  title "Home"

  def on_load
    views = NSBundle.mainBundle.loadNibNamed "RootView", owner:self, options:nil
    self.view = views[0]

    @label = view.viewWithTag 1
    @button = view.viewWithTag 2
  end

  def will_appear
    @button.addTarget self, action:'button_tapped:', forControlEvents:UIControlEventTouchUpInside
  end

  def button_tapped(button)
    @label.text = 'Goodbye world'
  end
end

That works just fine, but one thing you’ll notice is some of the code is using camelCase, and some is using snake_case. Ew! The reason for that is the mixture of ruby code with iOS API calls.

I think the Ruby Motion team, in an effort to better align with objective c, used camel case, even though ruby is generally written in snake case. Personally, I’m uncomfortable writing ruby code in camel case. There’s a good article about this debate on the MontionInMotion Blog.

The other problem with interface builder is Xcode sucks. Opening Xcode to push pixels around is annoying. Plus if you’re going to spend your days in Xcode, learn Swift. I avoid interface builder in my projects.

I’m a developer, I want to code my interface!

Coding your own layouts from scratch

Everything I did in interface builder can be done programmatically. I’ll start by coding up that view from scratch, in the ugliest way possible (though, still prettier than objective-c).

app/screens/root_screen.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class RootScreen < PM::Screen
  title "Home"

  def on_load
    view.backgroundColor = hex_color("#4bacc1")

    @label = UILabel.alloc.initWithFrame(CGRectMake(150, 50, 100, 100))
    @label.color = hex_color("#ffffff")
    @label.text = "Hello world"

    view.addSubview(@label)

    @button = UIButton.buttonWithType(UIButtonTypeRoundedRect)
    @button.frame = CGRectMake(120, 200, 150, 40)
    @button.setTitle("Click me", forState: UIControlStateNormal)
    @button.setTitleColor(hex_color("#ffffff"), forState: UIControlStateNormal)
    @button.backgroundColor = hex_color("#08677f")

    view.addSubview(@button)
  end

  def will_appear
    @button.addTarget self, action:'button_tapped:', forControlEvents:UIControlEventTouchUpInside
  end

  def button_tapped(button)
    @label.text = 'foo'
  end
end

Above, I used UILabel.alloc.initWithFrame to instantiate a UILabel. I passed it frame with CGRectMake (x position, y position, width, height). I then set the label attributes (color & text), and added it to the parent view. The button is then created in a similar fashion.

That’s a lot of code for a label and a button. And it’s ugly code at that. Also, I’ve lost the autolayout I got for free when using interface builder. I can take care of that with additional code, but there’s gotta be a better way to programmatically create this screen, right?

Pixate Freestyle

Pixate Freestyle is a “native iOS (and Android) library that styles native controls with CSS.” Freestyle doesn’t have APIs for creating UI elements, but it does allow you to position and style your elements using familiar CSS. There’s a gem for bringing Freestyle into a Ruby Motion project.

I followed the setup instructions on https://github.com/Pixate/RubyMotion-PixateFreestyle, including adding a default.css to the resources directory. Then, I converted my app to use Freestyle.

app/screens/root_screen.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class RootScreen < PM::Screen
  title "Home"

  def on_load
    view.styleId = 'root_view'

    @label = UILabel.alloc.initWithFrame(CGRectNull)
    @label.text = "Hello world"
    view.addSubview(@label)

    @button = UIButton.buttonWithType(UIButtonTypeRoundedRect)
    @button.setTitle("Select your video", forState: UIControlStateNormal)
    view.addSubview(@button)
  end

  def will_appear
    @button.addTarget self, action:'button_tapped:', forControlEvents:UIControlEventTouchUpInside
  end

  def button_tapped(button)
    @label.text = 'foo'
  end
end

Not a lot has changed in my screen. I’ve added a styleId to my parent view, and I’ve removed the code that set the positioning and styles of my elements. That is now handled in my default.css. Note: you can use scss syntax with Freestyle, but I chose to use standard css for this example.

resources/default.css
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#root_view {
  background-color: #4bacc1;
}

#root_view label {
  color: #fff;
  font-size: 20px;
  width: 100px;
  height: 100px;
  top: 50px;
  left: 150px;
}

#root_view button {
  background-color: #08677f;
  color: #fff;
  width: 150px;
  height: 40px;
  top: 200;
  left: 120;
}

I think Freestyle is a decent option, especially for those who have a css guru in-house to tackle frontend work. However, it still has position limitations, and, unlike ProMotion, I felt like I was straying too far from the native iOS path by using CSS. I liked the separation of styling, but I wanted tighter programmatic access to my styles, and a DSL closer to the native iOS API.

And that brings us to our final two options: motion-kit and Ruby Motion Query.

MotionKit

MotionKit is the succesor of TeaCup. It provides a relatively clean DSL for styling and laying out your screens. It also has a great frame/geometry library, and can handle auto layouts via constraint blocks.

MotionKit attempts to extract all styling and layout code from your screens. For example:

app/screens/root_screen.rb
1
2
3
4
5
6
7
8
class RootScreen < PM::Screen
  title "Home"

  def on_load
    @layout = RootLayout.new
    self.view = @layout.view
  end
end
app/layouts/root_layout.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class RootLayout < MotionKit::Layout
  def layout
    background_color rmq.color("#4bacc1")

    add UILabel, :label
    add UIButton, :button
  end

  def label_style
    text 'Hello world'
    text_alignment UITextAlignmentCenter
    text_color rmq.color("#ffffff")
    height 100
    width 100
    center ['50%', 100]
  end

  def button_style
    title "Click me!"
    height 40
    width 150
    center ['50%', 200]
    background_color rmq.color("#08677f")
    title_color rmq.color('#ffffff')
  end
end

In the layout method, which is automatically called when I instanciate my layout in my screen, I used some clean syntax for adding my label and button to the parent view. Then I used MotionKit’s convention ([element_name]_style) for creating two styling methods (label_style and button_style).

One thing you’ll notice is when I set colors, I’m using RMQ’s .color method.MotionKit doesn’t have a nifty way to reference a hex color, so you’ll want to use something like RMQ or SugarCube for that.

Now I have to make my button work again, which means in my screen, I need a way to access the elements created in my layout. Luckily MotionKit provides an easy way to do that via @layout.get(:element_name).

app/screens/root_screen.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class RootScreen < PM::Screen
  title "Home"

  def on_load
    @layout = RootLayout.new
    self.view = @layout.view

    @label = @layout.get(:label)
    @button = @layout.get(:button)
  end

  def will_appear
    @button.addTarget self, action:'button_tapped:', forControlEvents:UIControlEventTouchUpInside
  end

  def button_tapped(button)
    @label.text = 'foo'
  end
end

So, similar to Pixate, we’ve separated our design from our business logic, but with MotionKit, we have access to a bunch of syntactic sugar for dealing with the device’s geometry, and we’re not using CSS.

However, I find MotionKit breaks down when you’re building screens dynamically based on data from an API. Going between the layout and the screen isn’t always as easy as my example above, and the documentation is lacking (to say the least).

Furthermore, you’ll need SugarCube or RMQ. And if you’re going to add RMQ, why not use RMQ for the layout and styling too? Which brings us to the final example, and my personal favorite.

Ruby Motion Query (RMQ)

Ruby Motion Query, describes itself as “Fast, non-polluting, & chaining; it’s like jQuery for RubyMotion + stylesheets, animations, events, and more.” The ‘jQuery’ part actually turned me off initially, as it made me think of the javascript frameworks attempting native iOS experiences. Maybe that’s just me. Regardless, I’m glad I gave it a try, because it’s amazing.

Similar to MotionKit, RMQ moves styling and layout out of the screen, but IMHO, provides much better access to your screen from the “stylesheets” and vice versa.

Here’s my screen implemented in RMQ:

1
2
3
4
5
6
7
8
9
10
11
class RootScreen < PM::Screen
  title "Home"

  def on_load
    rmq.stylesheet = RootStylesheet
    rmq(self.view).apply_style :root_view

    rmq.append(UILabel, :label)
    rmq.append(UIButton, :button)
  end
end

RMQ’s concept of stylesheets is similar to MotionKit’s concept of layouts. With both, you can reuse styles throughout your app, though RMQs documentation is clearer on how to achieve this. In fact, RMQ requires an application_stylesheet to enforce this concept of shared styles, including named colors, named font styles, etc.

You’ll also see that with RMQ, I’m building my layout directly within the screen. The stylesheet is limited to styling and positioning.

app/stylesheets/root_stylesheet.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class RootStylesheet < ApplicationStylesheet

  def root_view(st)
    st.background_color = rmq.color("#4bacc1")
  end

  def label(st)
    st.frame = {t: 50, h:100, w: 100, centered: :horizontal}
    st.text_alignment = :center
    st.text = 'Hello world'
    st.color = rmq.color("#ffffff")
  end

  def button(st)
    st.frame = {t: 200, h:40, w: 150, centered: :horizontal}
    st.text = 'Click me!'
    st.color = rmq.color("#ffffff")
    st.background_color = rmq.color("#08677f")
  end
end

Again, we have convention-based method names (element_name), all of which automatically take in a Styler. RMQ comes with stylers for all common elements, which can easily be extended. These stylers provide consistency that MotionKit does not have. For example, setting the background color of a button in RMQ is simply st.background_color, just like a label, whereas in MotionKit we have to use title_color (closer to the actual iOS property). Little details like that are a huge pro for RMQ. I do want to be comfortable with the iOS API, but I’m a ruby developer and a Rails guy who wants to spend time writing code, not looking up property names.

MotionKit has a fantastic frame/geometry library, allowing you to position elements relative to other elements, relative to the screen size, etc. It also has canned animations, gesture support, a grid (!), validations (!!), and more.

Finally, making a syntactical mistake in RMQ rarely causes your app to crash. RMQ catches the error and provides awesome debug information in the console.

So, how to we make our button work in RMQ? Let’s take a look:

app/screens/root_screen.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class RootScreen < PM::Screen
  title "Home"

  def on_load
    rmq.stylesheet = RootStylesheet
    rmq(self.view).apply_style :root_view

    @label = rmq.append(UILabel, :label).get
    @button = rmq.append(UIButton, :button).get
  end

  def will_appear
    @button.addTarget self, action:'button_tapped:', forControlEvents:UIControlEventTouchUpInside
  end

  def button_tapped(button)
    @label.text = 'foo'
  end
end

RMQ has chaining syntax, so after defining my elements, I can simply add .get to set my instance variables. That’s it.

If I wanted to hide my label after clicking the button, I can use RMQs hide method.

app/screens/root_screen.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class RootScreen < PM::Screen
  title "Home"

  def on_load
    rmq.stylesheet = RootStylesheet
    rmq(self.view).apply_style :root_view

    @label = rmq.append(UILabel, :label).get
    @button = rmq.append(UIButton, :button).get
  end

  def will_appear
    @button.addTarget self, action:'button_tapped:', forControlEvents:UIControlEventTouchUpInside
  end

  def button_tapped(button)
    rmq(@label).hide
  end
end

And if I wanted to use cleaner syntax for handling the button click, I could do this:

app/screens/root_screen.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class RootScreen < PM::Screen
  title "Home"

  def on_load
    rmq.stylesheet = RootStylesheet
    rmq(self.view).apply_style :root_view

    @label = rmq.append(UILabel, :label).get
    @button = rmq.append(UIButton, :button).get
  end

  def will_appear
    rmq(@button).on(:touch) do |sender|
      button_tapped(sender)
    end
  end

  def button_tapped(button)
    rmq(@label).hide
  end
end

RMQ’s documentation is superb, the community is active, and the maintainers are super helpful. I can’t recommend it enough. In fact, I highly recommend you take a look at RedPotion which is setting the bar for RubyMotion development by combining the most popular gems, adding some helpful generators, and improving on RMQ’s live stylesheet reloading in the simulator. RedPotion is a true sign that the RubyMotion community is maturing!

Comments