SugarCube

Join the chat at https://gitter.im/rubymotion/sugarcube

Build Status Version

About

Cocoa and CocoaTouch are verbose frameworks. These extensions hope to make development in rubymotion more enjoyable. With SugarCube, you can create a color from an integer or symbol, or create a UIFont or UIImage from a string.

Many core 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 operations on their head. So instead 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 is the result of the hard work of many developers, but it’s mostly maintained by Colin Gray, with help from members of the RubyMotion community. If you have an idea, please open a pull request so we can discuss it! All PRs must be submitted with specs.

If you use SugarCube on your projects, please consider donating a small amount via gratipay.com, or better yet: fork the project, add some specs, and improve the quality of this super-handy gem!

Documentation

A work in progress. This README is the best source, but I am trying to be more diligent about adding Yard documentation, which is available here:

http://rubydoc.info/gems/sugarcube/latest

Versioning

SugarCube uses FerVer: https://github.com/jonathanong/ferver. This means that minor breaking changes occur in minor version bumps, and sometimes a non-breaking change occures in the major version (like when we added OS X support).

Installation

gem install sugarcube

# in Rakefile
require 'sugarcube'

# or in Gemfile
gem 'sugarcube', :require => 'sugarcube-classic'
# or for the bold:
# gem 'sugarcube', :require => 'sugarcube-all'
# or for the picky:
# gem 'sugarcube', :require => [
#   'sugarcube',
#   'sugarcube-repl',
# ]

# in terminal
$ bundle install

Packages

SugarCube has grown over time to be a pretty massive collection of helpers. While some people choose to use the entire library, other people like to pick and choose the extensions they want to use. With that in mind, SugarCube is written so that it does not pollute any classes by default. So if all you do is require "sugarcube", you are NOT going to get much mileage!

In the installation code above, I show the example of using :require => 'sugarcube-all' to include all of SugarCube's extensions. You can, alternatively require just the packages you need:

Gemfile
gem 'sugarcube', :require => [
  'sugarcube-ui',
  'sugarcube-events',
  'sugarcube-gestures',
  'sugarcube-568',
  'sugarcube-attributedstring',
]

Or, from your Rakefile:

$:.unshift('/Library/RubyMotion/lib')
require 'motion/project/template/ios'

require 'bundler'
Bundler.require

require 'sugarcube-ui'
require 'sugarcube-events'
require 'sugarcube-gestures'
require 'sugarcube-568'
require 'sugarcube-attributedstring'

You can require the packages in piecemeal like this, or you can require a group of packages: classic, common, or all.

  • sugarcube-classic: Excludes 568, attributedstring, gestures, repl, awesome, anonymous, unholy, and legacy
  • sugarcube-common: Excludes awesome, anonymous, unholy, and legacy
  • sugarcube-all: Excludes legacy

So without further ado,

SugarCube

Packages are sorted more-or-less by their usefulness. The more esoteric ones are at the end.

REPL (wiki)

If you install SugarCube and only use the REPL package, you will benefit from some of SugarCube's greatest tricks!

require 'sugarcube-repl'

This package is useful during development because it adds methods to the REPL that make adjusting and introspecting views much easier. You'll get a lot more done in the REPL with these additions.

You should NEVER use these methods in your application, because this package is only included in 'development' mode. That means if you hard-code a call to 'tree' in your code, that will crash when you go to release your app. YIKES.

To keep this document lean-and-mean, I've put most of the REPL documentation in the wiki, but here's a quick overview:

  • Use the tree commands to output your view hierarchy. It can accept a UIView, UIViewController, or CALayer object as the root object, or it defaults to your application's UIWindow object.
  (main)> tree
    0: . UIWindow(#6e1f950: [[0.0, 0.0], [320.0, 480.0]])
    1: `-- UIView(#8b203b0: [[0.0, 20.0], [320.0, 460.0]])
    2:     +-- UIButton(#d028de0: [[10.0, 10.0], [320.0, 463.400512695312]])
  • The number can be passed to the adjust method, aliased to a, and that will become the view or object you are adjusting.
  (main)> a 2
  => UIButton(#d028de0: [[10.0, 10.0], [320.0, 463.400512695312]])
  • Now you can modify that view, either by accessing it via a (with no arguments it returns the object being adjusted) or by using an adjust method:
  > up 1
  > wider 15
  # these have shorthands, too
  > u 1
  > w 15
  • Changed your mind? undo all adjustments to the object currently selected:
  > restore
  • Which element in the tree did I select with adjust? You can make the object selected flash in the simulator:
  > blink

Be sure to read more in the REPL Additions Wiki page.

UI on iOS: UIKit extensions (wiki)

A big package chock full of methods to make working in UIKit a joy.

require 'sugarcube-ui'

A few varieties of methods are in this package:

  • Conversions: 'string-to'.uiimage, image.uiimageview, 'string-to'.uilabel(font)
  • Helpers: shorthands for common operations, like a_view << a_subview, a_subview.convert_frame_to(a_view)
  • Symbols: :system.uifont(20), :label.uifontsize
  • Frame accessors: a_view.x, a_view.x = 100 (on UIView and CALayer)

There are too many methods to define here. Instead: a complete list of methods is available in the documentation, and the wiki page is a great source as well.

UI on OS X: AppKit extensions

Similar extensions as the iOS version, but using the ns prefix on method names:

  • Conversions: 'string-to'.nsimage, image.nsimageview, 'string-to'.nslabel(font)
  • Helpers: view << subview
  • Symbols: :white.nscolor, :system.nsfont
  • Frame accessors: a_view.x, a_view.x = 100 (on UIView, CALayer, NSWindow, and NSScreen)

UI on Android

(warning: extending built-in classes is not reliable in RubyMotion on Android)

ViewGroup gets the same << method that you see in UIView and NSView.

Constants

require 'sugarcube-constants'

There are lots and lots of constants in UIKit, so many that I wanted a way to write these as symbols instead of UILongConstantNames. This package adds methods to Symbols to convert them into a UIKit or Foundation constant.

:center.nsalignment  # => NSTextAlignmentCenter (formerly UITextAlignmentCenter)
:upside_down.uiorientation  # => UIDeviceOrientationPortraitUpsideDown
:rounded.uibuttontype  # => UIButtonTypeRoundedRect
:highlighted.uicontrolstate  # => UIControlStateHighlighted
:touch.uicontrolevent  # => UIControlEventTouchUpInside
:change.uicontrolevent  # => UIControlEventValueChanged
:all.uicontrolevent  # => UIControlEventAllEvents

# these are really handy for custom buttons - touch_start means the finger is
# inside the button, touch_stop is outside the button or canceled
:touch_start  # => UIControlEventTouchDown | UIControlEventTouchDragEnter
:touch_stop   # => UIControlEventTouchUpInside | UIControlEventTouchCancel | UIControlEventTouchDragExit

: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
:disclosure.uitablecellaccessory  # or .uitableviewcellaccessorytype
:blue.uitablecellselectionstyle  # or .uitableviewcellselectionstyle

See the complete list by browsing the documentation, or open up symbol.rb.

Timer

require 'sugarcube-timer'

Methods get added to the Fixnum class, and are available as methods on NSTimer, and can be called via the SugarCube::Timer module.

# once
1.second.later do
  @view.shake
end
# repeating
1.second.every do
  @view.shake
end

# you can assign the return value (an NSTimer)
timer = 1.second.every do
  @view.shake
end
# and invalidate it
timer.invalidate

# the `every` method is available in the SugarCube::Timer module,
# which you might find more readable
include SugarCube::Timer

every 1.minute do
  puts "tick"
end

# might as well make an alias for 'later', too
after 1.minute do
  puts "ding!"
end

# other time-related methods
# for compatibility with Time methods, the mins/secs (and min/sec) aliases are provided.  Personally,
# I like the more verbose minutes/seconds.
1.millisecond  || 2.milliseconds
1.millisec     || 2.millisecs
1.second  || 2.seconds
1.sec     || 2.secs     # aliases
1.minute  || 2.minutes  # 1.minute = 60 seconds
1.min     || 2.mins     # aliases
1.hour    || 2.hours    # 1.hour = 60 minutes
1.day     || 2.days     # 1.day = 24 hours
1.week    || 2.weeks    # 1.week = 7 days
# 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   # 1.month = 30 days
1.year    || 2.years    # 1.year = 365 days

Events

require 'sugarcube-events'

Inspired by BubbleWrap's when method, but I prefer jQuery-style verbs and SugarCube symbols. Adds methods to UIControl and UITextView.

UIControl

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

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

# remove handlers
button.off(:touch, :touch_up_outside, :touch_cancel)
button.off # all events

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.

UITextView

These handlers are functionally identical in usage to the same methods in UIControl. They use the NSNotificationCenter#addObserverForName:object:queue:usingBlock: method, but you do not have to worry about un-observing. When the text view is released, these observers will be removed.

There are two aliases for each event, or you can use the event notification. I prefer the present tense (jQuery-style on :change), but UIKit prefers past simple (:editing_did_begin). The notifications, on the other hand, are in present simple (UITextViewTextDidBeginEditingNotification). Whatever floats your boat.

Anyway, these are all the same:

:editing_did_begin   :begin   UITextViewTextDidBeginEditingNotification
:editing_did_change  :change  UITextViewTextDidChangeNotification
:editing_did_end     :end     UITextViewTextDidEndEditingNotification
text_view = UITextView.new
text_view.on :begin do |notification|  # <= you have to accept the notification in your block
  p 'wait for it...'
end
text_view.on :change do |notification|
  p text_view.text
end
text_view.on :end do |notification|
  p 'done!'
end

# if you want to remove the block, use the off method
text_view.off :editing_did_change
# or
text_view.off :change
# or
text_view.off UITextViewTextDidChangeNotification

Gestures

require 'sugarcube-gestures'

SugarCube's gesture support is very similar to BubbleWrap's, and it's entirely possible that the two will be merged into one thing. But SugarCube is all about extending base classes, whereas BubbleWrap tends to add new classes to do the heavy lifting. Plus the options you pass to SugarCube are very different, and the prefix is "on" instead of "when" (e.g. "on_pan" instead of "when_panned")

view.on_pan do |gesture|
  location = gesture.locationInView(view)
end

# other gesture methods, with common options:
view.on_tap   # use system defaults
view.on_tap(1)  # number of taps
view.on_tap(taps: 1, fingers: 1)  # number of taps and number of fingers

view.on_pinch   # no options
view.on_rotate  # no options

view.on_swipe   # use system defaults
view.on_swipe :left
view.on_swipe(direction: :left, fingers: 1)
view.on_swipe(direction: UISwipeGestureRecognizerDirectionLeft, fingers: 1)

view.on_pan   # use system defaults
view.on_pan(2)  # minimum and maximum fingers required
view.on_pan(fingers: 2)
view.on_pan(min_fingers: 2, max_fingers: 3)

# If present, overrides fingers options and instead handles gestures originating at specified screen edges (UIScreenEdgePanGestureRecognizer)
view.on_pan(edges: [<list>]) #  Some combination of [:left, :right, :top, :bottom, :all]. 

# `on_press` is a continuous event (it uses UILongPressGestureRecognizer), so
# you need to check the `gesture`:
view.on_press do |gesture|
  if gesture.state == UIGestureRecognizerStateBegan
    # handle press
  end
end
view.on_press(1.5)  # duration
view.on_press(duration: 1.5, taps: 1, fingers: 1)

# this version is only fired when the long-press begins; this is *probably* more
# useful to you:
view.on_press_begin do ... end

# or, when the gesture ends:
view.on_press_ended do ... end

Notifications

require 'sugarcube-notifications'

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

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

# you can access the userInfo dictionary directly from the notification
def notified(notification)
  notification[:user]  # => 'dict'
end

# very similar to add or remove an observer
MyNotification.add_observer(observer, :method_name)
MyNotification.add_observer(observer, :method_name, object)

# remove the observer
MyNotification.remove_observer(observer)
MyNotification.remove_observer(observer, object)

UIImage

Image Manipulation - VERY handy! Includes some quick maniputions on UIImage, and adds an interface to chain together CIFilters. Plus, you can refer to cifilter.rb to find out what filters are supported in iOS (all supported filters get a class method in this file).

require 'sugarcube-image'

UIImage additions
image.scale_to [37, 37]
image.rounded  # default: 5 pt radius
image.rounded(10)

image.in_rect([[10, 10], [100, 100]])  # get part of an image

image.darken  # => good for "pressed" buttons
image.darken(brightness: -0.5, saturation: -0.2)  # these are the defaults
image.gaussian_blur(radius: 5)
image.inverted

image.rotate(:left)
image.rotate(:right)
image.rotate(:flip)  # 180° - if you have a better name, let me know!
image.rotate(45.degrees)

image.in_rect(frame)  # returns the part of the image contained in frame
image.scale_to(new_size)  # won't stretch, but the image might have transparent padding
image.scale_to(new_size, background: :white)  # adds a white background before padding
image.scale_within(new_size)  # same as scale_to in that it doesn't stretch the
# image, but the size is not guaranteed to be new_size.  It is guaranteed not to
# be *bigger* than new_size
image.scale_to_fill(new_size)  # again, like scale_to, but the image is guaranteed
# to completely fill new_size, even if some of the image has to be cropped to fit.
# You can control which side or corner you prefer to remain visible.  because the
# aspect ratio is maintained, only ONE dimension will need to be cropped.
image.scale_to_fill(new_size, position: :top_left)

# returns a UIColor (and supports retina images)
image.color_at([5, 5])

# default insets are UIEdgeInsetsZero
image.tileable
image.tileable(insets)
image.stretchable
image.stretchable(insets)

# Apply a mask to an image.  The mask should be a grayscale image.  White areas
# will be made transparent, and black opaque.
image.masked(mask_image)

# Apply a color overlay to an image. This is used used on icons (PNG's), on which
# you want to change the color.
image.overlay(UIColor.redColor)
image.overlay(:red)

# Combine two images
image_ab = image_a << image_b

# Create an image from scratch!
UIImage.canvas([100, 100]) do |context|  # opaque: false, scale: UIScreen.mainScreen.scale
  # draw here!
end

# use an image as a background to draw on
image.draw do |context|
  # draw here!
end

# size
image = UIImage.canvas(size: [10, 20])
image.width  # => 10
image.height  # => 20
CIFilter additions
# create a filter
gaussy = CIFilter.gaussian_blur(radius: 5)
gaussy = CIFilter.gaussian_blur(5)  # this also works - you can find the arg order by looking in cifilter.rb

# apply a filter to a UIImage
new_image = image.apply_filter(gaussy).uiimage  # apply_filter returns a CIImage, which is converted to UIImage

# apply a chain of filters using the `|` operator or `apply_filter`
darken = CIFilter.color_controls(saturation: 0, brightness: 0)
new_image = image.apply_filter(gaussy).apply_filter(darken).uiimage

If you include sugarcube-pipes you can use the | operator to chain filters:

# using the filters from above
new_image = image | gaussy | darken | UIImage
new_view = view | gaussy | darken | UIView

There are 91 filters available in iOS 6, I won't list them here, but check out the Apple documentation to read about them, and study cifilter.rb.

UIColor

require 'sugarcube-color'

Methods to merge or manipulate a color, or to get information about a color. Works best on RGB colors, but HSB will work well, too. UIColors based on image patterns can't easily be inverted or mixed.

Any classes that have a well-defined "color" representation are given a uicolor method, so it's easy to create a color from hex codes, css names, or images (as patterns).

:blue.uicolor  # UIColor.blueColor
# uicolor() accepts an alpha value, too
:blue.uicolor(0.5)

# all CSS colors are supported (but no "grey" aliases, consistent with UIKit,
# which only provides "grayColor")
:firebrick.uicolor  # => 0xb22222.uicolor

# RGB values, in the range 0..255.
[160, 210, 242].uicolor  # => UIColor.colorWithRed(0.6274, green:0.8235, blue:0.9490, alpha:1.0)
[160, 210, 242].uicolor(0.5)  # => UIColor.colorWithRed(0.6274, green:0.8235, blue:0.9490, alpha:0.5)

# 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)

# works when using strings, too
"#fff".uicolor # => UIColor.whiteColor
"#ffffff".uicolor # => UIColor.whiteColor
"#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 == 0x000f0f.uicolor.  There's no way to tell the difference
# at run time between those two Fixnum literals.

# UIColor from image name, if the first character is not "#"
"pattern".uicolor == "pattern".uiimage.uicolor  # => UIColor.colorWithPatternImage(UIImage.imageNamed("pattern"))

These methods are added onto the UIColor class:

:red.uicolor.invert # => UIColor.cyanColor
:blue.uicolor.invert # => UIColor.yellowColor
:green.uicolor.invert # => UIColor.magentaColor
:red.uicolor + :blue.uicolor # => UIColor.purpleColor
:red.uicolor + :green.uicolor # => :olive.uicolor
# (I didn't know that until I tried it in the REPL, but it was pretty cool to
# see the UIColor#to_s method match that mixture to olive!)

# a more generic color mixing method (`+` delegates to this method):
:white.uicolor.mix_with(:black.uicolor, 0)  # => :white
:white.uicolor.mix_with(:black.uicolor, 0.25)  # => 0x404040.uicolor
:white.uicolor.mix_with(:black.uicolor, 0.5)  # => :gray, same as :white.uicolor + :black.uicolor
:white.uicolor.mix_with(:black.uicolor, 0.75)  # => 0xbfbfbf.uicolor
:white.uicolor.mix_with(:black.uicolor, 1)  # => :black

# you can get information about a color:
:cyan.uicolor.red         # => 0
:cyan.uicolor.green       # => 1
:cyan.uicolor.blue        # => 1
:cyan.uicolor.hue         # => 0.5
:cyan.uicolor.saturation  # => 1.0
:cyan.uicolor.brightness  # => 1.0
:cyan.uicolor(0.9).alpha  # => 0.9

# convert to CGColor
color.cgcolor

Factories

require 'sugarcube-factories'

UIAlertView

Accepts multiple buttons and handlers. In its simplest form, you can pass just a title and block. An optional :message can either be passed in as an option, or as the 2nd positional arg.

Options:

UIAlertView.alert(options)
UIAlertView.alert(title, options)
UIAlertView.alert(title, message, options)
0 => title => String - title of the alert.  optional positional arg.
1 => message => String - message of the alert.  optional positional arg.
:title => String - title of the alert.
:message => String - message of the alert.
:success => Proc - the success handler
:cancel => Proc - the cancel handler
:buttons => [] - List of buttons ([cancel, others...])
:buttons => {} - Hash of buttons ({cancel:, others: ...}) in any order of course
:style => Symbol | Fixnum - A symbol (uialertstyle) or constant (UIAlertViewStyle*)
:show => Boolean - Whether to show the action sheet (default: true)
# simple title/message alert
UIAlertView.alert('This is happening, OK?', 'An optional message') do
  self.it_happened!
end

# a little more complex - the cancel button should be first, and the block will
# receive a string and an index
UIAlertView.alert('This is happening, OK?',
  message: 'Don\'t worry, it\'ll be fine.',
  buttons: ['Nevermind', 'OK'],
  ) do |button, button_index|
  if button == 'OK'  # or: button_index == 1
    self.happened!
  end
end

# Full on whiz-bangery.  The cancel button should be the first entry in
# `buttons:`.  When you specify the success and cancel button handlers this way,
# you need not assign both.
UIAlertView.alert('I mean, is this cool?',
  buttons: ['No!', 'Sure!', 'Hmmmm'],
  message: 'No going back now',
  cancel: proc { self.cancel },
  success: proc { |pressed| self.proceed if pressed == 'Sure!' }
  )

# To keep up with BubbleWrap's awesome BW::ActionSheet and BW::AlertView
# helpers, SugarCube provides a similar interface.
UIAlertView.alert('Confirm action!', 'Are you sure you want to do this?',
  buttons: {
    cancel: 'No!',
    success: 'Sure!',
    unsure: 'Hmmm',
  }) do |button|
  # button will be :cancel, :success or :unsure
end
UIActionSheet

This is very similar to UIAlertView.alert, but instead of cancel and success handlers, you can have cancel, success, and destructive handlers, and there is no message argument.

If you use an array of buttons (which you probably should), the order of arguments is [:cancel, :destructive, :others, ...]. If you dont want a cancel or destructive button, pass nil in place.

Options:

UIActionSheet.alert(options)
UIActionSheet.alert(title, options)
0 => title => String - title of the action sheet
:title => Proc - title of the action sheet
:success => Proc - the success handler
:cancel => Proc - the cancel handler
:destructive => Proc - the destructive handler
:buttons => [] - List of buttons ([cancel, destructive, others...])
:buttons => {} - Hash of buttons ({cancel:, destructive:, others: ...}) in any order of course
:style => Symbol | Fixnum - A symbol (uiactionstyle) or constant (UIActionSheetStyle*)
:show => Boolean - Whether to show the action sheet (default: true)
:from => CGRect | UIBarButtonItem | UIToolbar | UITabBar | UIView (default: first window)
         Where to display the alert.  Mostly relevant on iPad.
:view => UIView (default: first window)
         The view to display the alert when used with :from => CGRect
# simple
UIActionSheet.alert 'This is happening, OK?' do
  self.happened!
end

# a little more complex, with cancel and destructive buttons
UIActionSheet.alert('This is happening, OK?', buttons: ['Cancel', 'Kill it!', 'Uh, what?']
  ) do |button|
  # button is 'Cancel', 'Kill it!' or 'Uh, what?'
end

# skip cancel and destructive buttons:
UIActionSheet.alert('Should I?', buttons: [nil, nil, 'OK', 'Nevermind']) { |pressed|
  self.do_it if pressed == 'OK'
}

UIActionSheet.alert 'I mean, is this cool?', buttons: ['Nah', 'With fire!', 'Sure', 'whatever'],
  cancel: proc { self.cancel },
  destructive: proc { self.kill_it_with_fire }
  success: proc { |pressed| self.proceed if pressed == 'Sure' }

# By passing a Hash to buttons you can get this improved interface, similar to
# BubbleWrap's awesome interface.
UIActionSheet.alert('Well, how bout it?',
  buttons: {
    cancel: 'Cancel',
    destructive: 'Kill it with fire!',
    help: 'Tell me more'
  }) do |button|
  # button is :cancel, :destructive or :help
end
UIAlertController

Starting with iOS 8.0, UIActionSheet and UIAlertView are deprecated and replaced by UIAlertController.

This is very similar to UIAlertView.alert and UIActionSheet.alert but you have to pass a UIViewController as first argument.

Options:

UIAlertController.alert(controller, options)
UIAlertController.alert(controller, title, options)
0 => title => String - title of the action sheet
:title => Title of the alert sheet
:buttons => [] - List of buttons ([cancel, destructive, others...])
:style => Symbol | Fixnum - A symbol (uialertcontrollerstyle) or constant (UIAlertControllerStyle*)
:show => Boolean - Whether to show the alert controller (default: true)
:from => CGRect | UIBarButtonItem | UIView (default: first window)
         Where to display the alert.  Mostly relevant on iPad.
:view => UIView (default: first window)
         The view to display the alert when used with :from => CGRect
# simple
UIAlertController.alert(self, 'This is happening, OK?', buttons: ['Cancel', 'Kill it!', 'Uh, what?']
  ) do |button|
  # button is 'Cancel', 'Kill it!' or 'Uh, what?'
end
UIButton
UIButton.buttonWithType(:custom.uibuttontype)
# =>
UIButton.custom

# many of these are obsolete since iOS 7
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)
UIButton.system            => UIButton.buttonWithType(:system.uibuttontype)
UITableView

Default frame is [[0, 0], [0, 0]], but most containers will resize it to be the correct size. But heads up, it was [[0, 0], [320, 480]] (until the iphone 5 / 4-inch retina came out).

UITableView.alloc.initWithFrame([[0, 0], [0, 0]], style: :plain.uitableviewstyle)
UITableView.alloc.initWithFrame([[0, 0], [320, 480]], style: :plain.uitableviewstyle)
UITableView.alloc.initWithFrame([[0, 0], [320, 568]], style: :plain.uitableviewstyle)
# custom frame:
UITableView.alloc.initWithFrame([[0, 0], [320, 400]], style: :grouped.uitableviewstyle)

# =>
UITableView.plain
UITableView.plain([[0, 0], [320, 480]])
UITableView.plain([[0, 0], [320, 568]])
# custom frame:
UITableView.grouped([[0, 0], [320, 400]])
UITableViewCell
# factory methods, named for the cell style. cell identifier is required.
UITableViewCell.default('cell_identifier')
UITableViewCell.value1('cell_identifier')
UITableViewCell.value2('cell_identifier')
UITableViewCell.subtitle('cell_identifier')

# you can options for the common settings
cell = UITableViewCell.default('cell_identifier',
  accessory: :disclosure,
  selection: :blue,
  text: 'text',
  image: 'icon', # coerced into a UIImage
  )
UISegmentedControl
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"])
# plain, bordered, and bezeled are the other types
UIActivityViewIndicator
UIActivityIndicatorView.alloc.initWithActivityIndicatorStyle(UIActivityIndicatorViewStyleWhite)
UIActivityIndicatorView.alloc.initWithActivityIndicatorStyle(UIActivityIndicatorViewStyleWhiteLarge)
UIActivityIndicatorView.alloc.initWithActivityIndicatorStyle(UIActivityIndicatorViewStyleGray)

# =>

UIActivityIndicatorView.white
UIActivityIndicatorView.large
UIActivityIndicatorView.gray
UIBarButtonItem

These factory methods accept a block, which will get wired up as a target/action.

# Get an instance containing the specified system item.
UIBarButtonItem.done do
  self.dismissViewControllerAnimated true, completion:nil
end
# =>
UIBarButtonItem.alloc.initWithBarButtonSystemItem(:done.uibarbuttonitem, target:self, action:"action:")
# with 'action' defined as:
def action(sender)
  self.dismissViewControllerAnimated true, completion:nil
end

# the method names are 1::1 with the uibarbuttonitem constants in symbol.rb
UIBarButtonItem.cancel { ... }           => UIBarButtonItem.alloc.initWithBarButtonSystemItem(:cancel.uibarbuttonitem, target:self, action:"action:")
UIBarButtonItem.edit { ... }             => UIBarButtonItem.alloc.initWithBarButtonSystemItem(:edit.uibarbuttonitem, target:self, action:"action:")
UIBarButtonItem.save { ... }             => UIBarButtonItem.alloc.initWithBarButtonSystemItem(:save.uibarbuttonitem, target:self, action:"action:")
  .
  .
  .
UIBarButtonItem.page_curl { ... }         => UIBarButtonItem.alloc.initWithBarButtonSystemItem(:page_curl.uibarbuttonitem, target:self, action:"action:")

For custom UIBarButtonItems, you can use the titled and imaged methods:

# Create a UIBarButtonItem, specifying the title
UIBarButtonItem.titled('Close') do
  self.dismissViewControllerAnimated(true, completion:nil)
end
# =>
UIBarButtonItem.alloc.initWithTitle('Close', style: :bordered.uibarbuttonstyle, target:self, action:"action:")
def action(sender)
  self.dismissViewControllerAnimated(true, completion:nil)
end


# You can also specify the style.
UIBarButtonItem.titled('Close', :plain) do  # :plain, :bordered, :done
  # ...
end

# Or specify the image instead
UIBarButtonItem.imaged('close_icon') do  # 'close_icon' will be coerced into a UIImage
  # ...
end
# =>
UIBarButtonItem.alloc.initWithImage('Close'.uiimage, style: :bordered.uibarbuttonstyle, ...)

# And, like `titled`, specify the style
UIBarButtonItem.imaged('close'.uiimage, :done) do
  # ...
end

# If you provide two images, they will be used as the portrait and landscape images
UIBarButtonItem.imaged(['portrait'.uiimage, 'landscape'.uiimage) do
  # ...
end
# =>
UIBarButtonItem.alloc.initWithImage('portrait'.uiimage, landscapeImagePhone:'landscape'.uiimage, style: :bordered.uibarbuttonstyle, target:self, action:"action:")

Example Usage:

toolbar = UIToolbar.new
toolbar.items = [
  @image_picker_button = UIBarButtonItem.camera { presentImagePickerController(self) },
  UIBarButtonItem.flexiblespace,
  @saveButton = UIBarButtonItem.save { save_photo(self) }
]
UITabBarItem

Easy to create system or custom UITabBarItems.

UITabBarItem.titled('My Title')
# with optional :image and :selected_image options:
UITabBarItem.titled('My Title', image: 'my_icon', selected_image: 'my_icon_selected')
# also supports :tag and :badge
UITabBarItem.titled('My Title', tag: MY_TABBAR_ITEM, badge: '+1')

# system items:
UITabBarItem.more
UITabBarItem.favorites

# Most of the UITabBarItem init methods accept the UIView#tag, and so there is
# support for that in the UITabBarItem factory methods.  Defaults to 0.  The
# :badge option is supported here as well
UITabBarItem.featured(tag: MY_TABBAR_ITEM, badge: 10)

# All of the UITabBarSystemItem helpers delegate to the 'UITabBarItem.system'
# method, which you can call direcly as well.  It accepts :tag and :badge
# options.
UITabBarItem.system(:top_rated, tag: MY_ITEM_TAG, badge: 'hi')
NSError

WARNING: Breaking change in 3.3.0, this method name was new, but that caused conflicts with a bunch of 3rd party CocoaPods.

# usually, NSError.error doesn't work, because the only initializer for NSError
# needs more arguments.  This method passes some defaults in.
NSError.error('message')
# same as =>
NSError.error('message', domain: 'Error', code: 0, userInfo: {})
UILabel

WARNING: Breaking change in 3.3.0, this method name was new, but that caused conflicts with a bunch of 3rd party CocoaPods.

# supports text, font, and font size
UILabel.label('label text')
UILabel.label('label text'.attrd)  # detects attributed strings, too
UILabel.label('label text', 'Font name')  # You can pass just a font name
UILabel.label('label text', UIFont.fontWithName('Font name', size: 20))  # Or a UIFont object
UILabel.label('label text', 'Font name', 20)  # Or the name *and* the size

Animations (wiki)

require 'sugarcube-animations'

Careful, once you start using these helpers, you'll never go back.

view.move_to [100.0, 100.0] # origin
view.center_to [100.0, 100.0] # center
view.scale_to 2  # double the size, and preserves existing rotation transform
# view.scale_to 4 -> CGAffineTransformMakeScale(4, 4)
# view.scale_to [4, 3] -> CGAffineTransformMakeScale(4, 3)
view.fade_out
view.slide :left, 100
view.rotate_to 180.degrees
view.shake  # great for showing invalid form elements
view.tumble  # great way to dismiss an alert-like-view
# tumbles in the other direction (towards the right side instead of left)
view.tumble(side: :right)
# wow, this is a SuperGoodDeleteWiggle! https://github.com/mxcl/SuperGoodDeleteWiggle
view.wiggle
view.dont_wiggle
# if you modify the AppDelegate to load the WiggleAnimationController you can
# see an example of this animation in action.

# the complement to 'tumble' is 'tumble_in' - the view starts above the window
# and drops in with the same kind of animation as 'tumble'.  Before you call
# this method, set the view.frame to the *destination* location.
view.tumble_in(side: :right)

These helpers all delegate to the UIView.animate method, which accepts all the options that UIView.animateWithDuration(delay:options:animations:completion:) or UIView.animateWithDuration(delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:) accept. All options are optional, and they will play nicely inside an animation chain (see below, and the wiki page).

UIView.animate do
  view.alpha = 0
end

The "spring" animations use the method UIView.animateWithDuration(delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:), available in iOS 7. In testing, I've found this method to be slightly unreliable, so use with caution. To "enable" it, pass in the :damping option. You can also use :velocity to set an initial velocity, if there is an animation in progress.

new_frame = [[110, 200], [100, 20]]

UIView.animate(damping: 0.1) do  # default velocity is 0
  view.frame = new_frame
end

# equivalent:
view.reframe_to(new_frame, damping: 0.1, velocity: 1)

The [wiki] page documents all the different animation methods, and documents animation chaining, which looks like this:

# fade out and slide left, then fade back in while returning to original position
UIView.animation_chain do
  view.fade_out
  view.slide :left
end.and_then do
  view.fade_in
  view.slide :right
end.start

Core Animation classes get some love, too!

view.layer.basic_animation('opacity', from: 0, to: 1, duration: 0.1)

Lots more information in the Wiki!

require 'sugarcube-modal'

It is nice that any UIViewController can present a modal, but if you have tabs or navs or crap in the way, this is actually NOT what you want. You should use the rootViewController (whatever it may be) to present to modal.

And since this is a property on UIWindow, which is more-or-less a constant, we can make this the easiest to do!

include SugarCube::Modal  # make these methods available globally

view_ctlr = EditSomethingViewController.new
present_modal(view_ctlr)
# ...later, when all is well...
dismiss_modal

These accept completion blocks:

present_modal(view_ctlr) { puts "Now You See Me!" }
dismiss_modal { puts "Now You Don't!" }

If you like these methods, but you want to specify the reciever, they are re-defined on UIViewController for this purpose:

controller.present_modal(other_controller) { puts "presented" }

Numbers

require 'sugarcube-numbers'

Converting input

Uses NSNumberFormatter to try and parse a human-readable number string.

if input.nan?
  UIAlertView.alert('not a number!')
else
  number = input.to_number
  # convert a currency
  number = input.to_number(NSNumberFormatterCurrencyStyle)
  # convert from symbol, requires 'sugarcube-constants'
  number = input.to_number(:currency)
end

Pretty print numbers

Use NSNumberFormatter to easily format a number in the current locale

10000.string_with_style  # => "10,000"
10000.string_with_style(NSNumberFormatterCurrencyStyle)  # => "$10,000.00"
# will convert symbol-constants using the sugarcube-constants package, if it is available
10000.string_with_style(:currency)  # => "$10,000.00"

Percent

100.0.percent # => 1.00
55.0.percent # => 0.55

Ordinals

# some number-to-string stuff
1.nth  # => 'st'
2.nth  # => 'nd'
3.nth  # => 'rd'
4.nth  # => 'th'
11.nth  # => 'th'
13.nth  # => 'th'
21.nth  # => 'st'
23.nth  # => 'rd'

Angles

Since you always want to work in radians, calling 10.degrees returns 10°, in radians. You can convert back to degrees using to_degrees. Lastly, you can specify a multiple of π as a number:

10.degrees  # => π / 18
45.degrees  # => π / 4
3.14159.to_degrees  # => approx 180
2.pi  # => 6.28318...

Distances

If you thought conversion from degrees to radians looks weird, you'll hate conversion from meters to miles:

distance = 1500  # this is in meters.  why?  because all the methods that return
                 # a "distance" return it in meters
distance = 2.miles  # => 3218.688, that's how many meters are in 2 miles
1500.in_miles  # converts meters to miles => 0.932056427001953

Time

By now you have probably "gotten it" and are hooked on these number helpers. Heads up, though, some of these conflict with MotionSupport's definition.

1.second * 5.per_second = 5
1.day * 3.per_hour = 72
1.year.in_minutes = 525960.0

Sizes

Similar conversion methods for hard disk sizes. Uses the "mebi-byte" concepts, e.g. 1024 bytes in a kilobyte.

1.byte      # => 1
1.kilobyte  # => 1024
1.megabyte  # => 1048576
1.gigabyte  # => 1073741824
1.terabyte  # => 1099511627776
1.petabyte  # => 1125899906842624
1.exabyte   # => 1152921504606846976

1.megabyte.in_kilobytes   # => 1024

Screen

1.pixel  # => 1 on non-retina, 0.5 on retina.
         # Useful when drawing if you want to specify the number of pixels

AttributedString

require 'sugarcube-attributedstring'

These are pretty fun! Check out [nsattributedstring_spec.rb][] for all the supported attributes (in theory they are all supported, but there's weird issues with missing constants).

'test'.nsattributedstring({})  #=> NSAttributedString.alloc.initWithString('test', attributes:{})
'test'.attrd  # => alias for `nsattributedstring`
'test'.bold  # => NSAttributedString.alloc.initWithString('test', attributes:{NSFontAttributeName => :bold.uifont})
'test'.italic  # => NSAttributedString.alloc.initWithString('test', attributes:{NSFontAttributeName => :italic.uifont})
'test'.underline  # => NSAttributedString.alloc.initWithString('test', attributes:{NSUnderlineStyleAttributeName => NSUnderlineStyleSingle})

# you can chain 'em, too.
'test'.bold.underline
# If you look up NSAttributedString Application Kit Additions, you can see all
# the constants.  Each of those has a method on NSAttributedString.

# you can add 'em, but the FIRST one MUST be an NSAttributedString
'test'.attrd + '-ing'.italic

# And there's where it gets FUN:
('This'.italic + ' is going to be ' + 'FUN'.bold).underline

# and if you need to join a bunch of strings, there's a helper for that!
['This', 'is'.bold, 'handy!'].join_attrd(' ')
# or join plain strings with an attributed string:
['a', 'b', 'c'].join_attrd('-'.bold)
# join_attrd will *always* return an NSAttributedString
['a', 'b', 'c'].join_attrd(' ')  # == NSAttributedString ('a b c')

# You can even generate attributed text from html!  This feature uses the
# NSHTMLTextDocumentType option to convert the html to NSAttributedString
'Why on <b>earth<b> would you want to do <em>that</em>?'.attributed_html

And you can easily turn an attributed string into a label, if you include the sugarcube-ui package.

view << (("We just met\n".attrd +
          "and this is " + "CRAZY".italic + "\n"
          "But here's my " + "id_rsa.pub".monospace + " file,\n" +
          "so give me SSH access.").uilabel

568

require 'sugarcube-568'

If you require 'sugarcube-568' in your Rakefile, you can use UIImage.imageNamed(name) or name.uiimage to load images that are specific to the 4" iphone.

'tall'.uiimage  # => UIImage.imageNamed('tall')
# => tall.png on iphone 3g
# => [email protected] on iphone 4
# => [email protected] on iphone 5

This code is ported from https://github.com/gaj/imageNamed568, which I had some problems with on RubyMotion (it worked, but not always. Very strange).

Files

Methods to find document files, resource files, cache files, temporary files, access entries out of the Info.plist file, and write data/arrays/dictionaries to a file.

require 'sugarcube-files'

# file operations
"my.plist".file_exists?   # => NSFileManager.defaultManager.fileExistsAtPath("my.plist")

"my.plist".remove_file!  # => NSFileManager.defaultManager.removeItemAtPath("my.plist".document, error: error)  (returns error, if any occurred)

"my.plist".document_path  # => NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0].stringByAppendingPathComponent("my.plist")
"my.plist".cache_path  # => NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, true)[0].stringByAppendingPathComponent("my.plist")
"my.plist".temporary_path  # => NSTemporaryDirectory().stringByAppendingPathComponent("my.plist")

# all of these can be turned into a URL, too
"my.plist".temporary_path.file_url  # => NSURL.fileURLWithPath("my.plist".temporary_path)

# get the resource path, useful if you include json files or images you manipulate in the app
"my.plist".resource_path  # => NSBundle.mainBundle.resourcePath.stringByAppendingPathComponent("my.plist")
# same, but get a URL instead - often used to display a static HTML page that is stored in resources.
# getting a path URL from a resource is just a little different from creating a URL from any other type of path
"index.html".resource_url  # => NSBundle.mainBundle.URLForResource("index", withExtension:"html")

# access data from Info.plist
"CFBundleVersion".info_plist  # => NSBundle.mainBundle.infoDictionary["CFBundleVersion"]

# write to file
[].write_to('array.plist'.document_path)
array = NSArray.read_from('array.plist'.resource_path)

{}.write_to('dict.plist'.document_path)
dict = NSDictionary.read_from('dict.plist'.resource_path)

data = UIImagePNGRepresentation('some_image'.uiimage)
# using sugarcube-nsdata:
# data = 'some_image'.uiimage.nsdata
data.write_to('some_image.png'.document_path)
image_data = NSData.read_from('some_image.png'.document_path)

SpriteKit

node_a = SKNode.node
node_a.name = 'parent'

node_b = SKNode.node
node_b.name = 'child'
node_c = SKNode.node
node_c.name = 'child'

# add child nodes
node_a << node_b
node_a << node_c

# set user data
node_a[:life] = 100

# enumerate child nodes
node_a.each_named('child') do |node, stop_ptr|
  if node == node_b
    stop_ptr.value = true
  end
end

node_a.run_action(SKAction.fadeOut) do
  # done fading out
end

Localized

require 'sugarcube-localized'

# 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')

# If you have an NSError object, you can retrieve the localizedDescription the same way:
error.localized
error._

NSCoder

Shorthands and hash-like access to the coder/decoder objects.

require 'sugarcube-nscoder'

# hash access is the handiest
coder['key'] = self.value
@value = decoder['key']

if decoder.key?('key')
  value = decoder['key']
end

# but if you want to store booleans and such (in their C form,
# which will take up less space):
coder.set('sugarcube_is_neat', toBool: self.sugarcube_is_neat?)
@sugarcube_is_neat = decoder.bool('sugarcube_is_neat')

coder.set('number_of_things', toInt:self.number_of_things)
@number_of_things = decoder.int('number_of_things')

# Archiving and unarchiving is straightforward
# archiving uses NSKeyedArchiver
NSCoder.archive(root_object)  # returns NSData
NSCoder.archive(root_object, to_file: 'foo'.document_path)  # returns success boolean
# unarchiving uses NSKeyedUnarchiver
NSCoder.unarchive(data)
NSCoder.unarchive('foo'.document_path)

# the entire list of encode/decode helpers:
coder.set(key, toBool:value)
coder.set(key, toDouble:value)
coder.set(key, toFloat:value)
coder.set(key, toInt:value)
coder.set(key, toPoint:value)
coder.set(key, toRect:value)
coder.set(key, toSize:value)

decoder.bool(key)
decoder.double(key)
decoder.float(key)
decoder.int(key)
decoder.point(key)
decoder.rect(key)
decoder.size(key)

NSData

require 'sugarcube-nsdata'

Going to and from NSData is really useful when doing HTTP posts.

# default string encoding is UTF8, you can pass other encodings to this method
string_data = 'String'.nsdata

# PNG data representation
image = 'an image'.uiimage
image_data = image.nsdata

# this will download the URL contents
url = 'http://localhost'.nsurl
url.nsdata  # => NSData.dataWithContentsOfURL(self)

string_data.nsstring  # => 'String'
image_data.uiimage  # => whatever 'an image' was

NSDate (wiki)

require 'sugarcube-nsdate'

This package includes additions to the NSDate class, and related additions to Numeric and NSString. There's a lot here, so check out the [wiki]NSDate Wiki for detailed information.

Foundation

Smaller additions to the CoreFoundation classes. Some extensions, like NSDate, are large enough that they get broken out into their own packages.

require 'sugarcube-foundation'

NSArray
[1, 3].nsindexpath  # NSIndexPath.indexPathWithIndex(1).indexPathByAddingIndex(3)
[1, 3].nsindexset
[1, 3].nsset
NSIndexSet
index_set.to_a  # => [1, 2, ...]
NSIndexPath
index_path.to_a  # => [1, 2, ...]
NSString
'https://github.com'.nsurl
'/path/to/file'.fileurl
'https://google.com/search?q=' + 'search terms'.escape_url  # => "..?q=search%20terms"
'%20'.unescape_url  #=> " "
'nô àccénts!'.remove_accents  # => "no accents!"
NSURL
url = 'https://github.com'.nsurl
url.can_open?  # => true, it will open in safari
url.open  # opens in safari
url.nsurlrequest  # convert to NSURLRequest object

IndexPath

require 'sugarcube-indexpath

Use the IndexPath class to match NSIndexPath objects, for instance in a UITableViewDelegate.

index_path = [0, 2].nsindexpath
case index_path
when IndexPath[0]
when IndexPath[1, 0..5]
when IndexPath[1, 5..objects.length]
end
[0, 2].nsindexpath.to_a == [0, 2]  # => true

NSUserDefaults

require 'sugarcube-nsuserdefaults'

This file does one thing very DANGEROUS... to "help" with defaults.

When storing nil into NSUserDefaults, it is converted into false, because Cocoa complains if you give it nil, and the RubyMotion runtime refuses to allow the NSNull.null object. Without relying on an external project (like nsnulldammit I don't know of a sensible workaround...

If you want to "tap into" the defaults system that SugarCube uses, add a to_nsuserdefaults method and that will get called if you hand your object to NSUserDefaults[]=. However, there's no way to get it back later, so the usefulness of this is very limited.

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

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

Keep in mind that NSUserDefaults serializes the object you pass to it, it doesn't maintain a reference. That means that if you modify an object in place, it will not get persisted. An example will explain this better:

NSUserDefaults['test'] = { my: 'test' }
NSUserDefaults['test']['my'] == 'test'
# NSUserDefaults['test'] returns the hash, and the 'my' key returns 'test', so
# this comparison returns `true`

# but there is a temptation, perhaps, to modify that hash:
NSUserDefaults['test']['my'] = 'new'  # BUG

# the corrected code
test = NSUserDefaults['test']
test['my'] = 'new'
NSUserDefaults['test'] = test  # saved

CoreGraphics

This package is included by a few of the other packages, like repl, animations, and image.

Is it CGMakeRect or CGRectMake? What arguments does CGRect.new take?

Instead, just use the coercion methods Rect(), Size() and Point(). They will happily convert most sensible (and some non-sensible) arguments into a CGRect/CGSize/CGPoint struct.

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

For more CoreGraphics additions, you should use geomotion by Clay Allsopp. It adds methods to CGRect, CGPoint, and CGSize to make these structures more rubyesque (these methods used to be part of SugarCube, but were removed in an attempt to decrease the amount of duplicated code).

f = Rect(view.frame)  # the identity function - returns a copy of the CGRect
o = Point(view.frame.origin)  # returns a copy of CGPoint
s = Size(view.frame.size)  # returns a copy of CGSize

# 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
f = Rect([x, y], [w, h])
# one array
f = Rect([[x, y], [w, h]])
# a CGPoint and CGSize
p = Point(x, y)  # or just [x, y] works, too
s = Size(w, h)  # again, [w, h] is fine
f = Rect(p, s)
# any combination of point/array and size/array
f = Rect(p, [w, h])
f = Rect([x, y], s)

Base64

require 'sugarcube-base64'

Todo: add UIImage/NSImage support Todo: add Android support?

Uses the NSData#base64EncodedStringWithOptions and NSData#initWithBase64EncodedData methods to encode/decode base64 data. Normal use is to convert your image/binary data into an NSData instance, and then call to_base64 on that object.

There is a helper on NSString, so you can convert an NSString instance directly to base-64, using UTF8 encoding (or any encoding Apple supports).

base64_str = 'test string'.to_base64
...
NSString.from_base64(base64_str) == 'test string'

# require 'sugarcube-nsdata'
image = 'some_image'.uiimage
data = image.nsdata  # defaults to PNG data
base64_str = data.to_base64
...
data = NSData.from_base64(base64_str)
image = data.uiimage  # defaults to reading PNG data

Pointer

require 'sugarcube-pointer'

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

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

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

To_s

require 'sugarcube-to_s'

to_s methods are defined on the following classes:

  • NSError
  • NSIndexPath
  • NSLayoutConstraint
  • NSNotification
  • NSSet
  • NSURL
  • UIColor
  • UIEvent
  • UIView
  • UILabel
  • UITextField
  • UITouch
  • UIViewController

The output of these is (much?) more useful than the default.

CoreLocation

require 'sugarcube-corelocation'

Open up CLLocationCoordinate2D to provide handy-dandies

# distances

> denver_co = CLLocationCoordinate2D.new(39.739188, -104.985223)
=> #<CLLocationCoordinate2D latitude=39.7391815185547 longitude=-104.985198974609>
> loveland_oh = CLLocationCoordinate2D.new(39.268128, -84.257648)
=> #<CLLocationCoordinate2D latitude=39.2681274414062 longitude=-84.2576293945312>
> denver_co.distance_to(loveland_oh)
=> 1779547.32010451  # in meters
> denver_co.distance_to(loveland_oh).in_miles
=> 1105.75943993609

# move around the globe using x/y distances in miles or kilometers
> denver_co.delta_miles(1101.6, -32.556)
=> #<CLLocationCoordinate2D latitude=39.2685263870498 longitude=-84.2744401887072>
# our location is pretty close!
> denver_co.delta_miles(1101.6, -32.556).distance_to(loveland_oh).in_miles
=> 0.900871117223827

> denver_co.delta_kilometers(10, 10)  # 10 kilometers east, 10 kilometers north
=> #<CLLocationCoordinate2D latitude=39.8290195284119 longitude=-104.868401269013>

Pipes

This package short-circuits the | operator to perform coercion and filtering between all sorts of objects.

Coercion

Any object that defines a coercion method (image.uicolor, string.uiimage, :symbol.uifont) can use the | and the class name to perform the same method.

:label | UIFont  # => # :label.uifont
"image_name" | UIImage  # => "image_name".uiimage
view | UIImage | UIColor  # => view.uiimage.uicolor

Filters

You can pipe objects that have some idea of "filter", like using an image mask or image filter.

image | mask  # => image.masked(mask)
image | darken_cifilter  # => image.apply_filter(darken_cifilter)
# this one is... interesting!
"My name is Mud" | /\w+$/   # => "Mud"
"My name is Mud" | /\d+$/   # => nil
"My name is Mud" | "Mud"  # => "Mud"
"My name is Mud" | "Bob"    # => nil

Awesome

require 'sugarcube-awesome'

SugarCube adds support for Motion-Awesome! The awesome_icon method is added to Symbol, which returns an NSAttributedString that uses the MotionAwesome font. You can pass in :size and :color options.

sugarcube-attributedstring is not required for this extension to function, but it adds NSAttributedString methods that really help. Below I'm usind #+ and #bold and #color to construct an NSAttributedString.

# in Rakefile
require 'sugarcube-awesome'
require 'sugarcube-attributedstring'

# in your app
label.attributedText = (:down_arrow.awesome_icon + ' Going down?'.bold).color(:white)
# OR for buttons
button.setAttributedTitle(:twitter.awesome_icon, forState:UIControlStateNormal)

Anonymous

require 'sugarcube-anonymous'

Convert Hashes into an "anonymous object". Existing keys will be able to be accessed using method names. Uses the SugarCube::Anonymous class to accomplish this, though the usual interface is via Hash#to_object.

h = { foo: 'FOO', 'bar' => 'BAR' }.to_object

# You can use methods instead of keys.
h.foo         # => h[:foo]
h.bar         # => h['bar']
h.foo = 'Foo' # => h[:foo] = 'Foo'
h.bar = 'Bar' # => h['bar'] = 'Bar'

# only existing keys are accessed this way
h.baz          # => NoMethodError
h.baz = 'baz'  # => NoMethodError

Unholy

require 'sugarcube-unholy'

I added these a long time ago to SugarCube, and so it's possible some people use these extensions. Honestly, I should have put them somewhere else, they are silly and not terribly useful, except maybe to me (I use them a lot!). :poop:

class Baz ; end
foo = Baz.new

# (:symbol || 'string').ivar
foo.instance_variable_set(:bar.ivar, value)  # => foo.instance_variable_set(:@bar, value)
foo.instance_variable_set(var_name.ivar, value)  # => foo.instance_variable_set("@#{var_name}", value)

# (:symbol || 'string').setter
foo.send(varname.setter, 'value')

# (:symbol || 'string').cvar
Baz.class_variable_set(var_name.cvar, value)  # => Baz.class_variable_set("@@#{var_name}", value)

Legacy

require 'sugarcube-legacy'

This is where deprecated methods go to suffer a long, slow death.

Contributions

If you want to see new features, please fork, commit, and pull-request! :smiley: