As stated in Chapter 3,
Foundation
defines the primitive object
classes and data types used by all the other classes in Cocoa. It’s
therefore the first stop in our examination of frameworks for Cocoa and
MacRuby.
When it comes to primitive types,
MacRuby developers often have a choice between Cocoa Foundation
classes and native Ruby classes.
Before going through the list of key classes, their purposes, and how to
use them, it is important to understand the differences and relationships
between Ruby primitives and Foundation primitives.
Table 4-1 shows classes that are implemented in such a way that the Ruby classes are compatible with their Foundation and Core Foundation counterparts.
Table 4-1. Ruby/Foundation/Core Foundation compatibility table
Ruby class | Compatible Foundation class | Compatible Core Foundation type |
---|---|---|
Note
Even though the Exception
and
NSException
classes are
not compatible per se, Ruby’s syntax can rescue NSException
instances, meaning that the
developer can define a behavior in case a defined exception is
caught.
This compatibility map
is important because it means that even though a certain API might expect
to receive an instance of NSArray
, if you send it
an instance of Array
, everything will
work as expected. Also, if an API returns an NSMutableDictionary
instance, for example, the
Hash
instance methods can
be used on the returned object. You can use any of the Hash
or NSDictionary/NSMutableDictionary
instance
methods on the returned object.
However, some Ruby classes are not compatible with their Foundation
equivalents and when using them in conjunction with other Cocoa libraries,
one needs to be careful to use the appropriate class. Table 4-2 shows Ruby and Foundation
classes that play similar roles but
are not compatible.
This means if an API expects an object of a certain type, you can’t
provide it with a counterpart from the other column. For instance, if a
Cocoa API expects an instance of NSDate
, you can’t pass it a Ruby date object.
What you might want to do in such a case is convert an object as shown in
Date, Time, and Calendars.
Now that we have this reference frame, let’s look at the key classes defined by Foundation.
Cocoa’s Foundation string class is NSString
. MacRuby’s String
class is fully compatible with NSString
/NSMutableString
because
NSString
is “toll-free bridged” with
its Core Foundation counterpart: CFString
. In other words,
whenever a method is expecting a String
, NSString
, or CFStringRef
, you are free to use whichever class
instance you want.
NSString
and String
have different APIs, but offer more or
less the same features. Here are a few NSString
methods that are not available in the
traditional Ruby API but are quite useful nonetheless:
>>
framework
'Foundation'
=>
true
>>
"/Developer/Examples/Ruby/MacRuby"
.
pathComponents
=>
[
"/"
,
"Developer"
,
"Examples"
,
"Ruby"
,
"MacRuby"
]
>>framework 'Foundation'
=> truefile_path = "~/Music/born_to_be_alive.m4a"
file_path.pathExtension
# => "m4a" # similar to Ruby'sFile.extname(path)
# => ".m4a"
Attributed string objects,
represented in Cocoa by NSAttributedString
,
manage sets of text attributes, such as font and kerning, that are
associated with strings. NSAttributedString
is not a subclass of
NSString
, but it does contain an
NSString
object to which it applies
attributes. NSAttributedString
supports
HTML, Rich Text Format (including file attachments and graphics), drawing
in NSView
objects, and word and line
calculation.
The NSAttributedString
instance
attributes are set/retrieved via a Hash
:
>>framework 'Foundation'
=> truefont = NSFont.fontWithName("Helvetica", size:14.0)
content = "So Long, and Thanks for All the Fish"
attributes = {NSFontAttributeName => font}
attr_string = NSAttributedString.alloc.initWithString(content, attributes: attributes)
attr_string.string
# => "So Long, and Thanks for All the Fish"total_range = NSMakeRange(0,attr_string.string.size)
attr_string.attributesAtIndex(0, effectiveRange: total_range)
=> {"NSFont"=>#<NSFont:0x20027d7c0>}
The AppKit framework also uses NSParagraphStyle
/NSMutableParagraphStyle
to encapsulate the paragraph or ruler attributes used by the NSAttributedString
classes.
Warning
String instances are usually mutable (see Mutability for details about mutability), but might be modified to become immutable.
MacRuby’s Array
implementation is fully compatible with the mutable version of NSArray
. That means that anywhere you need to use or pass a
NSMutableArray
, you can
use an instance of Array
, and vice
versa.
Warning
Array instances are usually mutable (see Mutability for details about mutability), but some APIs might modify a passed array to become immutable. Make sure you know which type you have.
Ruby’s API is often easier to use and less verbose. For instance,
here is how you create a new array instance using Cocoa’s NSMutableArray
:
>>framework 'Foundation'
# don't forget nil as the last element >>NSArray.arrayWithObjects('so', 'say', 'we', 'all', nil)
=> ["so", "say", "we", "all"]
Here’s the same thing using Ruby’s syntax:
>>
[
"so"
,
"say"
,
"we"
,
"all"
]
=>
[
"so"
,
"say"
,
"we"
,
"all"
]
You can read more about Ruby’s Array
on Ruby’s document
website.
The Ruby syntax creates mutable array versions by default, so if
you receive an immutable version of an array and want a mutable version,
you can use the mutableCopy
method:
>>
framework
'Foundation'
=>
true
# create an immutable array
>>
a
=
NSArray
.
arrayWithObjects
(
'terrans'
,
'zerg'
,
'protoss'
,
nil
)
=>
[
"terrans"
,
"zerg"
,
"protoss"
]
>>
b
=
a
.
mutableCopy
=>
[
"terrans"
,
"zerg"
,
"protoss"
]
>>
b
<<
"Xel'Naga"
=>
[
"terrans"
,
"zerg"
,
"protoss"
,
"Xel'Naga"
]
Conversely, if you want a copy of an mutable array to be immutable,
use the #copy
method available from
NSObject
:
>>
array
=
[
'M'
,
'V'
,
'C'
]
=>
[
"M"
,
"V"
,
"C"
]
>>
copy
=
array
.
copy
=>
[
"M"
,
"V"
,
"C"
]
>>
copy
.
class
=>
Array
Another way to achieve the same result is to use Ruby’s dup
instance
method.
Read the NSCopying
and NSMutableCopying
Protocol
References for more information. These protocols are implemented by a
majority of the Cocoa classes.
A few Foundation methods that have no counterparts in the traditional Ruby API can be very useful, including the following:
-
firstObjectCommonWithArray
Returns the first object contained in the receiver that’s equal to an object in another given array. For instance:
>>
[1,2,3].firstObjectCommonWithArray([3,4,5])
=> 3>>
(
[
1
,
2
,
3
]
&
[
3
,
4
,
5
]
)
.
first
=>
3
-
writeToFile:atomically
Writes the contents of the receiver to a file at a given path:
>>
order
=
[
'double double'
,
[
'pickles'
,
'onions'
]
,
{
:animal_style
=>
true
}
]
=>
[
"double double"
,
[
"pickles"
,
"onions"
]
,
{
:animal_style
=>
true
}
]
>>
order
.
writeToFile
(
"/tmp/in-n-out"
,
atomically
:
true
)
=>
true
The array is serialized to disk using a property list, producing the following file:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <array> <string>double double</string> <array> <string>pickles</string> <string>onions</string> </array> <dict> <key>animal_style</key> <true/> </dict> </array> </plist>
It can then be deserialized using the arrayWithContentsOfFile
class method:
>>
NSMutableArray
.
arrayWithContentsOfFile
(
"/tmp/in-n-out"
)
=>
[
"double double"
,
[
"pickles"
,
"onions"
]
,
{
"animal_style"
=>
true
}
]
MacRuby’s Hash
implementation is fully compatible with NSMutableDictionary
,
allowing you to use Ruby and Objective-C’s methods on the same objects. Also, because
NSDictionary
is “toll-free bridged”
with its Core Foundation counterpart, CFDictionary
, the Core Foundation type is interchangeable in function or
method calls with the bridged Foundation object. In other words, you can
use MacRuby, Foundation, or Core Foundation hash/dictionaries
interchangeably. If you are calling a C function that requires an instance
of one of these dictionary classes, you can pass it a Hash
instance, and it will work just
fine.
Since NSDictionary
object
creation is different from its Ruby counterpart, let’s compare both
approaches:
keys
=
[
'Matt'
,
'Laurent'
,
'Vincent'
]
objects
=
[
'double-double'
,
'3x3'
,
'cheeseburger'
]
NSMutableDictionary
.
dictionaryWithObjects
(
objects
,
forKeys
:
keys
)
# => {"Matt"=>"double-double", "Laurent"=>"3x3", "Vincent"=>"cheeseburger"}
NSMutableDictionary
.
dictionaryWithObjectsAndKeys
(
'double-double'
,
'Matt'
,
'3x3'
,
'Laurent'
,
'cheeseburger'
,
'Vincent'
,
nil
)
# => {"Matt"=>"double-double", "Laurent"=>"3x3", "Vincent"=>"cheeseburger"}
{
"Matt"
=>
"double-double"
,
"Laurent"
=>
"3x3"
,
"Vincent"
=>
"cheeseburger"
}
# => {"Matt"=>"double-double", "Laurent"=>"3x3", "Vincent"=>"cheeseburger"}
You can read more about Ruby’s Hash
on Ruby’s document
website.
Just as with NSArray
, Ruby’s API
is often easier to use. NSDictionary
also implements serialization via writeToURL:atomically:
and NSDictionary.dictionaryWithContentsOfFile
.
Ruby’s Set
, like Cocoa’s
NSSet
, NSMutableSet
, NSCountedSet
, and
NSHashTable
classes,
implements unordered collections of distinct elements. It might seem
convenient to build MacRuby’s Set
on
top of one of the Cocoa set classes, but it is actually implemented
entirely from scratch. The main reason is that NSSet
and its variants behave significantly
differently from Ruby’s Set
. So look at
the Cocoa and Ruby implementations and choose whichever one makes more
sense for your case.
The name NSHashTable
might lead you to believe the class
is somehow related to Ruby’s Hash
class, but they are quite different, NSHashTable
is a variant of NSSet
. The big difference between NSHashTable
and the other Set
implementations is that NSHashTable
supports weak references in a
garbage-collected environment. Unless you have a very specific need for
weak references, stick to Ruby or Cocoa’s Set implementations.
NSEnumerator
and
NSFastEnumerationEnumerator
are conceptually the
same as Ruby Enumerator
, except
that the Ruby version is much more powerful.
The aim of these classes is to allow collections of objects, such as arrays, sets, and dictionaries/hashes, to be processed one at a time.
When using the Ruby API, just pass a block (an anonymous method) to one of the many collection methods that return an enumerator:
[
"Eloy"
,
"Matthias"
,
"Joshua"
,
"Thibault"
].
each
do
|
name
|
puts
"
#{
name
}
is a great developer!"
end
# => Eloy is a great developer!
# => Matthias is a great developer!
# => Joshua is a great developer!
# => Thibault is a great developer!
[
"Eloy"
,
"Matthias"
,
"Joshua"
,
"Thibault"
]
The array is being enumerated and each item is sent to the block.
You can also get an Enumerator
instance and store it in a variable to call methods on it later on:
object_enumerator
=
{
'nl'
=>
"Eloy"
,
'ch'
=>
"Matthias"
,
'us'
=>
"Joshua"
,
'fr'
=>
"Thibault"
}
.
each_value
object_enumerator
.
each_slice
(
2
)
do
|
name_1
,
name_2
|
puts
"
#{
name_1
}
and
#{
name_2
}
"
end
# outputs
Thibault
and
Matthias
Eloy
and
Joshua
Cocoa and MacRuby provide much more sophisticated time options than simple timestamps or even date/time formatting routines.
NSDate
is the class that
implements dates and times in Cocoa. It is used to create date objects
and perform date-based calculations. NSDate
objects are invariant points in time
and therefore immutable:
now
=
NSDate
.
date
now
.
description
# => "2009-12-30 23:09:16 -0500"
seconds_per_day
=
24
*
60
*
60
tomorrow
=
NSDate
.
alloc
.
initWithTimeIntervalSinceNow
(
seconds_per_day
)
tomorrow
.
description
# => "2009-12-31 23:09:27 -0500"
NSDate
.
date
# => 2009-12-30 23:10:02 -0500
NSDate
.
dateWithTimeIntervalSinceNow
(
10
.
0
)
# => 2009-12-30 23:10:12 -0500
NSDate
also has more flexible
ways to create dates, for instance:
framework
'Foundation'
NSDate
.
dateWithNaturalLanguageString
(
'next Tuesday dinner'
)
.
description
# =>"2010-01-12 19:00:00 -0800"
NSDate
.
dateWithString
(
"2010-01-10 23:51:05 -0800"
)
.
description
=>
"2010-01-10 23:51:05 -0800"
date_string
=
"3pm June 30, 2010"
NSDate
.
dateWithNaturalLanguageString
(
date_string
)
.
description
# => "2010-06-30 15:00:00 -0700"
NSDate
.
dateWithNaturalLanguageString
(
"06/30/2010"
)
.
description
# => "2010-06-30 12:00:00 -0700"
Here is the Ruby API to create Time
instances:
>>
Time
.
now
# => 2009-12-30 23:19:00 -0500
>>
Time
.
now
+
(
24
*
60
*
60
)
# => 2009-12-31 23:19:00 -0500
Ruby’s date and time APIs are usually more flexible than their
Cocoa counterparts. However, some Cocoa APIs expect NSDate
instances and, in some cases, Ruby’s
API lacks some features. Being familiar with Cocoa’s date and time
management is therefore important.
If you want to convert an instance
of Time
and to an NSDate
instance, use NSDate.dateWithString
and pass it the string
representation of your time object. For example:
NSDate
.
dateWithString
(
Time
.
now
.
to_s
)
The NSCalendar
class
represents calendar objects and covers the most used calendars—Buddhist, Gregorian, Hebrew,
Islamic, and Japanese:
current_calendar
=
NSCalendar
.
currentCalendar
Calendars are used in conjunction with NSDateComponents
and
NSDate
:
today
=
NSDate
.
date
calendar
=
NSCalendar
.
currentCalendar
units
=
(
NSDayCalendarUnit
|
NSWeekdayCalendarUnit
)
components
=
calendar
.
components
(
units
,
fromDate
:
today
)
components
.
day
# => 30
components
.
weekday
# => 4
Consult the reference
documentation about the available NSCalendarUnit
constants to set the different units you might need. If you are using
the Gregorian calendar, you can more easily access most of the date
information using the strftime
method of
Ruby’s Time
class. However, the Cocoa
API is great for getting localized date management and access data
unavailable in Ruby’s API, such as era and quarter.
The NSData
and NSMutableData
classes are
typically used for data storage. NSData
is “toll-free
bridged” with its Core Foundation counterpart: CFData
. This means the Core Foundation type is
interchangeable with the bridged Foundation object in function or method
calls. If a function expects a CFDataRef
parameter, you can send it an NSData
instance, and vice versa.
If you have a string and you want to access its data representation,
use the dataUsingEncoding
method:
string
=
"Some classes gets initiated using data, (i.e NSXMLDocument)"
data
=
string
.
dataUsingEncoding
(
NSUTF8StringEncoding
)
# => #<NSConcreteMutableData:0x200295cc0>
Conversely, if you have some data and want to access a string representation, use the following:
NSString
.
alloc
.
initWithData
(
data
,
encoding
:
NSUTF8StringEncoding
)
# => "Some classes gets initiated using data, (i.e NSXMLDocument)"
Quite often, when I have to deal with a lot of string/data
conversions, I reopen the String and
Data
classes and add some conversion methods:
class
String
def
to_utf8_data
self
.
dataUsingEncoding
(
NSUTF8StringEncoding
)
end
end
class
NSData
def
to_utf8_string
NSString
.
alloc
.
initWithData
(
self
,
encoding
:
NSUTF8StringEncoding
)
end
end
"Can't wait until Taco Tuesday!"
.
to_utf8_data
# => #<NSConcreteMutableData:0x2002ac400>
data
.
to_utf8_string
=>
"Can't wait until Taco Tuesday!"
Note
In Syntactic Sugar you saw that
String
instances respond to #to_data
and NSData
instances respond to #to_str
. However, these methods encode the
data using ASCII 8-bit instead of UTF-8, and sometimes you need the
wider range of characters provided by UTF-8.
NSLocale
, as its name
implies, helps developers deal with different languages and linguistic,
cultural, and technological conventions and standards. Using NSLocale
, you can retrieve the system and/or
user locale and load any of the available locales.
In addition to providing you with the user country and language codes, locales are very useful for displaying all sorts of localized data (time, date, monetary amounts, dimensions, etc.). Locales store information such as symbols used for the decimal separator in numbers, the way dates are formatted, and more:
framework
'Foundation'
locale
=
NSLocale
.
currentLocale
locale
.
objectForKey
(
NSLocaleLanguageCode
)
# => "en"
# We could also use the MacRuby alias
locale
[
NSLocaleLanguageCode
]
# => "en"
locale
[
NSLocaleCurrencyCode
]
# => "USD"
locale
[
NSLocaleCurrencySymbol
]
# => "$"
locale
[
NSLocaleUsesMetricSystem
]
=>
false
locale
[
NSLocaleCalendar
].
calendarIdentifier
=>
"gregorian"
french_locale
=
NSLocale
.
alloc
.
initWithLocaleIdentifier
(
"fr_FR"
)
french_locale
.
displayNameForKey
(
NSLocaleIdentifier
,
value
:
"en_US"
)
# => "anglais (États-Unis)"
Note
MacRuby adds some shortcuts/aliases to improve some of Cocoa’s
APIs. For instance, if an object responds to objectForKey:
and setObject:forKey:
you can use
[]
and []=
instead. This is exactly what we did in
the previous example. Writing locale[NSLocaleLanguageCode]
has exactly the
same effect as writing locale.objectForKey(NSLocaleLanguageCode)
.
NSTimeZone
will provide
you with all the information you need to handle timezones:
time_zone
=
NSTimeZone
.
localTimeZone
time_zone
.
name
# => "America/Los_Angeles"
time_zone
.
abbreviation
# => "PST"
time_zone
.
secondsFromGMT
=>
-
28800
time_zone
.
daylightSavingTime?
# same as calling time_zone.isDaylightSavingTime
=>
false
french_locale
=
NSLocale
.
alloc
.
initWithLocaleIdentifier
(
"fr_FR"
)
time_zone
.
localizedName
(
NSTimeZoneNameStyleStandard
,
locale
:
french_locale
)
# => "heure normale du Pacifique"
Cocoa’s NSException
works the
same as Ruby’s Exception
. As the
following example shows, raised NSException
instances can be caught by the
standard Ruby syntax:
exception
=
NSException
.
exceptionWithName
(
'MacRuby Book'
,
reason
:
'documentation purposes'
,
userInfo
:
nil
)
begin
exception
.
raise
rescue
Exception
=>
e
puts
e
.
message
end
# => MacRuby Book: documentation purposes
As with most of the tasks discussed in this chapter, input and output (I/O) operations can be performed using either the Ruby or the Cocoa API. However, Cocoa will more than likely make your applications more robust and efficient, mainly because a lot of its APIs are asynchronous. Asynchronous APIs are important when writing desktop/mobile applications, because you don’t want to block the main application process while waiting for your I/O operation to finish.
Let’s imagine that you have a Twitter client and want to fetch the latest updates. If you don’t use an asynchronous API, when the user clicks the refresh button, the application will freeze and the “beach volleyball/pizza of death” will spin until the API response is received from Twitter. To ensure a better user experience, it is recommended that you use an asynchronous API and provide some sort of indication of progress.
The NSURL
, NSURLRequest
, and
NSURLConnection
classes
provide developers with ways to manipulate and load URLs and the resources
they reference.
An NSURL
object can refer to a
local or remote resource. It’s the recommended way to refer to files.
Various protocols are supported:
NSURL
also transparently supports
both proxy servers and SOCKS gateways using the user’s system
preferences.
Since NSURL
is a class you will
more than likely use often, let’s start by covering some of the frequently
used methods:
-
NSURL.fileURLWithPath
Initializes and returns a newly created
NSURL
object as a file URL with a specified path:framework
'Foundation'
url
=
NSURL
.
fileURLWithPath
(
"/usr/local/bin/macruby"
)
url
.
description
# => "file://localhost/usr/local/bin/macruby"
isFileReferenceURL
orfileReferenceURL?
Returns
True
if theNSURL
object refers to a file, false otherwise:framework
'Foundation'
macruby
=
NSURL
.
fileURLWithPath
(
"/usr/local/bin/macruby"
)
website
=
NSURL
.
URLWithString
(
"http://macruby.org"
)
macruby
.
fileReferenceURL?
# => true
website
.
fileReferenceURL?
# => false
-
NSURL.URLWithString
Creates and returns an
NSURL
object initialized with the string provided. Here is an example with some of the available accessors. Refer to the API documentation for more details:framework
'Foundation'
url
=
NSURL
.
URLWithString
(
"http://macruby.org/downloads.html#beta"
)
# => #<NSURL:0x200262ba0>
url
.
absoluteString
# => "http://macruby.org/downloads.html#beta"
url
.
fragment
# => "beta"
url
.
host
# => "macruby.org"
url
.
path
# => "/downloads.html"
url
=
NSURL
.
URLWithString
(
"http://macruby.org/downloads.html?sorted=true&nightly=false"
)
url
.
query
# => "sorted=true&nightly=false"
url
=
NSURL
.
URLWithString
(
"http://mattetti:apple@macruby.org:8888/admin"
)
url
.
port
# => 8888
url
.
user
# => "mattetti"
url
.
password
# => "apple"
The following list explains the methods used in the example. The different parts of a URL are defined by RFC 1808:
NSURLRequest
and
NSMutableURLRequest
objects represent URL load requests. These objects contain two different
aspects of a load request: the URL to load and the cache policy to use.
Once a URL request is defined, it can be loaded and processed using
NSURLConnection
or NSURLDownload
.
The concepts in the following subsections apply to both NSURLRequest
and NSMutableURLRequest
, but for the sake
of simplicity I’ll refer just to NSURLRequest
.
Caching allows better performance and optimized resource
usage. Of the standard protocols, NSURLConnection
caches
only HTTP and HTTPS requests, but custom protocols can also
provide caching. NSURLRequest
relies on
a composite on-disk and in-memory cache, which is enabled by
default.
Here is an example where we are sending a request to the MacRuby website ignoring any cached data:
framework
'Foundation'
url
=
NSURL
.
URLWithString
(
'http://macruby.org'
)
request
=
NSMutableURLRequest
.
requestWithURL
(
url
,
cachePolicy
:
NSURLRequestReloadIgnoringCacheData
,
timeoutInterval
:
30
.
0
)
NSURLConnection
.
connectionWithRequest
(
request
,
delegate
:
self
)
puts
"Connecting to http://macruby.org"
# response callback
def
connection
(
connection
,
didReceiveResponse
:
response
)
puts
"response received with status code:
#{
response
.
statusCode
}
"
exit
end
# Keep the main loop running
NSRunLoop
.
currentRunLoop
.
runUntilDate
(
NSDate
.
distantFuture
)
Unless specified otherwise, HTTP requests use the default HTTP cache protocol, which will cache them and use their ETag and Last-Modified headers to determine whether subsequent requests are stale. Using the headers, the cache is evaluated for each request. Read more about HTTP caching in RFC 2616 to see how to take advantage of this feature.
NSURLConnection
allows you to
alter default caching behavior. Four types of caching are available and
can be passed as an argument:
- Default cache policy (
NSURLRequestUseProtocolCachePolicy
) The default policy is specific to the protocol used and is the best conforming policy for the protocol.
- Disabled cache policy (
NSURLRequestReloadIgnoringCacheData
) - Use cached data, fallback on loading the resource (
NSURLRequestReturnCacheDataElseLoad
) Use of the cache can be forced, ignoring cache data age and expiration date. The request is loaded from its source only if no cache data is available. The data is also cached after it is loaded.
- Offline mode (
NSURLRequestReturnCacheDataDontLoad
) This caching policy simulates an offline mode and loads data only from the cache.
We have already touched briefly on the topic of asynchronous versus synchronous APIs. Ruby I/O APIs are synchronous, meaning that when an operation is performed, the process is blocked, waiting for the operation to be done. Blocking a process may not always be a problem. Separate threads can also be started to handle blocking I/Os.
When writing a web application, your application reacts to an action triggered by a user. If there is no traffic on your website, your code is idle. However, when writing a desktop or mobile application, your code is constantly running without requiring user interaction. To keep running, the application has a main run loop that keeps the code running. Within the run loops, you can use asynchronous APIs. Basically, you initiate an operation, return to other activities (or wait for further user input), and receive notification when the operation is done. To get the notification, you implement delegate methods and point to them when initiating the asynchronous operation.
If you are satisfied using synchronous APIs, stick to the Ruby
standard libraries such as net/http
or the methods available on NSString
or NSData
, as shown
here:
framework
'Foundation'
url
=
NSURL
.
URLWithString
'http://macruby.org'
content
=
NSMutableString
.
alloc
.
initWithContentsOfURL
(
url
,
encoding
:
NSUTF8StringEncoding
,
error
:
nil
)
require
'net/http'
content
=
Net
:
:HTTP
.
get
(
'www.macruby.org'
,
'/'
)
However, you will probably be more interested in using the
asynchronous APIs provided by Cocoa. As an example, I’ll discuss the
NSURLDownload
call mentioned in the
previous section, which downloads remote content to file.
NSURLDownload
downloads
requests asynchronously. The request’s body is saved into a file. When
initiating the download, you specify the destination path and the
delegator object containing the callbacks you want to use during the
download. Within the delegator object, you can define a rich set of
optional methods to check on redirection, download status,
authentication challenges, and completion. These run automatically as
the Cocoa runtime detects events. You can also cancel the load if
needed.
Here is a sample download script. It starts by defining a class
arbitrarily named DownloadDelegator
. I’ve chosen to
define four methods, including two variants of download
. After defining the class,
the script initiates a download by creating an object of type NSURLDownload
. The runtime takes care of the
rest, calling downloadDidBegin
and
downloadDidFinish
at the appropriate
moments.
The two download methods are defined on the DownloadDelegator
class. They follow the delegate method signature defined by the NSURLDownload
API. When we create our NSURLDownload
instance, we also create an
instance of DownloadDelegator
, which
we assign as a delegate object. This way the NSURLDownload
instance will automatically try
to dispatch the delegate methods if they are available on the delegate
object. The download
methods are then
called automatically:
framework
'Cocoa'
class
DownloadDelegator
def
downloadDidBegin
(
dl_process
)
puts
"downloading..."
end
def
download
(
dl_process
,
decideDestinationWithSuggestedFilename
:
filename
)
home
=
NSHomeDirectory
()
path
=
home
.
stringByAppendingPathComponent
(
'Desktop'
)
path
=
path
.
stringByAppendingPathComponent
(
filename
)
dl_process
.
setDestination
(
path
,
allowOverwrite
:
true
)
end
def
download
(
dl_process
,
didFailWithError
:
error
)
error_description
=
error
.
localizedDescription
more_details
=
error
.
userInfo
[
NSErrorFailingURLStringKey
]
puts
"Download failed.
#{
error_description
}
-
#{
more_details
}
"
exit
end
def
downloadDidFinish
(
dl_process
)
puts
"Download finished!"
exit
end
end
url_string
=
'http://www.macruby.org/files/nightlies/macruby_nightly-latest.pkg'
url
=
NSURL
.
URLWithString
(
url_string
)
req
=
NSURLRequest
.
requestWithURL
(
url
)
file_download
=
NSURLDownload
.
alloc
.
initWithRequest
(
req
,
delegate
:
DownloadDelegator
.
new
)
# keep the run loop running
NSRunLoop
.
currentRunLoop
.
runUntilDate
(
NSDate
.
distantFuture
)
In Unix-like computer operating systems, pipes allow you to
chain processes. The output of a process (stdout
) feeds
directly the input (stdin
) of another one.
The NSPipe
class provides an
interface for accessing pipes. An NSPipe
class instance represents both ends of a
pipe and enables communication through the pipe.
File handles are used in the NSTask
example a bit
later in this chapter. Look at the example’s code to learn how to create
and monitor pipes.
The NSFileHandle
class
provides a wrapper for accessing open files or communications channels.
File handles can be retrieved for a URL, a path, standard input, standard
output, standard error, or a null device. Some objects, such as pipes,
also expose a file handle. Using a file handle, one can read or write the
data source it references asynchronously.
File handles are used in the NSTask
example a bit later in this chapter. Look
at the example’s code to learn how to use file handles.
A bundle is a collection of code and
resources used by an application, generally created outside the
application. Bundles are usually built with Xcode using the Application or
Framework project types, or using plug-ins. In most cases, your
application bundle will contain
.nib/.xib files, media assets
(images/sounds), dynamic libraries, and frameworks. The NSBundle
class creates
objects that represent bundles.
-
NSBundle.mainBundle
A class method returning the
NSBundle
object that usually corresponds to the application file package or application wrapper (the .app folder holding your code).-
localizations
Returns a list of all the localizations available in the bundle:
framework
'Foundation'
NSBundle
.
mainBundle
.
localizations
# => ['English', 'French']
-
localizedStringForKey:value:table:
Returns the localized version of a string from the Localizable.strings file.
-
pathForResource:ofType:
Returns the full pathname for the resource identified by the specified name and file extension.
The following example registers a custom font (akaDylan.ttf) that was added to the resources folder. We can use it in our UI without having to install it on the user’s machine:
framework
'Foundation'
font_location
=
NSBundle
.
mainBundle
.
pathForResource
(
'akaDylan'
,
ofType
:
'ttf'
)
font_url
=
NSURL
.
fileURLWithPath
(
font_location
)
# using this Core Text Font Manager function to register the embedded font.
CTFontManagerRegisterFontsForURL
(
font_url
,
KCTFontManagerScopeProcess
,
nil
)
The OS X runtime offers both low-level threads and a collection of convenient ways to schedule activities such as timers and operation queues. We’ll look at the various options in this section.
Run loops process input from the mouse, keyboard events
from the window system, NSPort
objects, NSConnection
objects, NSTimer
events, and other sources. Run
loops are essential to keeping your application running and handling
inputs. In Cocoa, the NSRunLoop
class
implements the run loop concept.
You don’t need to create or manage NSRunLoop
instances. Every NSThread
object you
create is already attached to its own NSRunLoop
instance.
We’ve already used NSRunLoop
in
many of the examples in this chapter. What we did was retrieve the
current thread’s run loop and force it to run until a distant
date:
framework
'Foundation'
future
=
NSDate
.
distantFuture
NSRunLoop
.
currentRunLoop
.
runUntilDate
(
future
)
The most interesting methods provided by NSRunLoop
are:
-
NSRunLoop.currentRunLoop
As already shown, this returns the
NSRunLoop
object for the current thread. If a run loop does not yet exist, one is created and returned.-
NSRunLoop.mainRunLoop
-
currentMode
Run loops can run in different modes, each of which defines its own set of objects to monitor. This method returns the run loop’s input mode.
-
runUntilDate
Runs the run loop for a delimited amount of time. More exactly, it sets the run loop to run until a specific date.
Sometimes you might want to trigger a method to run at a
regular interval. For instance, you might want to redraw a video game
screen 30 times a second or call an API every 2 minutes to check the
status of a service. To do that, you can use a timer object defined by
the NSTimer
class.
Timers can be repeated or run once. If it’s a repeating timer, it sends a specified method to a target object again and again based on the defined time interval.
Here is an example of a game loop implementation based on NSTimer
:
framework
'Foundation'
class
GameLoop
def
start
@timer
=
NSTimer
.
scheduledTimerWithTimeInterval
0
.
06
,
target
:
self
,
selector
:
'refresh_screen:'
,
userInfo
:
nil
,
repeats
:
true
end
def
refresh_screen
(
timer
)
puts
"refresh"
end
def
stop_refreshing
@timer
.
invalidate
&&
@timer
=
nil
if
@timer
end
end
GameLoop
.
new
.
start
NSRunLoop
.
currentRunLoop
.
runUntilDate
(
NSDate
.
distantFuture
)
NSTimer
instances can fire only
when the run loop they live in is running. Also, you need to keep in
mind that the effective resolution of a timer is approximately 50 to 100
milliseconds. This should be fine for UI-driven interactive applications
that don’t depend on finer-grained execution times.
There are three ways to schedule a timer. The first is to use the following:
NSTimer.scheduledTimerWithTimeInterval:invocation:repeats:
NSTimer.scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
This method creates the timer and schedules it on the current run loop in the default mode.
This method relies on the additional NSInvocation
class, so
to keep things simpler, let’s look at an example using a second
approach:
framework
'Foundation'
def
do_something
(
timer
)
puts
'Do something'
end
NSTimer
.
scheduledTimerWithTimeInterval
0
.
06
,
target
:
self
,
selector
:
'do_something:'
,
userInfo
:
nil
,
repeats
:
true
NSRunLoop
.
currentRunLoop
.
runUntilDate
(
NSDate
.
distantFuture
)
The first argument is the time interval, so in this case, the
timer is going to fire 15 times a second (every 0.06 seconds). When the
timer is triggered, it will send the do_something:
selector to self
(target:
) and will pass a nil argument
(specified by userInfo:
). The timer
will be repeated indefinitely.
Note
Selectors represent methods. A selector is just a string consisting of the method name followed by a colon.
You can pass any object to the method triggered by the timer. To
do so, just pass your object via the userInfo
argument. If
you have multiple items of data to pass, combine them in an array or
object. To retrieve the passed data, call #userInfo
on the timer object passed to the
callback.
The following is an example of a timer that fires just once:
framework
'Foundation'
def
do_something
(
timer
)
puts
"
#{
timer
.
userInfo
}
says: do something!"
exit
end
NSTimer
.
scheduledTimerWithTimeInterval
0
.
5
,
target
:
self
,
selector
:
'do_something:'
,
userInfo
:
'Simon'
,
repeats
:
false
NSRunLoop
.
currentRunLoop
.
runUntilDate
(
NSDate
.
distantFuture
)
# outputs:
# => 'Simon says: do something!'
Notice that the userInfo
object
is evaluated only the first time it’s called. The following contrived
example should make that clear. No matter when the timer runs, the
do_something
prints just the time
that was stored in its argument the first time it ran:
framework
'Foundation'
def
do_something
(
timer
)
puts
timer
.
userInfo
end
NSTimer
.
scheduledTimerWithTimeInterval
0
.
5
,
target
:
self
,
selector
:
'do_something:'
,
userInfo
:
Time
.
now
,
repeats
:
true
NSRunLoop
.
currentRunLoop
.
runUntilDate
(
NSDate
.
distantFuture
)
# outputs:
# => 2010-01-10 23:05:45 -0800
# => 2010-01-10 23:05:45 -0800
# => 2010-01-10 23:05:45 -0800
# => 2010-01-10 23:05:45 -0800
# => 2010-01-10 23:05:45 -0800
# [...]
Another option is to create a timer and not schedule it to a run loop right away. Do this using one of the following calls:
NSTimer.timerWithTimeInterval:invocation:repeats:a NSTimer.timerWithTimeInterval:target:selector:userInfo:repeats:
After the timer is created, you must add it to a run loop using
the NSRunLoop
’s addTimer:forMode:
method. For example:
framework
'Foundation'
def
do_something
(
timer
)
puts
'do something'
end
timer
=
NSTimer
.
timerWithTimeInterval
0
.
5
,
target
:
self
,
selector
:
'do_something:'
,
userInfo
:
nil
,
repeats
:
true
# the timer isn't scheduled yet
# let's schedule it:
NSRunLoop
.
currentRunLoop
.
addTimer
(
timer
,
forMode
:
NSDefaultRunLoopMode
)
NSRunLoop
.
currentRunLoop
.
runUntilDate
(
NSDate
.
distantFuture
)
Finally, you can create a timer with a defined fire date and then attach it to a run loop:
framework
'Foundation'
def
do_something
(
timer
)
puts
'do something'
exit
end
in_2_seconds
=
Time
.
now
+
2
timer
=
NSTimer
.
alloc
.
initWithFireDate
NSDate
.
dateWithString
(
in_2_seconds
.
to_s
),
interval
:
0
.
5
,
target
:
self
,
selector
:
'do_something:'
,
userInfo
:
nil
,
repeats
:
false
NSRunLoop
.
currentRunLoop
.
addTimer
(
timer
,
forMode
:
NSDefaultRunLoopMode
)
NSRunLoop
.
currentRunLoop
.
runUntilDate
(
NSDate
.
distantFuture
)
You might want to run another program as a subprocess and monitor its execution. An easy way to do that is to use Ruby’s API to shell out and call a program:
puts
`/bin/ls -la /`
The back ticks execute the command, which is then printed. While this is very easy and nice, it blocks the main loop. So, if we were to start, for instance, an encoding process, we would have to wait for it to finish before we could continue the execution of our program. Not always ideal.
This is exactly when Cocoa’s NSTask
class shines.
NSTask
objects create separate
executable entities. You can monitor the execution of your task using
observers.
An NSTask
needs to be passed a
command to execute, the directory in which to run the task, and optional
standard input/output/error values. An NSTask
instance can be run only once.
Subsequent attempts to run the task will raise an exception.
Here is an example showing how to asynchronously start a new process and monitor it:
framework
'Foundation'
class
AsyncHandler
def
data_ready
(
notification
)
data
=
notification
.
userInfo
[
NSFileHandleNotificationDataItem
]
output
=
NSString
.
alloc
.
initWithData
(
data
,
encoding
:
NSUTF8StringEncoding
)
puts
output
end
def
task_terminated
(
notification
)
exit
end
end
notification_handler
=
AsyncHandler
.
new
nc
=
NSNotificationCenter
.
defaultCenter
task
=
NSTask
.
alloc
.
init
pipe_out
=
NSPipe
.
alloc
.
init
task
.
arguments
=
[
'-la'
]
task
.
currentDirectoryPath
=
"/"
task
.
launchPath
=
"/bin/ls"
task
.
standardOutput
=
pipe_out
file_handle
=
pipe_out
.
fileHandleForReading
nc
.
addObserver
(
notification_handler
,
selector
:
"data_ready:"
,
name
:
NSFileHandleReadCompletionNotification
,
object
:
file_handle
)
nc
.
addObserver
(
notification_handler
,
selector
:
"task_terminated:"
,
name
:
NSTaskDidTerminateNotification
,
object
:
task
)
file_handle
.
readInBackgroundAndNotify
task
.
launch
# keep the run loop running
NSRunLoop
.
currentRunLoop
.
runUntilDate
(
NSDate
.
distantFuture
)
We start by defining an AsyncHandler
class that implements some
callback methods (data_ready
and
task_terminated
). We
then create our task and a pipe to monitor the output of the task.
Finally, we add an observer on the pipe’s file handle (so we’ll know
when data is being written to it) and another notification (so we’ll
know when the task is done running). These observers invoke the callback
methods we defined at the start.
The task is executed and the output is printed as it is pushed by the notification system. Once the task is done running, the program exits.
Threads are often used to run code that might take some time to execute, when the developer doesn’t want to block the execution of the rest of the application.
In the case of a Cocoa application, the main thread handles the UI and the various inputs (user, network, devices, etc.). Using threads leaves the main thread to run smoothly while lengthy processing runs separately.
Using multiple threads can also distribute the load to multiple cores and therefore improve the performance of your code when run on multicore machines.
OS X uses POSIX threads, which are a standard seen on many Unix-type
systems. But Cocoa has enhanced these threads to use a simple locking
mechanism, NSLock
. You can create
threads using Cocoa’s NSThread
class or
Ruby’s Thread
. Ruby’s Thread
class creates only
POSIX threads, while NSThread
creates
Cocoa’s enhanced threads by default, with the option of creating POSIX
threads.
The only “inconvenience” with using POSIX threads is that unless
you start an NSThread
instance, Cocoa
frameworks will think you are running in a single-threaded mode. This
was historically important to optimize threaded versus nonthreaded code
path, but nowadays the difference is not very important.
Warning
It’s fine to mix Cocoa threads and POSIX threads, but make sure to use their respective mutex classes to lock them.
An NSThread
can be initiated
with a target and method to call on it. Here is an example showing how
to start an expensive process without blocking the main thread:
framework
'Foundation'
class
ExpensiveCalculation
def
start
(
to_process
)
puts
"processing on a separate thread"
sleep
(
2
)
puts
"processing is over"
end
end
calculation
=
ExpensiveCalculation
.
new
thread
=
NSThread
.
alloc
.
initWithTarget
(
calculation
,
selector
:
'start:'
,
object
:
'dummy_obj'
)
thread
.
start
puts
"the main thread is not blocked"
NSRunLoop
.
currentRunLoop
.
runUntilDate
(
NSDate
.
dateWithTimeIntervalSinceNow
(
3
.
0
))
The output will be something like the following. The order of statements may be different, because the order in which the main thread and subordinate thread run is unpredictable:
processing
on
a
separate
thread
the
main
thread
is
not
blocked
processing
is
over
Once you have an NSThread
instance, you can also ask for its status:
framework
'Foundation'
thread
=
NSThread
.
alloc
.
init
thread
.
start
thread
.
cancelled?
# => false
thread
.
executing?
# => false
thread
.
finished?
# => true
MacRuby developers might, however, prefer to use Ruby’s Thread
API or Grand Central Dispatch
(GCD).
As mentioned earlier, MacRuby’s Thread
class creates POSIX threads. The API is
very simple:
def
print_char
(
char
)
1
.
upto
(
5_000
)
do
char
end
end
Thread
.
new
{
print_char
(
'1'
)}
print_char
(
'0'
)
The output will look more or less like the following:
# [..]
0000000000001111111111111111111111111111111111111111111111111111111111111111111
1111111111011111111011111111111110011111111111110011111111111111001111111111111
0111111011111111111111001111111111111100000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000000000000000011
1111101111111111111111100111111111111100111111011111111011111111111111000000000
0000000000000000000000000000000000000000000000000000000000000001111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111
# [..]
You can also pass a variable to a thread as follows:
[
'Joshua'
,
'Laurent'
,
'Matt'
].
each
do
|
dev_name
|
Thread
.
new
(
dev_name
){
|
name
|
puts
"Hello,
#{
name
}
!
\n
"
}
end
Each thread has access to its own local variable called name
.
Dealing with threads can be quite cumbersome and difficult. Thankfully, Cocoa encapsulates threads by grouping work that you want to perform asynchronously into what are called operations. Operations can be used on their own or queued up and prioritized to run asynchronously.
Operations are discussed more in depth in Concurrency, but let’s look at how you can create a queue
and add operations to it. To do this, we will use the NSOperationQueue
class and NSOperation
.
Because the NSOperation
class
is an abstract class, we will need to create a subclass to define our
custom operation and add it to a queue:
framework
'Foundation'
class
MyOperation
<
NSOperation
attr_reader
:name
def
initWithName
(
name
)
init
@name
=
name
self
end
def
main
puts
"
#{
name
}
get to work!"
end
end
operation_matt
=
MyOperation
.
alloc
.
initWithName
(
'Matt'
)
queue
=
NSOperationQueue
.
alloc
.
init
queue
.
addOperation
(
operation_matt
)
# run the main loop for 2 seconds
NSRunLoop
.
currentRunLoop
.
runUntilDate
(
NSDate
.
dateWithTimeIntervalSinceNow
(
2
.
0
))
To create a valid NSOperation
subclass, we need to create a custom initiator (in this case, we used
initWithName
) and define a main
method that will be executed when the
operation is triggered.
Most applications trigger a lot of events in the runtime, ranging from a user clicking on a UI element to an I/O operation. Cocoa’s Foundation supplies a programming architecture that allows you to send and receive notifications and to subscribe to occurring events.
The notification architecture is divided into three parts: the notifications themselves, the notification centers, and the notification observers. When an event happens, a notification is created and sent to a notification center, which then broadcasts the notification to all the observers that registered for the event. Notifications can also be held in a notification queue before being broadcast.
NSNotification
objects
encapsulate information about events. The objects don’t know much about
anything except the event that sends them.
If you need to manage notifications in just one process,
use the NSNotificationCenter
class. Otherwise, you will have to rely on NSDistributedNotificationCenter
.
As explained earlier, notification centers receive notifications
posted by events and dispatch them to registered observers. Observers
are added to notification centers, and notifications can be manually
posted to a center or automatically triggered by classes triggering
notifications. The default notification center can be accessed using the
NSNotificationCenter.defaultCenter
method:
framework
'Foundation'
class
NotificationHandler
def
tea_time
(
notification
)
puts
"it's tea time!"
end
end
center
=
NSNotificationCenter
.
defaultCenter
notification_handler
=
NotificationHandler
.
new
center
.
addObserver
(
notification_handler
,
selector
:
"tea_time:"
,
name
:
'tea_time_reminder'
,
object
:
nil
)
center
.
postNotificationName
(
"tea_time_reminder"
,
object
:
self
)
Warning
A common problem reported by programmers new to notification centers is that their notifications are not working. This is often because their observers are garbage-collected, and, therefore, never get triggered. By design, observers use weak references instead of strong references, and registrations are automatically cleaned up when an observer is collected. You therefore need to ensure that the observers don’t get garbage-collected for as long as you want the notification registration to remain. One way to do this is to assign them to instance variables of the class.
One potential problem with notifications is that they are synchronous. Therefore, if an observer calls a slow method, the execution of the rest of the code is delayed. Notification queues, on the other hand, allow the coalescing of notifications as well as asynchronous posting.
As mentioned earlier, every thread has its own default notification center, but what I did not mention is that every default notification center also has a default notification queue. You can also create your own notification queue and associate it with a notification center.
To retrieve the default notification queue in the current thread,
use the defaultQueue
class
method on NSNotificationQueue
:
framework
'Foundation'
NSNotificationQueue
.
defaultQueue
# => #<NSNotificationQueue:0x200211100>
Now that you have a queue, you can post notifications to it. You have three options for doing this:
Posting as soon as possible, bypassing the queue (but posting only at the end of current run loop iteration).
Posting synchronously but with coalescence. The notification queue coalesces (combines) messages in two ways: by discarding identical duplicates of a message that arrive during a given time period and by combining small messages into a single notification.
These notification styles or priorities are flags that you set when posting the notification. To dispatch a notification using each of the three priorities, retrieve the default notification queue for the current thread and create a notification that is ready to be posted:
framework
'Foundation'
queue
=
NSNotificationQueue
.
defaultQueue
notification
=
NSNotification
.
notificationWithName
(
'fetch_feed'
,
object
:
nil
)
To post a notification as soon as possible, use the NSPostASAP
style:
queue
.
enqueueNotification
(
notification
,
postingStyle
:
NSPostASAP
)
This is often used for posting to an expensive resource, such as
the display server, to avoid having many clients flushing the window
buffer after drawing within the same run loop iteration. You can have
all the clients post the same NSPostASAP
style notification with coalescing
on name and object. As a result, only one of those notifications is
dispatched at the end of the run loop and the window buffer is flushed
only once.
To post a notification when the run loop is waiting (idle), use the following:
queue
.
enqueueNotification
(
notification
,
postingStyle
:
NSPostWhenIdle
)
Here is a simple example: imagine you are writing a text editor
and you want to display some statistics at the bottom of the page, such
as the number of lines, characters, words, and paragraphs in the buffer.
If you were to recalculate this information every time a key is pressed,
it would result in quite a lot of resource usage, especially if the user
types quickly. Instead, you can queue the notifications using the
NSPostWhenIdle
style
and post the information only when there is a pause in typing. This
saves loads of resources. The notification posting code will look like
the following code. Imagine that text_field
returns the main editor window with
the text field we want to monitor:
notification
=
NSNotification
.
notificationWithName
(
'text_edited'
,
object
:
text_field
)
queue
=
NSNotificationQueue
.
defaultQueue
queue
.
enqueueNotification
(
notification
,
postingStyle
:
NSPostWhenIdle
coalesceMask
:
NSNotificationCoalescingOnName
forModes
:
nil
)
The coalesceMask
parameter
tells the notification queue how to coalesce notifications.
Notifications can be coalesced by name (NSNotificationCoalescingOnName
) or by object
(NSNotificationCoalescingOnSender
).
Alternatively, this parameter can disable coalescing (NSNotificationNoCoalescing
). You can also
combine the constants using the bitwise OR operator:
NSNotificationCoalescingOnSender
|
NSNotificationCoalescingOnName
Finally, to post a notification synchronously but still using the coalescence feature of the queue, use the following:
queue
.
enqueueNotification
(
notification
,
postingStyle
:
NSPostNow
)
Using the NSPostNow
style is the
same thing as using postNotification
or
notificationWithName
,
but it has the advantage of being able to combine similar notifications
using coalescence. You will often want to use this type of notification
when you want a synchronous behavior, basically, whenever you want to
ensure that observing objects have received and processed the
notification before doing something else. Using coalescence, you still
ensure dispatched notifications are handled synchronously, but you also
guarantee the uniqueness of these notifications.
There are situations where you need to convert some of your objects into a form that can be saved to a file or transmitted to another process or machine, and then reconstructed. The native binary format that represents objects in memory while you manipulate them is not appropriate for storage and may not be readable on another system. Therefore, Cocoa and Ruby provide standard ways to translate between the binary formats and a format more suitable for storage, often in a human-readable text format. Translating the data into such a format is known as serialization or marshaling.
In some cases, you just need to serialize a simple hierarchical relationship like an API response, whereas in other cases you need to archive complex relationships, as Interface Builder does when it stores the objects and relationships that make up a UI in a nib file.
In Cocoa, archiving uses the NSCoder
abstract class,
which can encode and decode Objective-C objects, scalars, arrays,
structures, and strings. To be properly encoded and decoded, objects have
to implement the NSCoding
protocol. This
defines two methods: one to encode the object and one to restore it. All
of the Foundation
primitive classes
(NSString
, NSArray
, NSNumber
, and so on) and
most of the Application Kit UI objects implement the NSCoding
protocol and can be put into an
archive.
NSCoder
has three types of
archives, which differ in the encoding/decoding process they use:
- Sequential archives
Decoding is done in the same sequence in which the encoding took place. Use for sequential archives when your content needs to be processed linearly.
- Keyed archives
Information is encoded and decoded in a random access manner using assigned name keys. Because the keys can be requested by name, you can decode parts of the data in any order. This option offers flexibility for making serialized objects forward and backward compatible.
- Distributed objects
Used to implement distributed object architectures. To be used only when implementing distributed object architectures, which means when you want to do interprocess messaging between applications or threads.
Here is a simple keyed archiving process:
framework
'Foundation'
# let's assume we have a collection of Objective-C objects stored in
# a variable called objc_objects
archive_path
=
NSTemporaryDirectory
()
.
stringByAppendingPathComponent
(
"Objs.archive"
)
result
=
NSKeyedArchiver
.
archiveRootObject
(
objc_objects
,
toFile
:
archive_path
)
In Ruby, archiving is done using the Marshal
class. All Ruby
objects can be converted into a byte stream and then restored:
class
CylonCenturion
attr_accessor
:battery_life
,
:ammo
end
class
CylonSkinJob
attr_accessor
:physical_health
,
:mental_health
end
squadron
=
[]
10
.
times
{
squadron
<<
CylonCenturion
.
new
}
2
.
times
{
squadron
<<
CylonSkinJob
.
new
}
# encode and save to file
File
.
open
(
'cylon_squadron'
,
'w+'
){
|
f
|
f
<<
Marshal
.
dump
(
squadron
)}
# reload
loaded_squadron
=
Marshal
.
load
File
.
read
(
'cylon_squadron'
)
leader
=
loaded_squadron
.
find
{
|
soldier
|
soldier
.
is_a?
(
CylonSkinJob
)}
Often, you don’t need to serialize complex relationships and objects. What you need is to serialize some basic object types, such as to save some user’s preferences to disk. To do that, you have a few options:
The writeToURL
/writeTofile
options were
covered when we looked at the primitive classes added by Foundation
. Refer to their documentation to see
how to use this API:
For NSPropertyListSerialization
, use the
following:
framework
'Foundation'
user_info
=
{
:points
=>
4200
,
:level
=>
3
,
:name
=>
'Matt'
,
:teams
=>
[
'Blue'
,
'Red'
]
,
:twitter_update
=>
false
,
:urls
=>
{
:github
=>
'http://github.com/mattetti'
,
=>
'http://twitter.com/merbist'
}
}
plist_data
=
NSPropertyListSerialization
.
dataWithPropertyList
(
user_info
,
format
:
NSPropertyListXMLFormat_v1_0
,
options
:
0
,
error
:
nil
)
file_path
=
NSTemporaryDirectory
()
.
stringByAppendingPathComponent
(
"user_info.plist"
)
plist_data
.
writeToFile
(
file_path
,
atomically
:
true
)
You can also use MacRuby’s syntactical sugar:
user_info
=
{
:points
=>
4200
,
:level
=>
3
,
:name
=>
'Matt'
,
:teams
=>
[
'Blue'
,
'Red'
]
,
:twitter_update
=>
false
,
:urls
=>
{
:github
=>
'http://github.com/mattetti'
,
=>
'http://twitter.com/merbist'
}
}
plist
=
user_info
.
to_plist
Here is what the file content looks like:
<
?
xml
version
=
"1.0"
encoding
=
"UTF-8"
?>
<!
DOCTYPE
plist
PUBLIC
"-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd"
>
<
plist
version
=
"1.0"
>
<
dict
>
<
key
>
level
<
/key>
<integer>3</in
teger
>
<
key
>
name
<
/key>
<string>Matt</s
tring
>
<
key
>
points
<
/key>
<integer>4200</in
teger
>
<
key
>
teams
<
/key>
<array>
<string>Blue</s
tring
>
<
string
>
Red
<
/string>
</
array
>
<
key
>
twitter_update
<
/key>
<false/
>
<
key
>
urls
<
/key>
<dict>
<key>github</
key
>
<
string
>
http
:
//
github
.
com
/
mattetti
<
/string>
<key>twitter</
key
>
<
string
>
http
:
//
.
com
/
merbist
<
/string>
</
dict
>
<
/dict>
</
plist
>
To load and recover the object, you can reload the object and use the following code:
framework
'Foundation'
file_path
=
File
.
expand_path
(
"~/user_info.plist"
)
plist_data
=
NSData
.
alloc
.
initWithContentsOfFile
(
file_path
)
user_info
=
NSPropertyListSerialization
.
propertyListFromData
(
plist_data
,
mutabilityOption
:
NSPropertyListMutableContainersAndLeaves
,
format
:nil
,
errorDescription
:
nil
)
p
user_info
# => {"points"=>4200, "twitter_update"=>false,
"urls"
=>
{
"github"
=>
"http://github.com/mattetti"
,
"twitter"
=>
"http://twitter.com/merbist"
},
"level"
=>
3
,
"teams"
=>[
"Blue"
,
"Red"
]
,
"name"
=>
"Matt"
}
Notice that in this example, I chose to use the plist XML format, but I could have chosen to use the plist binary format instead.
Another way to deserialize a property list file is to use MacRuby’s helper to convert the content of the plist file:
file_path
=
File
.
expand_path
(
"~/user_info.plist"
)
user_info
=
load_plist
(
File
.
read
(
file_path
))
Because we know the type of object we are expecting from the deserialization (a dictionary), we could have also done the following:
file_path
=
File
.
expand_path
(
"~/user_info.plist"
)
Hash
.
dictionaryWithContentsOfFile
(
file_path
)
To encode basic types, we can also use YAML or JSON:
require
'yaml'
user_info
=
{
"points"
=>
4200
,
"twitter_update"
=>
false
,
"urls"
=>
{
"github"
=>
"http://github.com/mattetti"
,
"twitter"
=>
"http://twitter.com/merbist"
},
"level"
=>
3
,
"teams"
=>[
"Blue"
,
"Red"
]
,
"name"
=>
"Matt"
}
File
.
open
(
'user_info.yml'
,
'w+'
){
|
f
|
f
<<
user_info
.
to_yaml
}
The content of the YAML file looks like the following:
---
points
:
4200
twitter_update
:
false
urls
:
github
:
http
:
//
github
.
com
/
mattetti
:
http
:
//
.
com
/
merbist
level
:
3
teams
:
-
Blue
-
Red
name
:
Matt
To deserialize the content of the file, simply do the following:
require
'yaml'
YAML
.
load_file
(
'user_info.yml'
)
# => {"points"=>4200, "name"=>"Matt", "twitter_update"=>false,
"urls"
=>
{
"twitter"
=>
"http://twitter.com/merbist"
,
"github"
=>
"http://github.com/mattetti"
},
"level"
=>
3
,
"teams"
=>[
"Blue"
,
"Red"
]
}
Finally, you can use the JSON serialization format, as follows:
require
'json'
user_info
=
{
"points"
=>
4200
,
"twitter_update"
=>
false
,
"urls"
=>
{
"github"
=>
"http://github.com/mattetti"
,
"twitter"
=>
"http://twitter.com/merbist"
},
"level"
=>
3
,
"teams"
=>[
"Blue"
,
"Red"
]
,
"name"
=>
"Matt"
}
File
.
open
(
'user_info.json'
,
'w+'
){
|
f
|
f
<<
user_info
.
to_json
}
The content of the saved file looks like this:
{
"points"
:
4200
,
"name"
:"Matt"
,
"twitter_update"
:false
,
"urls"
:{
"twitter"
:"http://twitter.com/merbist"
,
"github"
:"http://github.com/mattetti"
},
"level"
:
3
,
"teams"
:
[
"Blue"
,
"Red"
]
}
To deserialize the
file, use JSON.load
:
require
'json'
JSON
.
parse
File
.
open
(
'user_info.json'
)
.
read
# => {"points"=>4200, "teams"=>["Blue", "Red"], "twitter_update"=>false,
"name"
=>
"Matt"
,
"urls"
=>
{
"twitter"
=>
"http://twitter.com/merbist"
,
"github"
=>
"http://github.com/mattetti"
},
"level"
=>
3
}
The Foundation
framework also comes with other
useful miscellaneous classes.
Foundation offers two approaches to XML parsing: an
event-driven parser called NSXMLParser
and a
NSXMLNode
based
solution using XPath.
The NSXMLParser
solution is
useful for processing a complete XML file, and is based on delegates.
You need to initiate an NSXMLParser
object with a URL or data. In the following example, I use NSXMLParser.alloc.initWithContentsOfURL
to
load XML from a URL. I then specify a delegate that will be called when
the parser finds elements. Once that is done, I call parse
on the parser object.
Here is a Really Simple Syndication parser built using NSXMLParser
. I won’t go through the details of
this script, but the overall structure gives you an idea of how this
kind of parser works. There is documentation
for NSXMLParser delegates at the Apple developer site:
framework
'Cocoa'
class
RSSParser
attr_accessor
:parser
,
:xml_url
,
:doc
def
initialize
(
xml_url
)
@xml_url
=
xml_url
NSApplication
.
sharedApplication
url
=
NSURL
.
alloc
.
initWithString
(
xml_url
)
@parser
=
NSXMLParser
.
alloc
.
initWithContentsOfURL
(
url
)
@parser
.
shouldProcessNamespaces
=
true
@parser
.
delegate
=
self
@items
=
[]
end
# RSSItem is a simple class that holds all of the RSS items.
# Extend this class to display/process the item differently.
class
RSSItem
attr_accessor
:title
,
:description
,
:link
,
:guid
,
:pubDate
,
:enclosure
def
initialize
@title
,
@description
,
@link
,
@pubDate
,
@guid
=
''
,
''
,
''
,
''
,
''
end
end
# Starts the parsing and send each parsed item through its block.
#
# Usage:
# feed.block_while_parsing do |item|
# puts item.link
# end
def
parse
(
&
block
)
@block
=
block
puts
"Parsing
#{
xml_url
}
"
@parser
.
parse
end
# Starts the parsing but keeps blocking the main run loop
# until the parsing is done.
# Do not use this method in a GUI app. Use #parse instead.
def
block_while_parsing
(
&
block
)
@parsed
=
false
parse
(
&
block
)
NSRunLoop
.
currentRunLoop
.
runUntilDate
(
NSDate
.
distantFuture
)
end
# Delegate getting called when parsing starts
def
parserDidStartDocument
(
parser
)
puts
"starting parsing.."
end
# Delegate being called when an element starts being processed
def
parser
(
parser
,
didStartElement
:
element
,
namespaceURI
:
uri
,
qualifiedName
:
name
,
attributes
:
attrs
)
if
element
==
'item'
@current_item
=
RSSItem
.
new
elsif
element
==
'enclosure'
@current_item
.
enclosure
=
attrs
end
@current_element
=
element
end
# as the parser finds characters, this method is being called
def
parser
(
parser
,
foundCharacters
:
string
)
if
@current_item
&&
@current_item
.
respond_to?
(
@current_element
)
el
=
@current_item
.
send
(
@current_element
)
el
<<
string
end
end
# method called when an element is done being parsed
def
parser
(
parser
,
didEndElement
:
element
,
namespaceURI
:
uri
,
qualifiedName
:
name
)
if
element
==
'item'
@items
<<
@current_item
end
end
# delegate getting called when the parsing is done
# If a block was set, it will be called on each parsed item
def
parserDidEndDocument
(
parser
)
@parsed
=
true
puts
"done parsing"
if
@block
@items
.
each
{
|
item
|
@block
.
call
(
item
)}
end
end
end
=
RSSParser
.
new
(
"http://twitter.com/statuses/user_timeline/16476741.rss"
)
# because we are running in a script, we need the run loop to keep running
# until we are done with parsing
#
# If we would to use the above code in a GUI app,
# we would use #parse instead of #block_while_parsing
.
block_while_parsing
do
|
item
|
item
.
title
end
The delegate object should implement some methods so it is alerted when the parser encounters an event. Delegate methods look like the following:
def
parser
(
parser
,
didStartElement
:
element
,
namespaceURI
:
uri
,
qualifiedName
:
name
,
attributes
:
attrs
)
end
This method, for instance, is called when the parser encounters the beginning of an element. It’s up to the developer to set up the proper business logic to capture and process the information that the parser outputs.
In some cases, however, you already know the structure of the XML
document you are going to process and you will want to access just one
or a limited set of notes. To do that, you can rely on NSXMLNode
/NSXMLDocument
and XPath
or XQuery.
Here is a simple example that fetches the MacRuby home page and searches for the current version using XPath:
framework
'Foundation'
url
=
NSURL
.
alloc
.
initWithString
(
'http://macruby.org'
)
url_content
=
NSMutableString
.
alloc
.
initWithContentsOfURL
(
url
,
encoding
:
NSUTF8StringEncoding
,
error
:
nil
)
data
=
url_content
.
dataUsingEncoding
(
NSUTF8StringEncoding
)
document
=
NSXMLDocument
.
alloc
.
initWithData
(
data
,
options
:
NSXMLDocumentTidyHTML
,
error
:
nil
)
root
=
document
.
rootElement
version_xpath
=
'//*[@id="current_version"]'
error
=
Pointer
.
new
(
:object
)
nodes
=
root
.
nodesForXPath
(
version_xpath
,
error
:
error
)
if
nodes
.
empty?
puts
error
[
0
].
description
else
puts
nodes
.
first
.
stringValue
end
Using Ruby’s enumerators and blocks, you can query a collection of objects. The following example illustrates the procedure:
Actor
=
Struct
.
new
:name
,
:oscars
actors
=
[]
actors
<<
Actor
.
new
(
"Marlon Brando"
,
2
)
actors
<<
Actor
.
new
(
"Sean Penn"
,
2
)
actors
<<
Actor
.
new
(
"Jack Nicholson"
,
3
)
actors
<<
Actor
.
new
(
"Adrien Brody"
,
1
)
actors
<<
Actor
.
new
(
"Neil Patrick Harris"
,
0
)
winners
=
actors
.
find_all
{
|
actor
|
actor
.
oscars
>=
1
}
# => [#<struct Actor name="Marlon Brando", oscars=2>, #<struct Actor name="Sean Penn",
oscars
=
2
>
,
#<struct Actor name="Jack Nicholson", oscars=3>,
#<struct Actor name="Adrien Brody", oscars=1>]
max_oscars
=
actors
.
map
{
|
actor
|
actor
.
oscars
}
.
max
super_star
=
winners
.
find
{
|
actor
|
actor
.
oscars
==
max_oscars
}
# => #<struct Actor name="Marlon Brando", oscars=3>
The code within curly braces resembles what you would put in a
do
loop. The first enumerator, for
instance, is:
|actor| actor.oscars >= 1
Ruby mixes in the Enumerable
module in collection classes to offer various traversal and searching methods such as max
, find
, and find_all
. Thus, as part
of the find_all
method, the previous
code extracts each item that is associated with one or more Oscar
awards.
Note
In this example, I did not explicitly create the Actor
class and its accessors. Instead, I
used the Struct
class to generate
the Actor
class. Read the Ruby
documentation to learn more about this interesting way to generate
simple classes.
In Cocoa, filtering and searching uses the NSPredicate
class. As
the Cocoa documentation explains quite well, this class is used to
define logical conditions that constrain a search either for a fetch or
for in-memory filtering. However, NSPredicate
can be used only on objects that
implement the key-value coding (KVC) protocol, which can be done
manually on custom objects.
Here are a few examples using NSPredicate
:
framework
'Foundation'
predicate
=
NSPredicate
.
predicateWithFormat
(
"SELF IN %@"
,
[
'Ninh'
,
'Hongli'
]
)
predicate
.
evaluateWithObject
"Matt"
# => false
filter
=
NSPredicate
.
predicateWithFormat
(
"SELF beginswith[c] 'm'"
)
[
'Matt'
,
'Mike'
,
'Nate'
].
filteredArrayUsingPredicate
(
filter
)
# => ["Matt", "Mike"]
Undo and redo are common patterns in applications that revert the state of an object. Before the user changes the state of an object, the object registers the initial state as well as the method called on the object. This way, the change can be undone and redone.
The NSUndoManager
class is
implemented in the Foundation
framework because executables other than applications might want to
revert changes to their states.
Application Kit also implements undo and redo in its NSTextView
class,
making it available to all its subclasses.
Here is an example implementing undo/redo on a player class:
framework
'Foundation'
class
Player
attr_accessor
:x
,
:y
def
initialize
@x
=
@y
=
0
end
def
undo_manager
@manager
||=
NSUndoManager
.
alloc
.
init
end
def
left
undo_manager
.
prepareWithInvocationTarget
(
self
)
.
right
@x
-=
1
end
def
right
undo_manager
.
prepareWithInvocationTarget
(
self
)
.
left
@x
+=
1
end
end
And now if we used the code, here is what it would look like:
>>
lara
=
Player
.
new
=>
<
Player
:
0x200267c80
@y
=
0
@x
=
0
>
>>
lara
.
undo_manager
.
canUndo
=>
false
# normal since we did not do anything yet
>>
lara
.
left
=>
-
1
>>
lara
.
x
# -1
=>
-
1
>>
lara
.
undo_manager
.
canUndo
=>
true
# now we can undo, so let's try
>>
lara
.
undo_manager
.
undo
# undo back to initial position
=>
#<NSUndoManager:0x200257560>
>>
lara
.
x
=>
0
>>
lara
.
undo_manager
.
canUndo
=>
false
# we can't do any more undoing
>>
lara
.
undo_manager
.
canRedo
=>
true
# however we can redo what was just undone
>>
lara
.
undo_manager
.
redo
# redo to before we called undo
=>
#<NSUndoManager:0x200257560>
>>
lara
.
x
=>
-
1
Foundation offers a convenient way to store a user’s
preferences via the NSUserDefaults
class. Preferences are saved in a shared database where
developers can save and load keyed settings using some primitive
objects. Here is a quick example that saves a token in the user’s
preferences. Keep on reading after the example to see how to save other
object types, such as arrays and hashes, and search for a preference
key:
framework
'Foundation'
def
set_api_token
(
token
)
NSUserDefaults
.
standardUserDefaults
[
'oreilly.api_token'
]
=
token
NSUserDefaults
.
standardUserDefaults
.
synchronize
# force sync
api_token
end
def
api_token
NSUserDefaults
.
standardUserDefaults
[
"oreilly.api_token"
]
end
if
api_token
.
nil?
puts
"The API token has not been set yet, please enter it now:"
cli_token
=
gets
set_api_token
(
cli_token
.
strip
)
puts
"API token set, thank you!"
else
puts
"Currently stored API token:
#{
api_token
}
"
end
After saving this code in a file called api_pref.rb, run it from the command line. The output will look like the following:
$
macruby
api_pref
.
rb
The
API
token
has
not
been
set
yet
,
please
enter
it
now
:
# typed my token: 42sosayweall42
API
token
set
,
thank
you!
Running it a second time will skip the token prompt:
$
macruby
api_pref
.
rb
Currently
stored
API
token
:
42
sosayweall42
Let’s jump to macirb and play with the preferences:
$
macirb
--
simple
-
prompt
>>
require
'api_pref.rb'
Currently
stored
API
token
:
42
sosayweall42
=>
true
>>
NSUserDefaults
.
standardUserDefaults
.
removeObjectForKey
(
'oreilly.api_token'
)
=>
#<NSUserDefaults:0x2002362a0>
>>
api_token
=>
nil
>>
set_api_token
'macruby is awesome'
=>
"macruby is awesome"
>>
api_token
=>
"macruby is awesome"
>>
NSUserDefaults
.
standardUserDefaults
.
dictionaryRepresentation
.
keys
.
grep
/oreilly/
=>
[
"oreilly.api_token"
]
>>
NSUserDefaults
.
standardUserDefaults
[
'oreilly.owned_books'
]
=
[
{
'topic'
=>
'macruby'
,
'isbn'
=>
'9781449380373'
}
]
>>
NSUserDefaults
.
standardUserDefaults
[
'oreilly.owned_books'
].
first
[
'isbn'
]
=>
"9781449380373"
Note
As shown here, we can delete preferences using NSUserDefaults.standardUserDefaults.removeObjectForKey
,
as well as access all the preferences using NSUserDefaults.standardUserDefaults.dictionaryRepresentation
,
which returns a Hash
.
Finally, we set a more
complex preference: an array containing a hash. However, the storage is
limited to objects supported by the property lists, that is, objects of
the following class families: NSData
, NSString
, NSNumber
, NSDate
, NSArray
, and NSDictionary
.
Get MacRuby: The Definitive Guide now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.