GDataObjCIntroduction
Introduction for Objective-C Developers
Introduction to Google Data APIs for Cocoa DevelopersGoogle Data APIs allow client software to access and manipulate data hosted by Google services. The Google Data APIs Objective-C Client Library is a Cocoa framework that enables developers for Mac OS X and iPhone to easily write native applications using Google Data APIs. The framework handles
RequirementsThe Google Data APIs Objective-C Client Library requires Mac OS X 10.4 due to its use of NSXMLDocument. Example ApplicationsThe Examples directory contains example applications showing typical interactions with Google services using the framework. The applications act as simple browsers for the structure of feed and entry classes for each service. The WindowController source files of the samples were written with typical Cocoa idioms to serve as quick introductions to use of the APIs. Adding Google Data APIs to a projectBuildingTheLibrary explains how to add the library to a Mac or iPhone application project. Google Data APIs BasicsServers respond to client GData requests with feeds that include lists of entries. For example, a request for all of a user's calendars would return a Calendar feed with a list of entries, where each entry represents one calendar. A request for all events in one calendar would return a Calendar Event feed with a list of entries, with each entry representing one of the user's scheduled events. Each feed and entry is composed of elements. Elements represent either standard Atom XML elements, or custom-defined GData elements. Feeds, entries, and elements are derived from GDataObject, the base class that implements XML parsing and generation. Google web application interactions are handled by service objects. A single transaction with a service is tracked with a service ticket. For example, here is how to use the Google Calendar service to retrieve a feed of calendar entries, where each entry describes one of the user's calendars. service = [[GDataServiceGoogleCalendar alloc] init]; [service setUserCredentialsWithUsername:username password:password]; NSURL *feedURL = [GDataServiceGoogleCalendar calendarFeedURLForUsername:username]; GDataServiceTicket *ticket; ticket = [service fetchFeedWithURL:feedURL delegate:self didFinishSelector:@selector(ticket:finishedWithFeed:error:)]; Service objects maintain cookies and track data modification dates to minimize server loads, so it's best to reuse a service object for sequences of server requests. The application may choose to retain the ticket following the fetch call so the user can cancel the service request. The ticket is valid so long as the application retains it. To cancel a fetch in progress, call [ticket cancelTicket]. Once the finished or failed callback has been called, the ticket is no longer useful and may be released. The delegate of the fetch is also retained until the fetch has stopped. Here is what the callback from a fetch of the calendar list might look like. This callback example just prints the title of the user's first calendar. - (void)ticket:(GDataServiceTicket *)ticket finishedWithFeed:(GDataFeedCalendar *)feed error:(NSError *)error { if (error == nil) { if ([[feed entries] count] > 0) { GDataEntryCalendar *firstCalendar = [[feed entries] objectAtIndex:0]; GDataTextConstruct *titleTextConstruct = [firstCalendar title]; NSString *title = [titleTextConstruct stringValue]; NSLog(@"first calendar's title: %@", title); } else { NSLog(@"the user has no calendars") } } else { NSLog(@"fetch error: %@", error); } } Service objects include a variety of methods for interacting with the service. Typically, the interactions include some or all of these activities:
Feeds and entries usually contain links to themselves or to other objects. The library provides convenience methods for retrieving individual links. For example, to retrieve the events for a user's calendar, use a Calendar service object to fetch from the Calendar's "alternate" link: GDataLink *link = [calendarEntry alternateLink]; if (link != nil) { [service fetchFeedWithURL:[link URL] delegate:self didFinishSelector:@selector(eventsTicket:finishedWithFeed:error:)]; } Typically, the alternate link points to an html or user-friendly representation of the data, though Google Calendar uses it as a link to the event feed for a calendar. Modifiable feeds may have a post link, which contains the URL for inserting new entries into the feed. Modifiable entries have an edit link, which is used to update or delete the entry. Entries that refer to non-XML data, such as photographs, may include an edit media link for modifying or deleting the media data. Both entries and feeds have self links, which self-referentially contain the URL of the XML for the entry or feed. The self link is useful for fetching a complete, current version of an entry prior to updating the entry on the server. A particularly important link in feeds is the next link; it is present when the feed contains only a partial set, or one page, of entries from the request. If the feed's -nextLink is non-nil, the client application must perform a new request using the "next" link URL to retrieve the next page of the feed, containing additional entries. Rather than make a new fetch for each "next" link, the library's service object can follow next links automatically, and return a feed whose entries are the full accumulated set of entries from fetching all pages of the feed (up to 25 pages.) This can be turned on in the service object with [service setServiceShouldFollowNextLinks:YES]. The fetch's ticket then applies to the sequence of http requests needed to obtain the entries from all pages in the feed, so canceling the ticket will cancel the sequence of requests. Note, however, that feeds spread over many pages may take a long time to be retrieved, as each "next" link will lead to a new http request. The server can be told to use a larger page size (that is, more entries in each page of the feed) by fetching a query for the feed with a maxResults parameter: GDataQueryCalendar *query = [GDataQueryCalendar calendarQueryWithFeedURL:feedURL]; [query setMaxResults:1000]; GDataServiceGoogleCalendar *service = [self calendarService]; GDataServiceTicket *ticket; ticket = [service fetchFeedWithQuery:query delegate:self didFinishSelector:@selector(ticket:finishedWithFeed:error:)]; Beginning with version 2 of the Google Data API core protocol, each fetched feed and entry has an ETag attribute. The ETag is just a server-generated hash string uniquely identifying the version of the entry data. Services may require that the ETag attribute be present when updating or deleting an entry or other file, such as a photo. This prevents the client application from accidentally modifying or deleting the wrong version of the data on the server. ETag strings are also useful to clients for determining if a feed, entry, or other file on the server has changed. If the underlying resource data has changed, the ETag is guaranteed to have changed as well. Service-specific query objects can generate URLs with parameters appropriate for restricting a feed's entries. For example, a query could request a feed of Calendar events between specific dates, or of database items of a specified category. Here is an example of a query to retrieve the first 5 events from a user's calendar: - (void)beginFetchingFiveEventsFromCalendar:(GDataEntryCalendar *)calendar { NSURL *feedURL = [[calendar alternateLink] URL]; GDataQueryCalendar* query = [GDataQueryCalendar calendarQueryWithFeedURL:feedURL]; [query setStartIndex:1]; [query setMaxResults:5]; GDataServiceGoogleCalendar *service = [self calendarService]; [service fetchFeedWithQuery:query delegate:self didFinishSelector:@selector(queryTicket:finishedWithEntries:error:)]; } Creating GDataObjects from scratchTypically GDataObjects are created by the framework from XML returned from a server, but occasionally it is useful to create one from scratch, such as when uploading a new entry. This snippet shows how to create a new event to add to a user's calendar: - (void)addAnEventToCalendar:(GDataEntryCalendar *)calendar { // make a new event GDataEntryCalendarEvent *newEvent = [GDataEntryCalendarEvent calendarEvent]; // set a title, description, and author [newEvent setTitleWithString:@"Meeting"]; [newEvent setSummaryWithString:@"Today's discussion"]; GDataPerson *authorPerson = [GDataPerson personWithName:@"Fred Flintstone" email:@"fred.flinstone@spurious.xxx.com"]; [newEvent addAuthor:authorPerson]; // start time now, end time in an hour NSDate *anHourFromNow = [NSDate dateWithTimeIntervalSinceNow:60*60]; GDataDateTime *startDateTime = [GDataDateTime dateTimeWithDate:[NSDate date] timeZone:[NSTimeZone systemTimeZone]]; GDataDateTime *endDateTime = [GDataDateTime dateTimeWithDate:anHourFromNow timeZone:[NSTimeZone systemTimeZone]]; // reminder 10 minutes before the event GDataReminder *reminder = [GDataReminder reminder]; [reminder setMinutes:@"10"]; GDataWhen *when = [GDataWhen whenWithStartTime:startDateTime endTime:endDateTime]; [when addReminders:reminder]; [newEvent addTime:when]; // add it to the user's calendar NSURL *feedURL = [[calendar alternateLink] URL]; GDataServiceGoogleCalendar *service = [self calendarService]; [service fetchEntryByInsertingEntry:newEvent forFeedURL:feedURL delegate:self didFinishSelector:@selector(addTicket:addedEntry:error:)]; } GData services always return to the callback the newest version of an entry that has been inserted or updated, so the methods are called "fetch" even for inserts and updates. When an entry is deleted, however, the callback is passed nil as the object parameter. Adding custom data to GDataObject instancesOften it is useful to add data locally to a GDataObject. For example, an entry used to represent a photo being uploaded would be more convenient if it also carried a path to the photo's file. Your application can add data to any instance of a GDataObject (such as entry and feed objects, as well as individual elements) in three ways. Each GDataObject has methods setUserData: and userData to set and retrieve a single NSObject. Adding a local path string to a photo entry with setUserData: would look like this: GDataEntryPhoto *newPhotoEntry = [GDataEntryPhoto photoEntry]; [newPhotoEntry setUserData:localPathString]; An application can set and retrieve multiple objects as named properties of any GDataObject instance with the methods setProperty:forKey: and propertyForKey:. This is useful when there is more than one bit of data to attach to the object: GDataEntryPhoto *newPhotoEntry = [GDataEntryPhoto photoEntry]; [newPhotoEntry setProperty:localPathString forKey:@"myPath"]; [newPhotoEntry setProperty:thumbnailImage forKey:@"myThumbnail"]; Property names beginning with an underscore are reserved by the library and should not be used by applications. Finally, applications may subclass GDataObjects to add fields and methods. To have your subclasses be instantiated in place of the standard object class during the parsing of XML following a fetch, call setServiceSurrogates:, as demonstrated here: NSDictionary *surrogates = [NSDictionary dictionaryWithObjectsAndKeys: [MyEntryPhoto class], [GDataEntryPhoto class], [MyEntryAlbum class], [GDataEntryPhotoAlbum class], nil]; service = [[GDataServiceGooglePhotos alloc] init]; [service setServiceSurrogates:surrogates]; These three techniques only add data to elements locally for the Objective-C code; the data will not be retained on the server. Some services support an extendedProperty element which can retain arbitrary data for users. Passing objects to fetch callbacksIt is also often useful to pass an object to a callback. To retain an object from a fetch call to its callback, use GDataServiceTicket's setUserData: or setProperty:forKey: methods. For example: GDataServiceTicket *ticket = [service fetchFeedWithURL:...]; [ticket setProperty:callbackData withKey:@"myCallbackData"]; The callback can then access the data: - (void)ticket:(GDataServiceTicket *)ticket finishedWithFeed:(GDataFeedBase *)feed error:(NSError *)error { id myCallbackData = [ticket propertyForKey:@"myCallbackData"]; ... } Batch requestsSome services support batch requests, allowing many insert, update, or delete operations to be performed with a single http request. Services which allow batch operations on a feed's entries will provide a batch link in the feed: NSURL *batchURL = [[feed batchLink] URL]; To execute a batch request, create an empty feed for the appropriate service, such as: GDataFeedContact *batchFeed = [GDataFeedContact contactFeed]; Then add entries to insert, update, and delete to the batch feed. For updates and deletes, add entries that were previously fetched rather than ones created from scratch, as the fetched entries will have additional elements (edit links or ETags) that the server may need to check that modifications are occurring on the expected entry versions. Using the first five entries from a fetched contacts feed in a batch feed might look like: NSRange entryRange = NSMakeRange(0, 5); NSArray *entries = [[contactFeed entries] subarrayWithRange:entryRange]; [batchFeed setEntriesWithEntries:entries]; If the same operation applies to all entries, add that operation to the batch feed instance: GDataBatchOperation *op; op = [GDataBatchOperation batchOperationWithType:kGDataBatchOperationDelete]; [batchFeed setBatchOperation:op]; If different entries require different operations, add operation elements to the individual entries of the batch feed. Optionally, each entry may be assigned a batch ID. A batch ID is an arbitrary string, created by your application, that is copied by the server into the entries of the batch results feed. This lets your code match the results to the entries of the original batch feed. The server merely copies the ID string; the string's contents are up to the application. static unsigned int staticID = 0; NSString *batchID = [NSString stringWithFormat:@"batchID_%u", ++staticID]; [entry setBatchIDWithString:batchID]; As with other kinds of fetches in the library, objects may be passed to the batch callbacks as properties or userData on the ticket: GDataServiceGoogleContact *service = [self contactService]; GDataServiceTicket *ticket; ticket = [service fetchFeedWithBatchFeed:batchFeed forBatchFeedURL:batchURL delegate:self didFinishSelector:@selector(batchDeleteTicket:finishedWithFeed:error:)]; [ticket setProperty:@"my data" forKey:kMyDataKey]; The callback for a batch operation receives a feed that includes entries as the response for each operation. Each result entry has a status code element, and may have the optional ID string. If batch processing was halted at an entry, the corresponding response entry will have a GDataBatchInterrupted element. - (void)batchTicket:(GDataServiceTicket *)ticket finishedWithFeed:(GDataFeedBase *)resultsFeed error:(NSError *)error { if (error == nil) { for (int idx = 0; idx < [resultsFeed count]; idx++) { GDataEntryContact *entry = [resultsFeed objectAtIndex:idx]; NSString *batchIDString = [[entry batchID] stringValue]; GDataBatchStatus *status = [entry batchStatus]; int statusCode = [[status code] intValue]; NSString *statusReason = [status reason]; GDataBatchInterrupted *interrupted = [entry batchInterrupted]; if (interrupted != nil) { // batch processing was interrupted, probably by invalid XML; // no further entries were processed, and no more result entries // are provided } } } } To ensure reasonable response times, a service may impose a limit on the number of entries in a single batch. See the service's documentation for specific limits on the size of a batch. The batch processing documentation has additional information on batch requests. Uploading filesSome services allow uploading of a file when inserting an entry into a feed. Uploading requires setting the upload data and MIME type in the entry. Some services also require a slug as the file name. This snippet shows the basic steps for uploading a spreadsheet document. GDataEntrySpreadsheetDoc *newEntry = [GDataEntrySpreadsheetDoc documentEntry]; NSString *path = @"/mySpreadsheet.xls"; NSData *data = [NSData dataWithContentsOfFile:path]; if (data) { NSString *fileName = [path lastPathComponent]; [newEntry setUploadSlug:filename]; [newEntry setUploadData:data]; [newEntry setUploadMIMEType:@"application/vnd.ms-excel"]; NSString *title = [[NSFileManager defaultManager] displayNameAtPath:path]; [newEntry setTitleWithString:title]; NSURL *postURL = [[docListFeed postLink] URL]; ticket = [service fetchEntryByInsertingEntry:newEntry forFeedURL:postURL delegate:self didFinishSelector:@selector(uploadTicket:finishedWithEntry:error:)]; } Upload progress monitoringWhen uploading large blocks of data, such as photos or videos, your application can request periodic callbacks to update a progress indicator. To receive the periodic callback, set an upload progress selector in the service, such as: SEL progressSel = @selector(ticket:hasDeliveredByteCount:ofTotalByteCount:); [service setServiceUploadProgressSelector:progressSel]; // then do the fetch // GDataServiceTicket *ticket = [service fetch...]; // If future tickets should not use the progress callback, // set the selector in the service back to nil [service setServiceUploadProgressSelector:nil]; The callback is a method with a signature matching this: - (void)ticket:(GDataServiceTicket *)ticket hasDeliveredByteCount:(unsigned long long)numberOfBytesRead ofTotalByteCount:(unsigned long long)dataLength { } Note: In library versions 1.8 and earlier, the progress callback's first argument is a pointer to a GDataProgressMonitorInputStream, and the ticket is available from the stream's -monitorSource method. Status 304 and service data cachingGData servers provide a "Last-Modified" header with their responses. The service object remembers the header, and provides it as an "If-Modified-Since" request the next time the application makes a request to the same URL. If the request would have the same response as it did previously, the server returns no data to the second request, just status 304, "Not modified". Your service delegate will see the "Not modified" response in its callback method. The application handles it like this: - (void)ticket:(GDataServiceTicket *)ticket finishedWithFeed:(GDataFeedBase *)feed error:(NSError *)error { if (error == nil) { // fetch succeeded } else { // fetch failed if ([error code] == kGDataHTTPFetcherStatusNotModified) { // status 304 // no change since previous request } else { // some unexpected error occurred } } } The service can optionally remember the dated responses in a cache and provide them to the application instead of calling the failure method. To enable the caching, the application should call [service setShouldCacheDatedData:YES]; The service will thereafter call the fetch callback methods with duplicates of the original response rather than with a status 304 error. You can call [service clearLastModifiedDates] to purge the cache, or [service setShouldCacheDatedData:NO] to purge and disable the future caching. Fetching during modal dialogsThe networking code in GDataService classes is based on NSURLConnection, and as in NSURLConnection, callbacks are deferred while a modal dialog is displayed. Under Mac OS X 10.5 or later, you can specify run loop modes to allow networking callbacks during modal dialogs: NSArray *modes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSModalPanelRunLoopMode, nil]; [service setRunLoopModes:modes]; Setting the run loop modes will have no effect for service objects on Mac OS X 10.4. Authentication errors and captchasAuthenticating the user with Google's services is handled mostly transparently by the framework. If your application sends the setUserCredentialsWithUsername:password: message to the service object, the service object will sign in prior to fetching the requested object. The most common error during authentication is an invalid username or password. Occasionally, the servers may also request that the user solve a captcha, a visual puzzle. It is optional for your application to handle captcha requests. One easy way to handle them is to just open Google's unlock captcha web page in the user's web browser. NSURL *captchaUnlockURL = [NSURL URLWithString:@"https://www.google.com/accounts/DisplayUnlockCaptcha"]; [[NSWorkspace sharedWorkspace] openURL:captchaUnlockURL]; For the best user experience in handling captcha requests, your application may download and display the captcha image to the user, and wait for the user to provide the answer. If the user offers an answer, put the captcha token and the user's answer into the service object with setCaptchaToken:captchaAnswer:, and retry the fetch. To handle captchas, the fetch callback method might look something like this: - (void)ticket:(GDataServiceTicket *)ticket finishedWithFeed:(GDataFeedBase *)feed error:(NSError *)error { if (error == nil) { // fetch succeeded } else { // fetch failed if ([error code] == kGDataBadAuthentication) { NSDictionary *userInfo = [error userInfo]; NSString *authError = [userInfo authenticationError]; if ([authError isEqual:kGDataServiceErrorCaptchaRequired]) { // URL for a captcha image (200x70 pixels) NSURL *captchaURL = [userInfo captchaURL]; NSString *captchaToken = [userInfo captchaToken]; // a synchronous read of the image is simple, as shown here, // but to be nice to users, you can use GDataHTTPFetcher for // an easy asynchronous fetch of the data instead NSData *imageData = [NSData dataWithContentsOfURL:captchaURL]; if (imageData) { NSImage *image = [[[NSImage alloc] initWithData:imageData] autorelease]; [self askUserToSolveCaptchaImage:image withToken:captchaToken]; // pass the token and user's captcha answer later to the service, like // [service setCaptchaToken:captchaToken captchaAnswer:theUserAnswer] // prior to retrying the fetch } } else { // invalid username or password } } else { // some other error authenticating or retrieving the GData object // or a 304 status indicating the data has not been modified since it // was previously fetched } } } Tip: to force a captcha request from the server for testing, provide an invalid e-mail address as the username several times in a row. Documentation on authentication for Google data APIs is available here. Automatic retry of failed fetchesGData service classes and the GDataHTTPFetcher class provide a mechanism for automatic retry of a few common network and server errors, with appropriate increasing delays between each attempt. You can turn on the automatic retry support for a GData service by calling [service setIsServiceRetryEnabled:YES]. The default errors retried are http status 408 (request timeout), 503 (service unavailable), and 504 (gateway timeout), and NSURLErrorTimedOut and NSURLErrorNetworkConnectionLost. You may specify a maximum retry interval other than the default of 10 minutes, and can provide an optional retry selector to customize the criteria for each retry attempt. Proxy AuthenticationIn corporate or institutional settings where a password-protected proxy is in use, a proxy error may show up in the failure callback. It would have constant error domain and code values, as shown here: - (void)fetchTicket:(GDataServiceTicket *)ticket finishedWithFeed:(GDataFeedBase *)feed error:(NSError *)error { if (error == nil) { // fetch succeeded } else { // fetch failed if ([error code] == kGDataHTTPFetcherErrorAuthenticationChallengeFailed && [[error domain] isEqual:kGDataHTTPFetcherErrorDomain]) { NSURLAuthenticationChallenge *challenge; challenge = [[error userInfo] objectForKey:kGDataHTTPFetcherErrorChallengeKey]; } } ... } If you want to handle such errors, use the challenge object to display a dialog showing information about the host and requesting account and password credentials for the proxy. The proxy credentials can then be passed to the ticket's authentication fetcher: ticket = [service fetchFeed...]; if (proxyAccountName && proxyPassword) { NSURLCredential *cred; cred = [NSURLCredential credentialWithUser:proxyAccountName password:proxyPassword persistence:NSURLCredentialPersistencePermanent]; [[ticket authFetcher] setProxyCredential:cred]; } Logging http server trafficDebugging GData transactions is often easier when you can browse the XML being sent back and forth over the network. To make this convenient, the framework can save copies of the server traffic, including http headers, to files in a local directory. Your application should call [GDataHTTPFetcher setIsLoggingEnabled:YES] to turn on logging. Normally, logs are written to the directory GDataHTTPDebugLogs in the current user's Desktop folder, though the path to another folder can be specified with the +setLoggingDirectory: method. In the iPhone simulator, the default logs location is the user's home directory. On the iPhone device, the default location is the application's documents folder on the device. To view the most recently saved logs, use a web browser to open the symlink named My_App_Name_http_log_newest.html (for whatever your application's name is) in the logging directory. Note that Camino and Firefox display XML in a more useful fashion than does Safari. Tip: providing a convenient way for your users to enable logging is often helpful in diagnosing problems when using the API. Service IntrospectionFor each feed, services provide an XML document that describes capabilities of the feed, typically including the types of media data that is accepted for upload. Applications can dynamically adjust how they use a feed by retrieving the feed's service document. For instance, given the URL to a feed for a user's photo album, the service document for the feed can be fetched like this: GDataQueryGooglePhotos *introspectQuery; introspectQuery = [GDataQueryGooglePhotos photoQueryWithFeedURL:albumFeedURL]; [introspectQuery setResultFormat:kGDataQueryResultServiceDocument]; GDataServiceTicket *ticket; ticket = [photosService fetchFeedWithQuery:introspectQuery delegate:self didFinishSelector:@selector(introspectTicket:finishedWithServiceDocument:error)]; The service document includes a workspace that has a collection describing the feed. Feeds whose entries refer to other feeds will have one collection for each entry's feed. For an album feed, the MIME types that can be uploaded would be obtained from the fetched service document this way: - (void)introspectTicket:(GDataServiceTicket *)ticket finishedWithServiceDocument:(GDataAtomServiceDocument *)serviceDoc error:(NSError *)error { if (error == nil) { GDataAtomCollection *collection = [[serviceDoc primaryWorkspace] primaryCollection]; NSArray *theMIMETypes = [collection serviceAcceptStrings]; ... } } Performance and Memory ImprovementsOnce your application successfully works with Google Data APIs, review the tips on the Performance Tuning page. Questions and CommentsIf you have any questions or comments about the library or this documentation, please join the discussion group. |
The example applications are really helpful. I hope you guys will be releasing some iPhone example apps soon.
Would be nice to see how to handle a spreadsheet feed and get rows, cells etc.
question about contacts. When I create a contact with contactEntryWithTitle:@"myTitle"? and update google with it, I get an error that either Title or Organization must be set (also happens if I set title with string setter). If I set organization, the contact is updated fine. When I look at GMail to see the contacts, the one that updates successfully has what I set as title ("myTitle") in the first line of the contact, but not as title. This seems like a bug. Is it? What field is really being set when I set title? How do I actually set the title so that somebody with no organization can be created?
Great stuff. I'm planning on releasing an iPhone game that entirely uses a Google Spreadsheet as a cloud service. (http://www.SquareMasters.com) It's set to go live on Dec 15th, so we'll see if it handles the load. :) I have about 27,000 users.
My only question: is there a way to abort a fetch in progress, in the Objective-C lib? I know I could simply ignore the result (or error) once it arrives, but I'd rather abandon the fetch altogether since network access depletes an iPhone battery quite rapidly. I have the service, and the ticket, but I don't see an obvious way to abort the fetch.
nerd@SquareMasters?.com
Anyone in this group interested in extending it to cover the Google Apps suite, e.g. to allow provisioning and care of the sites, membership functions, etc.?
Also interested in finding out if there is an equivalent Mac-oriented function to the "goofs" Python project that allows mounting the Google space as a fuse file system.