Chapter 9. Address Book Example
The first full example in this book shows how to write an application around Twitter and the Address Book. The goal here is to import our Twitter contacts into our address book and learn new tricks as we go along.
Let’s begin by creating a MacRuby application using Xcode. For that, start Xcode and choose the MacRuby template. In the template settings, set the Product Name to TwitterContactImporter and use “clear” or “deselect” or “disable” the Document-Based and Core Data options.
User Interface
I personally find it easier to start by working on the UI,
and then adding the required code. Click on the .xib
file to edit the interface, then select the Window object, as shown in
Figure 9.1, “Initial UI”.
We are going to add a button that will automatically import our Twitter contacts. Once the contacts are imported, we will display the list of our contacts and let the user search for a specific contact.
To start, we need to use an image to put on our button. Head to the
Twitter logos and
icons site and download the logo of your choice. Save the image as
twitter_logo.png somewhere on your
hard drive. Once the image is saved, drag and drop it in Xcode into the
Resources folder. An option screen like the one shown
in Figure 9.2, “Import image option screen” will open.
Warning
Make sure to select the destination box.
Now, drag a Square Button from the Object Library to the window view, as shown in Figure 9.3, “Addition of a square button”.
Let’s add our Twitter logo to our button. To do that, select the
button and select the Attributes inspector. Open the drop-down menu for
the Image attribute and choose twitter_logo. Now resize the button to make it
bigger. Resizing the button is easy: just use the handles on the sides of
the button. Once you are satisfied, make sure the TwitterContactImporter
scheme is selected and click Run to compile your application and preview
your UI (Figure 9.4, “Final view of the running application”). We can also use
the document simulator (in the Editor menu), but since the MacRuby source
code doesn’t have to be compiled, I think it’s just as easy and more
reliable to run the real app.
If you try to close the window, you will notice that the application
is still in the dock. Let’s add a line of code to exit the application
once the main window is closed. Open AppDelegate.rb and add the following code at
the bottom of the AppDelegate class
(i.e., before the last “end”):
def windowWillClose(sender); exit; end
Before testing this code, we need to do one more thing. We need to
tell our window to use our AppDelegate
so our callback will work as expected. Edit the .xib
file, right-click on the Window instance, and drag the window delegate
outlet to the App Delegate object. At this point, the window will delegate
everything to our AppDelegate instance,
which is what we want, since that’s where we defined the windowWillClose delegate
method. Depending on the application you write, you might want to write a
window-specific delegate.
Run the application one more time, and notice that this time, when closing the window, the application exits.
Address Book
To use the AddressBook framework, we need first to load it. Edit
rb_main.rb and on the line loading
the Cocoa framework, add the following code:
framework 'AddressBook'
Earlier, in the section called “Method Missing”, we examined how to
wrap the AddressBook Cocoa API to make
it easier to work with. I wrote a bunch of basic wrappers for the AddressBook API. Let’s add them to our
project.
Right-click the TwitterContactImporter folder
in the Project navigator and choose New Group. Name the folder Wrappers.
Download the wrapper files for this chapter from the book repository and
drag them in the Wrappers folder (the wrapper files
are named ab*_ and you will also see a file adding
some helper methods to Ruby). When the option screen displays,
choose to copy the items into the destination group. The files reopen some
AddressBook classes and add some
convenient methods. The files will be automatically loaded by the
rb_main.rb file.
Next, we need to load the user’s address book and make sure the user
marked a record as her own. Let’s start by creating an accessor for the
user’s address book. We want to do that when the application finishes
launching. Edit AppDelegte.rb, add a
new attribute reader called address_reader, and set its value inside the
applicationDidFinishLaunching method as
follows:
class AppDelegate
attr_accessor :window
attr_reader :address_book
def applicationDidFinishLaunching(a_notification)
@address_book = ABAddressBook.sharedAddressBook
end
def windowWillClose(sender); exit; end
endLet’s also add a method to alert the user if something goes wrong, such as, for instance, if she didn’t set a contact as her own:
def alert(title='Alert', message='Something went wrong: ')
NSAlert.alertWithMessageText(title,
defaultButton: 'OK',
alternateButton: nil,
otherButton: nil,
informativeTextWithFormat: message).runModal
endAnd now let’s verify that the user marked an account as hers. If not, let’s display a warning:
def check_me
unless address_book.me
alert "Address Book Problem",
"You need to open the Address Book and mark a contact as yours. Choose a contact,
click on Card, and select 'Make this my card'."
end
endAnd call our newly created method from inside applicationDidFinishLaunching:
def applicationDidFinishLaunching(a_notification) @address_book = ABAddressBook.sharedAddressBook check_me end
This is a good first step, but we also need to check that we set our Twitter nickname in the address book. Without this nickname representing the user’s Twitter account, we can’t find her contacts. Notice that only public Twitter accounts are supported.
Add the following code at the bottom of the check_me method:
if address_book.me.nickname.nil? || address_book.me.nickname == '' alert "Address Book Problem", "You need to set your Twitter account as your nickname in your contact card." end
Great, so now we have now created a warning that will inform the user if her contact card doesn’t contain the information we need. But we also need to create a new contact group to store all the Twitter contacts and keep them somewhat isolated:
def create_twitter_group
twitter_group = address_book.groups.find{|g| g.name == 'Twitter'}
if twitter_group
twitter_group
else
group = ABGroup.alloc.init
group.name = 'Twitter'
puts "adding Twitter group"
address_book << group
address_book.save
twitter_group
end
endWe are going to use this method in a minute, when we call the web API.
Web API Call
Now that the Twitter information is ready, we can write the code that will be called to fetch the Twitter contacts when the user clicks our Twitter button.
In Cocoa terms, such a method is called an action. We will connect our button’s
performClick event to a new method,
passing as its argument the instance that calls the method. Here is the
action code:
def import_twitter_contacts(sender)
me = address_book.me
twitter_group = create_twitter_group
puts "Importing from Twitter"
Thread.new{
fetch_paginated_friends(me.nickname) do |friends|
puts "Importing a batch of friends"
add_twitter_friends(friends, twitter_group)
end
address_book.save
puts "Import done"
}
endThis code calls a method that we
haven’t implemented yet, named fetch_paginated_friends. And for each
batch of retrieved friends, we add them to the newly created
group, using a new method called add_twitter_friends.
Finally, when all the friends have been imported, the address book is
saved.
The reason the code is wrapped in a new thread is so we don’t block the main loop. Because our application is very simple, using the main thread wouldn’t have been a big deal. Indeed, a user can’t do anything else in the application while we’re loading contacts, so making him wait until the import is done wouldn’t be too bad. However, this is a bad practice and, as you can see, running the code in a separate thread is really straightforward.
Let’s connect our button to this action. To do that, select the
MainMenu.xib file, right-click on the
App Delegate icon, and drag the circle attached to import_twitter_contacts
(in the Received Action submenu) to the big button we added.
Now let’s implement the paginated fetching, using the Twitter API:
def fetch_paginated_friends(account, &block)
cursor = '-1'
until cursor == 0
url_string =
"http://twitter.com/statuses/friends/#{account}.json?cursor=#{cursor}"
url = NSURL.alloc.initWithString(url_string)
json_friends = NSMutableString.alloc.initWithContentsOfURL(url,
encoding:NSUTF8StringEncoding,
error:nil)
friend_results = JSON.parse(json_friends)
block.call(friend_results['users'])
cursor = friend_results['next_cursor']
end
endThe code isn’t complicated, but it mixes some Cocoa API calls with some advanced Ruby idioms. The first thing you might notice is that the second method argument starts with an ampersand and looks funny. It’s Ruby’s way of saying that this method expects to be called with a block. The method can’t be called without passing a block.
The Twitter API uses the principle of a cursor to paginate through the results. The first page starts at cursor –1 and each result set contains information about the previous or next cursor. When there aren’t any pages to iterate through, the cursor is set to 0. Because of this, we can write our code so it will keep calling the Twitter API until a response defines the next cursor as 0. The data coming back from the API is a string in the JSON format. To parse this data, we first need to require the JSON library at the top of the file, as follows:
require 'json'
That’s the library used to parse the body of the API response. We then extract a subset of this data and send it to our block, like this:
block.call(friend_results['users'])
This calls the passed block and passes it our data subset as a
parameter. Look at how we called this method and used the block. See that
the block’s parameter is called friends? Our implementation executes the passed
block and replaces “friends” with the data subset so that it can be
manipulated from within the block:
fetch_paginated_friends(me.nickname) do |friends| # do something with a subset of the friends end
For our code to work, we also need to implement the add_twitter_friends
method that is called from within the block:
def add_twitter_friends(friends, group)
friends.each do |friend|
# avoid duplicates
if friend['screen_name']
query = ABPerson.searchElementForProperty(KABNicknameProperty,
label:nil, key:nil, value:friend['screen_name'],
comparison:KABEqual)
results = address_book.recordsMatchingSearchElement(query)
contact = results.first || ABPerson.alloc.initWithAddressBook(address_book)
else
contact = ABPerson.alloc.initWithAddressBook(address_book)
end
first_name, last_name = friend['name'].split(' ')
contact.firstName = first_name if first_name
contact.lastName = last_name if last_name
contact.nickname = friend['screen_name']
urls = {'Twitter' => "http://twitter.com/#{friend['screen_name']}"}
urls[KABHomePageLabel] = friend['url'] unless friend['url'].blank?
contact.URLs = urls.to_ab_multivalue
contact.imageData = NSData.dataWithContentsOfURL(friend['profile_image_url'].\
to_nsurl)
notes = []
notes << friend['description'] unless friend['description'].blank?
notes << "Location: #{friend['location']}" unless friend['location'].blank?
contact.note = notes.join("\n")
group << contact
end
endEven though this method is a bit long, what it does is very simple. For each friend in the passed array, it checks whether a matching record already exists in the address book. If so, the method updates the record based on the information returned by Twitter, or creates a new record. We even download the profile image and assign it as the contact’s image. Once the method is done parsing the data, the updated/created contact is added to the group. Voilà!
Cleaning Up: Better Management of Widgets
The application we’ve thrown together is great, but we have a few issues to fix and features to add. First, a user might click multiple times on the button, and that would run our fetching code in multiple parallel threads. We obviously don’t want that. The other thing is that the user isn’t notified that the contacts are being fetched. Let’s fix that!
The first thing to do is give our code access to our big Twitter button. By now, you should know what we need to do:
Add a button attribute accessor in our class so Xcode sees it as an outlet.
In the Interface Builder, right-click the App Delegate icon and assign the button outlet to the big Twitter button.
In this section, instead of using the Interface Builder tool provided by Xcode, we are going to set the rest of the UI programmatically. This is mostly meant as an exercise and to show you that it is not that hard after all.
Now that we have access to our button, we want to hide the button after it was clicked and show a progress indicator. To do that, let’s create a method and dispatch it from the button action:
def make_user_wait
button.hidden = true
if @spinner
@spinner.hidden = false
else
x = window.frame.size.width/2 - 32/2
y = window.frame.size.height/2 - 32/2
@spinner = NSProgressIndicator.alloc.initWithFrame([x,y,32,32])
@spinner.style = NSProgressIndicatorSpinningStyle
window.contentView.addSubview(@spinner)
end
@spinner.startAnimation(nil)
endThis code is a bit more complicated than it should be, but it shows how to display a view item in relation to another one. The first thing we do is hide the Twitter button Then, if the spinner already exists, we show it, otherwise we create an instance that we locate exactly at the middle of the window. To do that, we calculate the x and y positions by inspecting the window frame size and locating the indicator in the center, based on its size in pixels (32×32). We also need to start the spinning animation.
The only problem is that we tell the users to wait, but then don’t
show them anything. Thankfully, Cocoa has a widget that lets you integrate
a contact browser in your app. Let’s add it to our sample and display the
contact browser once all the contacts are imported. Our import_twitter_contacts
method will look like this:
def import_twitter_contacts(sender)
make_user_wait
me = address_book.me
twitter_group = create_twitter_group
puts "Importing from Twitter"
Thread.new{
fetch_paginated_friends(me.nickname) do |friends|
puts "Importing a batch of friends"
add_twitter_friends(friends, twitter_group)
end
address_book.save
puts "Import done"
display_contacts
}
endAnd display_contacts looks like
this:
def display_contacts @spinner.stopAnimation(nil) @spinner.hidden = true frame = [0,0, window.frame.size.width, window.frame.size.height - 120] @picker = ABPeoplePickerView.alloc.initWithFrame(frame) window.contentView.addSubview(@picker) end
Again, the code is straightforward. We stop the spinner and hide it.
We then create an instance of ABPeoplePickerView that will be slightly shorter
than the window’s height. Once the instance is created, we add it to our
window’s content view.
The Extra Mile: Displaying More Information Through Notifications
What we have done already is really cool, but it would be
even nicer to display some of the contact’s details while browsing them.
We can do this by adding more UI elements and by observing the interaction
with the ABPeoplePickerView.
This time, instead of creating UI elements and placing them programmatically on the window, let’s set our elements but hide them until we need them. Before we do that, let’s add some outlets for all of the UI elements and, while we are at it, add an attribute reader for the picker:
class AppDelegate attr_accessor :window, :button attr_reader :address_book, :picker attr_accessor :contact_name, :contact_image, :contact_twitter #... end
Next, add an ImageWell instance
of 100x100 on the top left of your UI. Edit the Image view’s settings and
change the drop-down to make the view scale proportionally up or down.
Then, to the right of the image, add two labels. Before you hide them all,
right-click the app delegate icon and connect your three outlets to your
new UI items. Now, go to each of them and set their drawing attributes to
hidden.
Now we just need to write the code displaying the hidden UI items, setting up the notification center and its callback method:
def display_contacts
@spinner.stopAnimation(nil)
@spinner.hidden = true
frame = [0,0, window.frame.size.width, window.frame.size.height - 120]
@picker = ABPeoplePickerView.alloc.initWithFrame(frame)
center = NSNotificationCenter.defaultCenter
center.addObserver(self,
selector: "record_changed:",
name: ABPeoplePickerNameSelectionDidChangeNotification,
object:picker)
picker.allowsMultipleSelection = false
picker.addProperty KABNicknameProperty
contact_image.hidden = false
contact_name.hidden = false
contact_twitter.hidden = false
window.contentView.addSubview(picker)
endIn the section called “Notification Centers”, we covered the notification center and observers. The principle is simple: when the user changes the selected contact, the callback selector is called. The following code implements the callback method, which simply finds the selected record and sets the UI elements with the record’s data:
def record_changed(notification)
selected = picker.selectedRecords || []
if selected.empty?
contact_image.image = nil
contact_name.stringValue = ""
contact_twitter.stringValue = ""
else
person = selected.first
contact_image.image = NSImage.alloc.initWithData(person.imageData)
contact_name.stringValue = "#{person.firstName} #{person.lastName}"
contact_twitter.stringValue = person.nickname || ''
end
endOur app is now finished. After running the app and playing a bit with it, you can open your AddressBook.app and see all your Twitter contacts being imported and their details becoming available, as shown in Figure 9.5, “My Twitter contacts imported and previewed”.










Add a comment



Add a comment