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.

Installation

gem install sugarcube

# or in Gemfile
gem 'sugarcube'

# in Rakefile
require 'sugarcube'

Examples

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
: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

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.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.alloc.initWithFrame([[0, 0], [320, 480]], style: :plain.uitableviewstyle)
UITableView.alloc.initWithFrame([[0, 0], [320, 480]], style: :grouped.uitableviewstyle)
# =>
UITableView.plain
UITableView.grouped

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)

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

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]])
CGRect,CGPoint,CGSize 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 puts, to show that the to_s method is a little more readable.

> 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
CGRect([0.0, 0.0],{1.0 × 1.0})

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"