I have a few goals in this thought:

  1. Implement a UIToolbar, because they are nifty
  2. Add some basic, but ubiquitous, controls, like add, edit, delete
  3. Take a long hard look at the code, and come with a way I would have preferred to write it. This will become my propsal for teacup

teacup is the recently planned DSL that will be developed by the rubymotion community, the first of many we hope! I would love to see a CoreData or sqlite project that can support teacup (saucer?).

First, we create our usual AppDelegate and Controller. I am not going to bother with an iPad controller this time around.

Btw, don't do this!

class MyApplicationController < UIViewController

  def init
    super
    @left_controller = null
    @right_controller = null
  end

end

Apparently it is not cool to do stuff in init... not sure what is up with that.

Building the UIToolbar is pretty easy, though I certainly had my share of missteps.

def viewDidLoad
  toolbar = UIToolbar.alloc.initWithFrame [[0, 416], [320, 44]]
  toolbar.tintColor = rgba_color(21, 78, 118)  # dark aqua blue?
  self.view.addSubview(toolbar)

  toolbar_button = UIBarButtonItem.alloc.initWithBarButtonSystemItem(UIBarButtonSystemItemRefresh,
      target: self,
      action: :flipp)

  toolbar.setItems([toolbar_button], animated:false)
end

def flipp
  # todo...
end

This gives us a toolbar with a "refresh" button at the bottom:

  • toolbar.png

    Toolbar Png

For our views (I'm gonna call them left_view_controller and right_view_controller), we will just have a couple plain views with pretty backgrounds:

class LeftViewController < UIViewController

  def viewDidLoad
    self.view.backgroundColor = rgba_color(0, 136, 14)
    # the default frame is at [0, 20]
    self.view.frame = [[0, 0], [320, 460]]
  end

end


class RightViewController < UIViewController

  def viewDidLoad
    self.view.backgroundColor = rgba_color(16, 110, 255)
    self.view.frame = [[0, 0], [320, 460]]
  end

end

Back in the main ApplicationController, we will assign one or the other to a @current property, which we will then use to determine which one is visible.

def viewDidLoad
  # ...
  @current = left_view_controller
  self.view.insertSubview(@current.view, atIndex:0)
end

def left_view_controller
  # if you can imagine what this looks like in Obj-C, you know how nice
  # this tiny snippet really is.
  @left_view_controller ||= LeftViewController.alloc.init
end

def right_view_controller
  @right_view_controller ||= RightViewController.alloc.init
end

Okay, we're ready to flipp!

def flipp
  remove = @current

  if @current == left_view_controller
    @current = right_view_controller
  else
    @current = left_view_controller
  end

  remove.view.removeFromSuperview
  self.view.insertSubview(@current.view, atIndex:0)
end

This should run now, and when you press the button, the color changes.

LAME

Let's make it animate! I'll try to narrate as we go.

def flipp
  # the name "flipp" does not matter, unless you are doing low-level stuff
  # neither does the context, which needs to be a (void*) anyway, which
  # is some tricky Macruby code...
  UIView.beginAnimations("flipp", context:nil)
  UIView.setAnimationDuration(1.25)
  UIView.setAnimationCurve(UIViewAnimationCurveEaseInOut)

  # k, we've got our "old" controller, and its view is at remove.view
  remove = @current

  if @current == left_view_controller
    @current = right_view_controller
    # different transition, depending on whether we're going "left-right"
    # or "right-left"
    transition = UIViewAnimationTransitionCurlUp
  else
    @current = left_view_controller
    transition = UIViewAnimationTransitionCurlDown
  end

  # transition is easy enough.
  # forView: is the view that "contains" the animation, which is our
  # rootController's view, aka self.view
  # cache: true means that it should take a snapshot before the animation.
  # if you were animating a video, you might not cache it, so that the video
  # would continue during the animation
  UIView.setAnimationTransition(transition,
                forView:self.view,
                cache:true)

  # these get called on the *controller*, not the view.  that is unintuitive to
  # me, so i'm pointing it out.
  remove.viewWillDisappear(true)
  @current.viewWillAppear(true)

  # now do the actual view code
  remove.view.removeFromSuperview
  self.view.insertSubview(@current.view, atIndex:0)

  # and say "done".  the system will take some snapshots and animate them...
  remove.viewDidDisappear(true)
  @current.viewDidAppear(true)

  # ... here!
  UIView.commitAnimations
end
  • page_curl.png

    Page Curl Png

Disclaimer

This is already (today is Jun 6, 2012) an OLD blog post... I will leave it here, for the sake of historical posterity, but this is not at all how teacup ended up. Check out the project at github.com/rubymotion/teacup.

Also, I will continue to write about teacup as it progresses. This is the very first mention of it on my blog (aw, adorable!).

teacup

I promised I would try and re-think this, and that would be my teacup proposal. Here goes.

At one end of the spectrum is the "don't fix what works" argument, which is where we're at right now, and I'm glad that Laurent did not ship rubymotion with a slew of "helpers" or DSLs. Instead, he is the framework/engine guru, and us lowly hackers can mess around with lots of ideas and "see what sticks".

At the other end is the desire to use Ruby to make writing iOS apps really fun, in the spirit of what Sinatra did for writing web apps. As long as the ability to dig back into the "heart" of Cocoa, I think this approach is fun, too, but I've noticed some frameworks out there that don't go far enough, they provide a very small amount sugar. I think we can go further. Go big or go home, right!? :-)

My main paradigm here is for teacup to accept some stuff, you give it a block where you use a DSL that replaces all the crap that Cocoa wants you to type.

Let's start at the AppDelegate. Lots of code, just to get a window to load a view controller. We are always going to build a window with the frame UIScreen.mainScreen.bounds, right? And we'll always makeKeyAndVisible, and always return true? The only thing that changes is the controller class!

Teacup::App do |application, options, window|
  MyApplicationController  # BAM, suckas.
end

WOAH, wait, we need ipad / iphone support!

def ipad?
  UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad
end

Teacup::App do |application, options, window|
  if ipad?
    MyIpadController
  else
    MyIphoneController
  end
end

You do need custom window stuff?

Teacup::Window do |application, options|
  window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
  window.makeKeyAndVisible

  # do your custom stuff here

  window
end

There. Now, in our UIViewController, let's see what we can do about creating the toolbar and buttons.

def viewDidLoad
  toolbar = Teacup::Toolbar(
      frame: [[0, 416], [320, 44]],
      # i like this more than just supporting a bunch of types for :tint, instead
      # add a `color` method to String ('#113B66'.color), Symbol (:black.color), and
      # Array ([bw] => grayscale, [r,g,b], or [r,g,b,a])
      tint: [21, 78, 118].color,
    )
  self.view << toolbar  # that looks like ruby, right?  :-)

  target = self
  toolbar_button = Teacup::BarButtonItem(:refresh) { target.flipp }

  toolbar << toolbar_button  # setItems:animated:true

  @current = left_view_controller
  self.view.insertSubview(@current.view, atIndex:0)
  # we *could* change this, but i don't know...
  self.view[0, 0] = @current.view  # looks weird to me...
end

Another way to do the same thing:

target = self

toolbar = Teacup::Toolbar(
    frame: [[0, 416], [320, 44]],
    tint: [21, 78, 118].color,
  ) do
  # self refers to the newly created UIToolbar, so self.BarButtonItem will
  # know where to add the button
  toolbar_button = self.BarButtonItem(:refresh) { target.flipp }
end

So far so good! Let's mess with this animation crap.

def flipp
  remove = @current

  if @current == left_view_controller
    current = right_view_controller
    transition = :curl_up
  else
    current = left_view_controller
    transition = :curl_down
  end

  @current = current

  Teacup::Animate('flipp',
      # don't need this!  just showing that it is optional
      # context: nil,
      duration: 1.25, ease: :ease_in_out,
      transition: transition,
      view: self.view
    ) do
    # hmmm, not sure how I feel about THIS:
    hide remove
    show current, index: 0
  end
end

lastly,

Let's play with styling.

class LeftViewController < UIViewController

  def viewDidLoad
    Teacup::style(self.view) do
      background_color = [0, 136, 14].color
      frame = [[0, 0], [320, 460]]
      status_bar = :black
      opacity = 0.5
    end
  end

end

class RightViewController < UIViewController

  def viewDidLoad
    Teacup::style(self.view) do
      background_color = [0, 136, 14].color
      frame = [[0, 0], [320, 460]]
      status_bar = :black
      opacity = 0.5
    end
  end

end

Code repetition!?

Not on my watch!

framed_view = Teacup::Style do
  frame = [[0, 0], [320, 460]]
end

colored_view = Teacup::Style(framed_view) do
  status_bar = :black
  opacity = 0.5
end

class LeftViewController < UIViewController

  def viewDidLoad
    Teacup::style(self.view, colored_view) do
      background_color = [0, 136, 14].color
    end
  end

end

class RightViewController < UIViewController

  def viewDidLoad
    Teacup::style(self.view, colored_view) do
      background_color = [0, 136, 14].color
    end
  end

end

The propsal is at teacup proposal