Chapter 10. Geolocation
The example in this chapter shows how to write an application
that uses the user’s geographical location and a location web service. To do
that, we are going to use the CoreLocation framework, the
gowalla.com web API (offering location information
based on coordinates), and a TableView
instance.
As before, we start by creating a MacRuby application using Xcode. Start Xcode and choose the MacRuby template. In the template settings, set the Product Name to “AroundMe” and disable the Document-Based and Core Data options.
User Interface
In this example, the UI will be very simple. We are going to
work with just an NSTableView instance and
a bunch of columns (NSTableColumn).
Edit your .xib file and resize
the main window to be 640 pixels by 440 pixels (use the Size Inspector in
the Utilities panel). Now drag and drop a Table View from the Object
Library inside the Window’s view. Using the grid guides, scale the table
to use most of the window’s real estate.
At this point, make sure the AroundMe scheme is selected and click Run. The app should look like Figure 10.1, “Blank Table View”.
We are going to use the Gowalla API to find the locations (called spots in Gowalla’s jargon) around our users. You can see the type of information the API provides us by going to the following page: http://gowalla.com/api/explorer#/spots?lat=30.2697&lng=-97.7494&radius=50.
We can’t easily display all the spot’s information. Instead, we are going to filter the data and display only the following:
The spot’s category
The spot’s name
The spot’s description
The spot’s distance
The number of items in the spot
The number of users who visited this spot
The number of checkins in the spot
Let’s add some columns to our table. The easiest way to do that is to copy/paste an existing table column. The end result should look like Figure 10.2, “Seven table columns added”.
Now, let’s name each column’s header by setting the header cell title via the Attributes Inspector (Figure 10.3, “Setting the column’s header title”).
Our first column will display an image, so we need to change the type of cell it uses. Drag and drop an Image Cell inside the first column so the Text Field cell gets replaced by its Image counterpart (Figure 10.4, “Image Cell being used in the first column”).
Finally, one by one, edit the table columns and use the Attributes Inspector to make sure the columns are not editable.
Note
You can select all the columns at once and set the flag the way you want it.
That’s enough for the UI for now. Let’s move on to writing some code.
Table View
Before we get to call the web API and fetch the information,
we need to wire the table view. We are going to call our table spot_table and create an outlet for it in our
AppDelegate.rb file:
class AppDelegate attr_accessor :window attr_accessor :spot_table ... end
Going back to the UI editor, right-click the App Delegate object,
choose the spot_table outlet, and
connect it to the table view.
Warning
Connect the outlet to the table view, not the scroll view. The table view is inside the scroll view.
We can now initialize the table view delegation as follows:
def applicationDidFinishLaunching(a_notification) spot_table.dataSource = self spot_table.doubleAction = "preview:" end
Setting the TableView instance’s
dataSource to self means that we are
expected to implement the table’s methods in our AppDelegate class (which is what self refers to in this context). We are also
setting the doubleAction selector,
representing the action to dispatch when a row is double-clicked. Of
course, that means that we will need to implement this action in a
minute.
For the table to work properly, we need to implement a few methods.
But before we get there, we need to define an object that will hold the
references to spots. Our table will use this object to fetch and sort its
content. Let’s call this object @spots,
make an attribute accessor for it, and initialize it as an empty array
when the application finishes loading. We are going to use this object as
the table data source. For more information about how to use a table data
source, please read the Apple developer page, About
Table Views in Mac OS X Applications.
This is how our AppDelegate class
should look:
class AppDelegate
attr_accessor :window
attr_accessor :spots, :spot_table
def applicationDidFinishLaunching(a_notification)
@spots = []
spot_table.dataSource = self
spot_table.doubleAction = "preview:"
end
def windowWillClose(sender); exit; end
endNote
I skipped the windowWillClose
delegate explanation here because I covered it many times previously.
Remember that you need to set the window’s delegate to the AppDelegate class.
Now that we defined our data source object, we can define some table delegate methods based on this reference object. One of these methods indicates to the table view the number of records in the source:
def numberOfRowsInTableView(view) spots.size end
We also need to implement some sort of overloaded method. In Objective-C, a single method name can be defined with different method signatures. MacRuby supports the same approach when a hash is used in the method signature. We are going to implement the following methods:
def tableView(view, objectValueForTableColumn:column, row:index) end def tableView(view, sortDescriptorsDidChange:descriptors) end
These are two alternative signatures defined by the TableView API. The first method retrieves the
value for a given column, whereas the second defines sorting in the table.
In both, the second argument is a hash object (Ruby 1.9 style), but the
keys are different between the two method signatures.
When the first method is dispatched, it is passed the column object and row index to retrieve the data to display. Now we have two issues: we don’t have any data to look at yet, and we don’t have a way to look up a record’s value with a column instance as a key. To make things easier, we are going to edit our columns and set their editors with the matching JSON key we are getting from the web API call. Once we implement the web API fetching, we will wrap each spot in an object that will respond to the key names. We can also directly look up the values in the JSON object, but that won’t allow us to customize and massage the data easily, since the JSON data structure is somewhat limited.
In the UI editor, select each column and set the identifier as follows:
Category → image
Name → name
Description → description
Distance → radius_meters
Items → items_count
Users → users
Checkins → checkins
The implementation of the first method is as follows:
def tableView(view, objectValueForTableColumn:column, row:index)
spot = spots[index]
id = column.identifier
if id == 'image'
# NSImageCell instance, we need to feed it an NSImage
@cell_images ||= {}
@cell_images[spot.image] ||= NSImage.alloc.initWithData(
NSURL.URLWithString(spot.image).resourceDataUsingCache(true)
)
else
spot.send(id.to_sym)
end
endThe first thing we are doing in this method is fetching the spot
based on its index in the data source. We then extract the column
identifier and check whether it matches the image string. If it does, we allocate a
new NSImage instance based on the image
URL that was fetched from the web API. To improve performance, we are
memoizing all the NSImage instances we create and making sure to use the built-in cache.
Finally, for all other columns, we are dispatching the identifier’s method
using Ruby’s #send method, which takes,
as its argument, the method’s name as a symbol (#to_sym converts a string into a symbol).
We’ll keep the second method very simple for now. Instead of doing anything here, we will just reload the data for the moment:
def tableView(view, sortDescriptorsDidChange:descriptors) spot_table.reloadData end
Core Location
Before we can call Gowalla’s API, we still need to get the
user’s location. For that, we are going to use CoreLocation, a framework available in OS X that
allows the user to share his geographical location with a given
program.
The CLLocationManager class,
available from CoreLocation (see http://developer.apple.com/library/mac/#documentation/CoreLocation/Reference/CoreLocation_Framework/_index.html)
framework, does all the work, but it’s a bit clumsy. So let’s write a thin
wrapper to keep our delegate class simpler. Add a new Ruby file called
location_manager.rb and paste the
following code in the file:
framework 'CoreLocation'
# CLLocationManager wrapper
class LocationManager
def initialize(&block)
@loc = CLLocationManager.alloc.init
@loc.delegate = self
@callback = block
end
def start
@loc.startUpdatingLocation
end
def stop
@loc.stopUpdatingLocation
end
# Dispatch the CLLocationManager callback to the Ruby callback
def locationManager(manager, didUpdateToLocation: new_location,
fromLocation: old_location)
@callback.call(new_location, self)
end
endThis wrapper doesn’t do much, but it will make our code cleaner. We
basically define a new class called LocationManager, which expects a block at
initiation. The block is kept in memory and is called when new location
information is retrieved. I also added two simple instance methods,
#start and #stop, which delegate to the underlying CLLocationManager instance. There is documentation
for CoreLocation at the Apple developer site.
This nice wrapper allows us to print a location like this:
location_manager = LocationManager.new do |new_location, manager|
puts "location: #{new_location.description}"
manager.stop
end
location_manager.startAs a matter of fact, you can put the wrapper and the previous code in a file and get your location that way. You will need to add the following line at the bottom of the file to keep the run loop running:
NSRunLoop.currentRunLoop.runUntilDate(NSDate.distantFuture)
Execute the file using MacRuby, and you should be presented with a dialog box like the one shown in Figure 10.5, “The location-sharing security dialog box”.
And something like this should be printed out on your terminal:
location: <+32.67000000, 117.24000000> +/- 181.00m
(speed -1.00 mps / course -1.00) @ 2011-05-15 22:46:24 -0700Back to our app. Let’s use our wrapper in our applicationDidFinishLaunching callback
method:
def applicationDidFinishLaunching(notification)
@spots = []
location_manager = LocationManager.new do |new_location, manager|
puts "location: #{new_location.description}"
gowalla = gowalla_req(new_location)
@spots = gowalla['spots'].map{|spot| GowallaSpot.new(spot)}
@spots.sort!{|a,b| a.radius_meters <=> b.radius_meters}
spot_table.reloadData
manager.stop
end
spot_table.dataSource = self
spot_table.doubleAction = "preview:"
location_manager.start
endWithin the block, we are making a Gowalla call, iterating through
the spots, and storing them in an array of GowallaSpot instances. The spots are then sorted
based on their distances. The table view reloads its data and the location
manager is stopped.
The location manager obviously cannot be started from within the callback block, so we need to start it once everything is set up. That’s what we do before closing the method definition.
Web API
Right now, this code won’t work. We still need to implement
the GowallaSpot class and the #gowalla_req method. I personally like to write
high-level code before I implement the underlying methods; I feel it helps
me design my APIs better.
The GowallaSpot class is very
straightforward. Create a new file with the following code:
class GowallaSpot
attr_reader :name
attr_reader :url
attr_reader :description
attr_reader :highlights_url
attr_reader :lat, :lgn
attr_reader :image
attr_reader :items_count
attr_reader :radius_meters
attr_reader :users, :checkins
def initialize(spot_hash)
@name = spot_hash['name']
@url = NSURL.URLWithString(
"http://gowalla.com#{spot_hash['url']}")
@description = spot_hash['description']
@highlights_url = 'http://gowalla.com' \
+ spot_hash['highlights_url']
@lat = spot_hash['lat']
@lgn = spot_hash['lgn']
@image = spot_hash['_image_url_50']
@items_count = spot_hash['items_count']
@radius_meters = spot_hash['radius_meters']
@users = spot_hash['users_count']
@checkins = spot_hash['checkins_count']
end
endThe #gowalla_req method is
defined inside the AppDelegate class
like this:
def gowalla_req(location)
http = Net::HTTP.start('api.gowalla.com')
req = Net::HTTP::Get.new("/spots?lat=#{location.coordinate.latitude}&" + \
"lng=#{location.coordinate.longitude}&radius=500")
req.add_field("Accept", "application/json")
req.add_field("X-Gowalla-API-Key", GOWALLA_API_KEY)
response = http.request(req)
JSON.parse(response.body)
endThis method uses Ruby’s
net/http library to make a call to the Gowalla web API and the
built-in JSON library. To use these libraries, we need to
require them at the top of our file using require
"net/http" and require
"json". You need to get your own API key from Gowalla and store it in the
GOWALLA_API_KEY constant.
If you run the app, everything should load properly and you will see the various spots around you. It’s nice, but we are missing one last thing: the preview action. Remember that earlier we set a selector to trigger when a user double-clicks a row. Let’s implement this preview action:
def preview(sender)
if spot_table.selectedRow >= 0
url = spots[spot_table.selectedRow].url
NSWorkspace.sharedWorkspace.openURL(url)
end
endNSWorkspace.sharedWorkspace.openURL (see http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ApplicationKit/Classes/NSWorkspace_Class/Reference/Reference.html)
opens a given URL in the user’s default browser. We made sure that when
this method is called, the table has a valid selected row and we retrieve
the URL by accessing the spots data source using the row index.
If you run the application now, you should be able to view all the Gowalla spots around you and visit their web pages by double-clicking a row (Figure 10.6, “The end result”).











Add a comment



Add a comment