Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Some sugar for your cocoa, or your tea. RubyMotion helpers.

Fetching latest commit…

Octocat-spinner-32-eaf2f5

Cannot retrieve the latest commit at this time

Octocat-spinner-32 lib
Octocat-spinner-32 .gitignore
Octocat-spinner-32 LICENSE
Octocat-spinner-32 README.md
Octocat-spinner-32 Rakefile
Octocat-spinner-32 sugarcube.gemspec
README.md

sugarcube

Some sugar for your cocoa, or your tea.

About

CocoaTouch/iOS is a verbose framework. These extensions hope to make development in rubymotion more enjoyable by tacking "UI" methods onto the base classes (String, Fixnum, Numeric). With sugarcube, you can create a color from an integer or symbol, or create a UIFont or UIImage from a string.

Some UI classes are opened up as well, like adding the '<<' operator to a UIView instance, instead of view.addSubview(subview), you can use the more idiomatic: view << subview.

The basic idea of sugarcube is to turn some operations on their head. Insead of

UIApplication.sharedApplication.openURL(NSUrl.URLWithString(url))

How about:

url.nsurl.open

DISCLAIMER

It is possible that you will not like sugarcube. That is perfectly fine! Some people take milk in their coffee, some take sugar. Some crazy maniacs don't even drink coffee, if you can imagine that... All I'm saying is: to each their own. You should checkout BubbleWrap for another take on Cocoa-wrappage.

CONTRIBUTIONS

SugarCube started out as a Fusionbox project (see the announcement), but as its popularity increased, the decision was made to offer it to the rubymotion community, in the spirit of open-source and collaboration. It is a great compliment to teacup, especially when paired with sweettea!

Installation

gem install sugarcube

# or in Gemfile
gem 'sugarcube'

# in Rakefile
require 'sugarcube'

Examples

Array

[1, 3].nsindexpath  # NSIndexPath.indexPathWithIndex(1).indexPathByAddingIndex(3)

Fixnum

# create a UIColor from a hex value
0xffffff.uicolor # => UIColor.colorWithRed(1.0, green:1.0, blue:1.0, alpha:1.0)
0xffffff.uicolor(0.5) # => UIColor.colorWithRed(1.0, green:1.0, blue:1.0, alpha:0.5)

Numeric

# create a percentage
100.0.percent # => 1.00
55.0.percent # => 0.55

1.3.seconds.later do
  @someview.fade_out
end

NSURL

# see String for easy URL creation
"https://github.com".nsurl.open  # => UIApplication.sharedApplication.openURL(NSURL.URLWithString("https://github.com"))

NSString

# UIImage from name
"my_image".uiimage  # => UIImage.imageNamed("my_image")
"blue".uicolor  # => UIColor.colorWithPatternImage(UIImage.imageNamed("blue"))

# UIFont from name
"my_font".uifont # => UIFont.fontWithName("my_font", size:UIFont.systemFontSize)
"my_font".uifont(20) # => UIFont.fontWithName("my_font", size:20)

# UIColor from color name OR image name OR hex code
"blue".uicolor == :blue.uicolor # => UIColor.blueColor
"#ff00ff".uicolor == :fuchsia.uicolor == 0xff00ff.uicolor # => UIColor.colorWithRed(1.0, green:0.0, blue:1.0, alpha:1.0)
"#f0f".uicolor(0.5) == :fuchsia.uicolor(0.5) == 0xff00ff.uicolor(0.5) # => UIColor.colorWithRed(1.0, green:1.0, blue:1.0, alpha:0.5)
# note: 0xf0f.uicolor == 0x00f0f.uicolor.  There's no way to tell the difference
# at run time between those two Fixnum literals.
"my_image".uicolor == "my_image".uiimage.uicolor # => UIColor.colorWithPatternImage(UIImage.imageNamed("my_image"))

# NSLocalizedString from string
"hello".localized  # => NSBundle.mainBundle.localizedStringForKey("hello", value:nil, table:nil)
"hello"._          # == "hello".localized
"hello".localized('Hello!', 'hello_table')  # => ...("hello", value:'Hello!', table:'hello_table')

# file location
"my.plist".exists?   # => NSFileManager.defaultManager.fileExistsAtPath("my.plist")
"my.plist".document  # => NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0].stringByAppendingPathComponent("my.plist")

# NSURL
"https://github.com".nsurl  # => NSURL.URLWithString("https://github.com")

Symbol

This is the "big daddy". Lots of sugar here...

:center.uialignment  # => UITextAlignmentCenter
:upside_down.uiorientation  # => UIDeviceOrientationPortraitUpsideDown
:rounded.uibuttontype  # => UIButtonTypeRoundedRect
:highlighted.uicontrolstate  # => UIControlStateHighlighted
:touch.uicontrolevent  # => UIControlEventTouchUpInside
:changed.uicontrolevent  # => UIControlEventValueChanged
:all.uicontrolevent  # => UIControlEventAllEvents
:blue.uicolor  # UIColor.blueColor
# all CSS colors are supported, and alpha
# (no "grey"s, only "gray"s, consistent with UIKit, which only provides "grayColor")
:firebrick.uicolor(0.25)  # => 0xb22222.uicolor(0.25)
:bold.uifont  # UIFont.boldSystemFontOfSize(UIFont.systemFontSize)
:bold.uifont(10)  # UIFont.boldSystemFontOfSize(10)
:small.uifontsize # => UIFont.smallSystemFontSize
:small.uifont  # => UIFont.systemFontOfSize(:small.uifontsize)
:bold.uifont(:small)  # UIFont.boldSystemFontOfSize(:small.uifontsize)
:large.uiactivityindicatorstyle  # :large, :white, :gray
:bar.uisegmentedstyle   # :plain, :bordered, :bar, :bezeled

# UITableView and UITableViewCell have LOTS of associated constants... I'm
# adding them as I come across them.
:automatic.uitablerowanimation   # or .uitableviewrowanimation
:default.uitablecellstyle        # or .uitableviewcellstyle
:disclosue.uitablecellaccessory  # or .uitableviewcellaccessorytype
:blue.uitablecellselectionstyle  # or .uitableviewcellselectionstyle

UIImage

image = "my_image".uiimage
image.uicolor # => UIColor.colorWithPatternImage(image)

UIAlertView

Accepts multiple buttons and success and cancel handlers. In its simplest form, you can pass just a title and block.

# simple
UIAlertView.alert "This is happening, OK?" { self.happened! }
# a little more complex
UIAlertView.alert("This is happening, OK?", buttons: ["Nevermind", "OK"],
  message: "don't worry, it'll be fine.") {
  self.happened!
}

# Full on whiz bangery.  Note the success block takes the pressed button, but as
# a string instead of an index.  The cancel button should be the first entry in
# `buttons:`
UIAlertView.alert "I mean, is this cool?", buttons: %w[No! Sure! Hmmmm]
  message: "No going back now",
  cancel: { self.cancel },
  success: { |pressed| self.proceed if pressed == "Sure!" }

UIView

self.view << subview  # => self.view.addSubview(subview)
self.view.show  # => self.hidden = false
self.view.hide  # => self.hidden = true
Animations

jQuery-like animation methods.

# default timeout is 0.3
view.fade_out { |view|
  view.removeFromSuperview
}
# options:
view.fade_out(0.5, delay: 0,
                  options: UIViewAnimationOptionCurveLinear,
                  opacity: 0.5) { |view|
  view.removeFromSuperview
}

view.move_to([0, 100])  # move to position 0, 100
view.delta_to([0, 100])  # move over 0, down 100, from current position

view.slide :left   # slides the entire view one "page" to the left, right, up, or down

view.shake  # shakes the view.
# options w/ default values:
shake offset: 8,   # move 8 px left, and 8 px right
      repeat: 3,   # three times
    duration: 0.3, # for a total of 0.3 seconds
     keypath: 'transform.translate.x'

# vigorous nodding - modifying transform.translation.y:
view.shake offset: 20, repeat: 10, duration: 5, keypath: 'transform.translation.y'
# an adorable wiggle - modifying transform.rotation:
superview.shake offset: 0.1, repeat: 2, duration: 0.5, keypath: 'transform.rotation'
View factories
UIButton
UIButton.buttonWithType(:custom.uibuttontype)
# =>
UIButton.custom

UIButton.custom            => UIButton.buttonWithType(:custom.uibuttontype)
UIButton.rounded           => UIButton.buttonWithType(:rounded.uibuttontype)
UIButton.rounded_rect      => UIButton.buttonWithType(:rounded_rect.uibuttontype)
UIButton.detail            => UIButton.buttonWithType(:detail.uibuttontype)
UIButton.detail_disclosure => UIButton.buttonWithType(:detail_disclosure.uibuttontype)
UIButton.info              => UIButton.buttonWithType(:info.uibuttontype)
UIButton.info_light        => UIButton.buttonWithType(:info_light.uibuttontype)
UIButton.info_dark         => UIButton.buttonWithType(:info_dark.uibuttontype)
UIButton.contact           => UIButton.buttonWithType(:contact.uibuttontype)
UIButton.contact_add       => UIButton.buttonWithType(:contact_add.uibuttontype)
UITableView
UITableView.alloc.initWithFrame([[0, 0], [320, 480]], style: :plain.uitableviewstyle)
# custom frame:
UITableView.alloc.initWithFrame([[0, 0], [320, 400]], style: :grouped.uitableviewstyle)

# =>

UITableView.plain
# custom frame:
UITableView.grouped([[0, 0], [320, 400]])
UISegmentedControle
control = UISegmentedControl.alloc.initItems(["one", "ah-two-whoo", "thr-r-r-ree"])
control.segmentedControlStyle = :bar.uisegmentedstyle

# =>

UISegmentedControl.bar(["one", "ah-two-whoo", "thr-r-r-ree"])

UIControl

Inspired by BubbleWrap's when method, but I prefer jQuery-style verbs and sugarcube symbols.

button = UIButton.alloc.initWithFrame([0, 0, 10, 10])

button.on(:touch) { my_code }
button.on(:touchupoutside, :touchcancel) { |event|
  puts event.inspect
  # my_code...
}

# remove handlers
button.off(:touch, :touchupoutside, :touchcancel)
button.off(:all)

You can only remove handlers by "type", not by the action. e.g. If you bind three :touch events, calling button.off(:touch) will remove all three.

UINavigationController

push/<< and pop instead of pushViewController and popViewController. ! and !(view) instead of popToRootViewController and popToViewController

animated is true for all these.

nav_ctlr.push(new_ctlr)
nav_ctlr << new_ctlr
nav_ctlr.pop
nav_ctlr.!
nav_ctlr.!(another_view_ctlr)

UITabBarController

I have mixed feelings about adding this extension, but I needed it, so maybe you will, too... Usually UITabBarControllers have a static number of tabs, but in my case, I needed to be able to add one later, when a certain condition was met.

controllers = tabbar_ctlr.viewControllers
controllers << new_ctlr
tabbar_ctlr.setViewControllers(controllers, animated: true)

# =>

tabbar_ctlr << new_ctlr

NSNotificationCenter

Makes it easy to post a notification to some or all objects.

# this one is handy, I think:
"my notification".post_notification  # => NSNotificationCenter.defaultCenter.postNotificationName("my notification", object:nil)
"my notification".post_notification(obj)  # => NSNotificationCenter.defaultCenter.postNotificationName("my notification", object:obj)
"my notification".post_notification(obj, user: 'dict')  # => NSNotificationCenter.defaultCenter.postNotificationName("my notification", object:obj, userInfo:{user: 'dict'})

NSTimer

1.second.later do
  @view.shake
end

1.second.every do
  @view.shake
end

# since that looks funny, an every method is available in the SugarCube::Timer module
include SugarCube::Timer
every 1.minute do
  puts "tick"
end

# might as well make an alias
after 1.minute do
  puts "ding!"
end

# other time-related methods
1.second  || 2.seconds
1.minute  || 2.minutes  # 60 seconds
1.hour    || 2.hours    # 60 minutes
1.day     || 2.days     # 24 hours
1.week    || 2.weeks    # 7 days
# with sensible values for 'month' and 'year', even though we all know you can't
# **really** define them this way (go back to python if you find your brain
# hemorrhaging):
1.month   || 2.months   # 30 days
1.year    || 2.years    # 365 days

NSUserDefaults

'key'.set_default(['any', 'objects'])  # => NSUserDefaults.standardUserDefaults.setObject(['any', 'objects'], forKey: :key)
'key'.get_default  # => NSUserDefaults.standardUserDefaults.objectForKey(:key)

# symbols are converted to strings, so theses are equivalent
:key.set_default(['any', 'objects'])  # => NSUserDefaults.standardUserDefaults.setObject(['any', 'objects'], forKey: :key)
:key.get_default  # => NSUserDefaults.standardUserDefaults.objectForKey(:key)

This is strange, and backwards, which is just sugarcube's style. But there is one advantage to doing it this way. Compare these two snippets:

# BubbleWrap
App::Persistance[:test] = { my: 'test' }
# sugarcube
:test.set_default { my: 'test' }
# k, BubbleWrap looks better

App::Persistance[:test][:my] == 'test'  # true
:test.get_default[:my]  # true, and odd looking - what's my point?

App::Persistance[:test][:my] = 'new'  # nothing is saved.  bug
:test.get_default[:my] = 'new'  # nothing is saved, but that's *obvious*

test = App::Persistance[:test]
test[:my] = 'new'
App::Persistance[:test] = test  # saved

test = :test.get_default
test[:my] = 'new'
:test.set_default test

CoreGraphics

Is it CGMakeRect or CGRectMake?

Instead, just use Rect, Size and Point. They will happily convert most sensible arguments into a Rect/Size/Point, which can be treated as a CGRect object OR as an Array (woah).

These are namespaced in SugarCube::CoreGraphics module, but I recommend you include SugarCube::CoreGraphics in app_delegate.rb.

f = Rect(view.frame)  # converts a CGRect into a Rect
o = Point(view.frame.origin)  # converts a CGPoint into a Point
s = Size(view.frame.size)  # converts a CGSize into a Size

# lots of other conversions are possible.
# a UIView or CALayer => view.frame
f = Rect(view)
# 4 numbers
f = Rect(x, y, w, h)
# or two arrays
p = Point(x, y)  # or just [x, y] works, too
s = Size(w, h)  # again, [w, h] is fine
f = Rect(p, s)
# like I said, a straight-up array of nested arrays is fine, too.
f = Rect([[x, y], [w, h]])
CG{Rect,Point,Size} is a real boy!

These methods get defined in a module (SugarCube::CG{Rect,Size,Point}Extensions), and included in CGRect and Rect. The idea is that you do not have to distinguish between the two objects.

These methods all use the methods as described in CGGeometry Reference, e.g. CGRectContainsPoint, CGRectIntersectsRect, etc.

# intersection / contains
Point(0, 0).intersects?(Rect(-1, -1, 2, 2))  # => true
# if a Point intersects a Rect, the Rect intersects the Point, right?
Rect(-1, -1, 2, 2).intersects? Point(0, 0)  # => true

# CGRect and the gang are real Ruby objects.  Let's treat 'em that way!
view.frame.contains? Point(10, 10)  # in this case, contains? and intersects? are synonyms
view.frame.intersects? Rect(0, 0, 10, 10)  #  <= but this one
view.frame.contains? Rect(0, 0, 10, 10)    #  <= and this one are different.

# CGRect has factory methods for CGRectEmpty, CGRectNull, and - KINDA - CGRectInfinite
# BUT, there is a bug (?) right now where CGRectIsInfinite(CGRectInfinite) returns false.
# so instead, I've built my own infinite? method that checks for the special "Infinite" value
> CGRect.infinite
=> [[0, 0], [Infinity, Infinity]]
> CGRect.infinite.infinite?
=> true
> CGRect.null
=> [[Infinity, Infinity], [0.0, 0.0]]
> CGRect.null.null?
=> true
> CGRect.empty
=> [[0.0, 0.0], [0.0, 0.0]]
> CGRect.empty.empty?
=> true

A lot of the methods in CGGeometry Reference are available as instance methods

view.frame.left    # => CGRectGetMinX(view.frame)
view.frame.right   # => CGRectGetMaxX(view.frame)
view.frame.top     # => CGRectGetMinY(view.frame)
view.frame.bottom  # => CGRectGetMaxY(view.frame)
view.frame.width   # => CGRectGetWidth(view.frame)
view.frame.height  # => CGRectGetHeight(view.frame)
view.frame.center  # => Point(CGRectGetMidX(view.frame), CGRectGetMidY(view.frame))

view.frame.intersection(another_rect)  # => CGRectIntersection(view.frame, another_rect)
view.frame + another_rect  # => CGRectUnion(view.frame, another_rect)
view.frame + a_point  # => CGRectOffset(view.frame, a_point.x, a_point.y)
view.frame + a_offset  # => CGRectOffset(view.frame, a_offset.horizontal, a_offset.vertical)
view.frame + edgeinsets  # => UIEdgeInsetsInsetRect(view.frame, edgeinsets)
view.frame + a_size  # => CGRectInset(view.frame, -a_size.width, -a_size.height)
# Adding a size to a view keeps the view's CENTER in the same place, but
# increases its size by `size.width,size.height`. it's the same as using
# UIEdgeInsets with top == bottom, and left == right
> Rect(0, 0, 10, 10).center
=> Point(5.0, 5.0)  # note the center
> Rect(0, 0, 10, 10) + Size(10, 10)
=> Rect([-10.0, -10.0],{30.0 × 30.0})  # origin and size changed, but...
> (Rect(0, 0, 10, 10) + Size(10, 10)).center
=> Point(5.0, 5.0)
# See?  It's bigger, but the center hasn't moved.

to_hash/from_hash, and notice here that I used inspect, to show that it is a little more readable.

NOTE As of today, Aug. 25, 2012, rubymotion v1.22, the inspect method in SugarCube is not being called. I think this is a bug... this worked before!

> Rect(0, 0, 10, 10).to_hash
=> {"Width"=>10.0, "Height"=>10.0, "Y"=>0.0, "X"=>0.0}
> puts CGRect.from_hash(Rect(0, 0, 1, 1).to_hash).inspect
CGRect([0.0, 0.0],{1.0 × 1.0})

to_s/from_s, which rely on NSStringFromCGRect/CGRectFromString (et. al.)

> Rect(0, 0, 10, 10).to_s
=> "{{0, 0}, {10, 10}}"
> puts CGRect.from_s Rect(0, 0, 10, 10).to_s
{{0, 0}, {10, 10}}

REPL View adjustments

Pixel pushing is an unfortunate but necessary evil. Well, at least we can make it a little less painful.

These methods help you adjust the frame of a view. They are in the SugarCube::Adjust module so as not to conflict. If you don't want the prefix, include SugarCube::Adjust in app_delegate.rb

Assume I ran include SugarCube::Adjust in these examples.

# if you are in the REPL, you might not be able to click on the view you want...
> adjust superview.subviews[4].subviews[1]
> up 1
> down 1  # same as up -1, obviously
> left 1
> right 1  # same as up -1, obviously
> origin 10, 12  # move to x:10, y:12
> wider 1
> thinner 1
> taller 1
> shorter 1
> size 100, 10  # set size to width:100, height: 10
> shadow(opacity: 0.5, offset: [0, 0], color: :black, radius: 1) # and path, which is a CGPath object.
> restore  # original frame and shadow is saved when you call `adjust`
> # short versions!
> a superview.subviews[4].subviews[1]  # this is not uncommon in the REPL
> u          # up, default value=1
> d          # down
> l          # left
> r          # right
> o 10, 12   # origin, also accepts an array (or Point() object)
> w          # wider
> n          # thinner
> t          # taller
> s          # shorter
> z 100, 10  # size, also accepts an array (or Size() object)
> # you can also query your view.  You will get nice-looking
> # SugarCube::CoreGraphics objects
> f   # frame
[[0, 0], [320, 480]]
> o   # origin
[0, 0]
> z   # size
[320, 480]
> h   # shadow - this returns an array identical to what you can pass to `shadow`

# if you forget what view you are adjusting, run `adjust` again
> a
=> {UITextField @ x: 46.0 y:214.0, 280.0×33.0} child of UIView

Pointers

These are not UIKit-related, so I reverted to Ruby's preferred to_foo convention.

[0.0, 1.1, 2.2].to_pointer(:float)

floats = Pointer.new(:float, 3)
floats[0] = 0.0
floats[1] = 1.1
floats[2] = 2.2

UUID

Quick wrapper for CFUUIDCreate() and CFUUIDCreateString(). Identical to the BubbleWrap::create_uuid method.

> SugarCube::UUID::uuid
"0A3A76C6-9738-4458-969E-3B9DF174A3D9"

# or
> include SugarCube::UUID
> uuid
# => "0A3A76C6-9738-4458-969E-3B9DF174A3D9"
Something went wrong with that request. Please try again.