Skip to content

Latest commit

 

History

History
1025 lines (796 loc) · 39.5 KB

File metadata and controls

1025 lines (796 loc) · 39.5 KB

Using the Google APIs Client Library for Objective-C for REST (GTLR)

Google APIs allow client software to access and manipulate data hosted by Google services.

The Google APIs Objective-C Client Library for REST APIs is a Objective-C framework that enables developers for iOS, macOS, tvOS, and watchOS to easily write native applications using Google's JSON REST APIs. The framework handles

  • JSON parsing and generation
  • Networking
  • File uploading and downloading
  • Batch requests and responses
  • Service-specific protocols and query generation
  • Testing without network activity
  • HTTP logging

Preparing to Use the Library

Example Applications

The Examples directory contains example applications showing typical interactions with Google services using the framework. The applications act as simple browsers for the data 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.

The example applications run on macOS, but the library does not provide any user interface support apart from authentication, so use of the library APIs is the same for Mac and iOS applications.

In order to use the example applications that require authentication, you can follow the same steps as for the GTMAppAuth Examples The client secret can be blank/nil.

Google APIs which do not require authentication may require an API key, also obtained from the Developer Console.

Adding the Library to a Project

Integration via CocoaPods

If you are building from CocoaPods, just use the pod provided, GoogleAPIClientForREST.

The Core subspec includes the common parts of the library. There is also a subspec for each service API provided, such as Calendar and Drive. Your project can just depend on one or more service subspecs, and the core library files will be built as well.

See the podspec file for the subspec names.

For example, if you needed the Drive apis, you'd just need to add:

pod 'GoogleAPIClientForREST/Drive'

To your Podfile and run pod install.

If you are generating code for your own APIs, then add pod 'GoogleAPIClientForREST/Core' to get the supporting runtime and then manually add the generated sources to your Xcode project.

Integration via Swift Package Manager (SwiftPM)

Refer to the Xcode docs for how to add SwiftPM based dependences to the Xcode UI or via your Package.swift file.

The GoogleAPIClientForRESTCore product includes the common parts of the library, and then there are specific products for each service API provide. Your project can use one or multiple services.

See the Package.swift for the different product names.

For example, if you needed the Drive apis, you just need to depend on the GoogleAPIClientForREST_Drive product.

If you are generating code for your own APIs, then add GoogleAPIClientForRESTCore to get the supporting runtime and then manually add the generated sources to your Xcode project.

#imports and @imports

Since CocoaPods and SwiftPM use different models for how things are built, the module names for @import directives will be specific to each packaging system.

However, if consuming this library via Objective-C, all the packages export their headers as GoogleAPIClientForREST/HEADER.h, so you can always #import them as a framework import and that will work with either packaging system, i.e. - #import <GoogleAPIClientForREST/GTLRService.h and #import <GoogleAPIClientForREST/GTLRYouTube.h.

Dependencies

The Google APIs Library for Objective-C for REST uses the prefix GTLR.

The library uses one other Google libraries with a GTM prefix. It provides http handling (gtm-session-fetcher).

Authentication and Authorization

Authentication is the confirmation of a user's identity using her username, password, and possibly other data, such as captcha answers or 2-step codes provided via mobile phone. Authentication is required for access to non-public data.

Authorization is the use of access tokens to allow specific requests. Applications pass the OAuth 2 authorization object to a service class's setAuthorizer: method to authorize queries.

Google APIs rely on OAuth 2 for user sign-in. The GTMSessionFetcher provides an Objective-C protocol to provide this, any library that supports that protocol will work.

Note: Neither packaging system used here includes a dependency on any authentication support, if you plan on using an api that also needs authentication, you'll either want to use GTMAppAuth by depending on GTMAppAuth also, or you can use something else like the Google Sign-In SDK; see their site for full information this.

Note: The Oauth2 subspec in the pod is NOT needed for authentication, that is for talking to that service directly. You just need something like GTMAppAuth or the Google Sign-In SDK.

API Quotas

For each API used by your application, enable the API and check its default quota in the Services section of the Developer Console.

Applications which make frequent API requests or have many users may need more than the default limit. The specific API documentation or the service's control in the API Console will explain how to request a larger quota.

Note: Be sure to estimate your application's total queries per day for all users and request an appropriate quota before releasing your application to a large audience. An inadequate quota may lead to errors on API requests after your application ships.

Generated Interfaces

The Google APIs Client Library for Objective-C includes generated service, query and data classes. These are Objective-C files constructed by processing the output of the Google APIs Discovery Service. The generated classes are derived from GTLRService, GTLRQuery, and GTLRObject.

Basics

Objects and Queries

Servers respond to client API requests with an object. The object is typically either an individual item, or a collection with an items property that accesses an NSArray of items.

For example, a search for files on Google Drive would return a GTLRDrive_FileList collection, where the items of the collection are the GTLRDrive_File items found by the search.

The individual items are derived from GTLRObject. The collection is derived from GTLRCollectionObject, which is a GTLRObject that also provides for indexed access to items via subscripts. GTLRCollectionObject also supports for loops over the collection items by implementing the NSFastEnumeration protocol, as shown in the next code snippet.

Each request to the server is a query. Server interactions in the library are handled by a service object. A single transaction with a service is tracked with a service ticket.

For example, here is how to use the API library to fetch a list of items in a public YouTube playlist.

- (void)fetchPublicPlaylistWithID:(NSString *)playlistID {
  // Create a service for executing queries. For best performance, reuse
  // the same service instance throughout the app.
  //
  // Some of the service's properties may be set on a per-query basis
  // via the query's executionParameters property.
  GTLRYouTubeService *service = [[GTLRYouTubeService alloc] init];

  // Services which do not require user authentication may need an API key
  // from the Google Developers Console
  service.APIKey = @"put your API key here";

  // APIs which retrieve a collection of items may need to fetch
  // multiple pages. The service can optionally make multiple requests
  // to fetch all pages. The page size can be set in most APIs with the
  // query parameter maxResults.
  service.shouldFetchNextPages = YES;

  // The library can retry common networking errors. The retry criteria
  // may be customized by setting the service's retryBlock property.
  service.retryEnabled = YES;

  // Each API method has a unique class.  The required properties
  // of the API method are the parameters of the constructor.
  // Optional properties of the API method are properties of the
  // class.
  
  // The YouTube API requires a "part" parameter for each query.
  // The playlist ID an an optional property of the method.
  GTLRYouTubeQuery_PlaylistItemsList *query =
    [GTLRYouTubeQuery_PlaylistItemsList queryWithPart:@"snippet"];
  query.playlistId = playlistID;

  // A ticket is returned to let the app monitor or cancel query execution.
  GTLRServiceTicket *ticket =
    [service executeQuery:query
        completionHandler:^(GTLRServiceTicket *callbackTicket,
                            GTLRYouTube_PlaylistItemListResponse *playlistItemList,
                            NSError *callbackError) {
    // This callback block is run when the fetch completes.
    if (callbackError != nil) {
      NSLog(@"Fetch failed: %@", callbackError);
    } else {
      // The error is nil, so the fetch succeeded.
      //
      // GTLRYouTube_PlaylistItemListResponse derives from
      // GTLRCollectionObject, so it supports iteration of
      // items and subscript access to items.
      for (GTLRYouTube_PlaylistItem *item in playlistItemList) {
        // Print the name of each playlist item.
        NSLog(@"%@", item.snippet.title);
      }
    }
  }];
}

Unseen by the application, the server is returning a JSON tree in response to queries. Each GTLRObject, such as GTLRYouTube_PlaylistItemListResponse in the example above, is just an Objective-C wrapper for a tree of JSON data. The GTLRObject allows the JSON data to be treated like a first-class Objective-C object, using normal Objective-C property notation. This use of properties is visible in the code snippet above, where each product item result name is accessible as item.snippet.title.

GTLRObject's support for Objective-C properties allows compile-time syntax checking and enables Xcode's autocompletion for each object. The header files for each object class clearly define the fields of each object. For example, the YouTube playlist item object interface looks in part like this:

@interface GTLRYouTube_PlaylistItem : GTLRObject

@property(strong) GTLRYouTube_PlaylistItemContentDetails *contentDetails;
@property(copy) NSString *ETag;
@property(copy) NSString *identifier;
@property(copy) NSString *kind;
@property(strong) GTLRYouTube_PlaylistItemSnippet *snippet;
@property(strong) GTLRYouTube_PlaylistItemStatus *status;

@end

Each object property returns either a standard Objective-C type (NSString, NSNumber, NSArray), other GTLRObjects, or GTLRDateTime.

When your application gets a property from a GTLRObject, the library converts the property name to the JSON key string to get or set the result in the underlying JSON tree. Subtrees of the JSON are returned wrapped in a new GTLRObjects. To reduce memory overhead, the GTLRObjects are not created for inner trees of the JSON until they are needed by the application.

Normally, applications will use GTLRObject properties and so will not need to access the plain JSON tree, but it is available for each GTLRObject as a dictionary with the property JSON.

Queries may also be executed with a delegate and selector for the callback:

GTLRServiceTicket *ticket = [service executeQuery:query
                                         delegate:self
                                 finishedSelector:@selector(serviceTicket:finishedWithObject:error:)];

The delegate is retained until the query has completed, or until the ticket has been canceled.

Services and Tickets

Service objects maintain cookies and track other persistent data across queries. Typically, an application will create and retain a single instance of a service object to use for executing all queries.

The service object makes a copy of each query for execution, so changes to a query object made after calling executeQuery: will not affect the request.

Query execution by the service is inherently asynchronous. There is no need to use a dispatch or operation queue to run queries on other threads. Generally, it's best to execute queries on the main application thread. Any number of queries may be executed either concurrently or sequentially, subject to rate limits shown in the Developer Console. Additional information about threading support is listed below in the section on performance optimizations.

A new ticket is created each time a query is executed by the service. When a ticket is created, many of the ticket properties, such as retry settings and surrogates (both described below), are initialized from the service's properties and from the query’s executionProperties property.

The application may choose to retain the ticket after a query starts executing, allowing the user to cancel the service request.

Once either a query's callback has been invoked or the ticket is canceled, the ticket is no longer useful and may be released. To cancel a query in progress, call [ticket cancelTicket].

The query being executed by a ticket is available as ticket.originalQuery. For queries that fetch multiple result pages, the query for the page currently being fetched is available as ticket.executingQuery. The GTMSessionFetcher object used to execute the query is accessible as ticket.objectFetcher.

Result Pages

A query may return an object with only a subset of the results in the items array. The object is considered one of several pages. When a result object includes a nextPageToken token, then the query can be executed again with the token provided as the pageToken property of the new query, fetching the next set of results.

// Check if more pages are available for this collection object.
if (object.nextPageToken) {
  // Manually make a query to fetch the next page of results by reusing a copy
  // of the previous query that was made before the query was executed.
  GTLRTasksQuery *nextQuery = copyOfPreviousQuery;
  nextQuery.pageToken = object.nextPageToken;
  GTLRServiceTicket *nextTicket = [service executeQuery:nextQuery ...
}

For APIs that provide a nextPageToken property, the library can automatically fetch all pages, and return an object whose items array includes the items of all pages (up to 25 pages). This can be turned on by setting the shouldFetchNextPages property of a service or of the query’s execution parameters:

// Turn on automatic page fetches
service.shouldFetchNextPages = YES;

Note, however, that results spread over many pages may take a long time to be retrieved, as each page fetch will lead to a new http request. The server can be told to use a larger page size (that is, more items in each page returned) by fetching a query for the feed with a maxResults value:

// Specify a large page size to reduce the need to fetch additional result pages
GTLRTasksQuery *query = [GTLRTasksQuery_TasklistsList query];
query.maxResults = 1000;
ticket = [service executeQuery:query ...

Ideally, maxResults will be large enough that, for typical user data, all results will be returned in a single page.

For queries that search public data, with potentially a very large number of items resulting, shouldFetchNextPages should not be enabled for the service or the ticket. Additional pages of large data sets can be fetched manually.

Query Operations

Query objects typically implement these basic operations:

  • list - fetch a list of items
  • insert - add an item to a list
  • update - replace an entire item
  • patch - replace fields of an item
  • delete - remove an item

There may be custom query operations as well, such as for uploading and downloading files. To find the Objective-C query class for a documented API operation, search for the method name shown in the documentation for the API. It is listed before each method name in the query class interface, as shown here for the method named “youtube.playlistItems.list”:

/**
 *  Returns a collection of playlist items that match the API request
 *  parameters.
 *
 *  Method: youtube.playlistItems.list
 */
@interface GTLRYouTubeQuery_PlaylistItemsList : GTLRYouTubeQuery

/**
 *  Fetches a GTLRYouTube_PlaylistItemListResponse
 *
 *  @param part The part parameter specifies a comma-separated list of one or
 *    more playlistItem resource properties that the API response will include.
 *
 *  @returns GTLRYouTube_PlaylistItemListResponse
 */
+ (instancetype)queryWithPart:(NSString *)part;

The comments for each method also specify the type of object passed to the callback if the query succeeds. In this example, the callback object class is GTLRYouTube_PlaylistItemListResponse.

Creating GTLRObjects from Scratch

Typically, GTLRObjects are created by the library from JSON returned from a server, but occasionally it is useful to create one from scratch, such as when inserting a new item or patching an existing item. The +object method creates an empty instance of a GTLRObject.

This snippet shows how to create a new folder for the user's Google Drive account:

- (void)createAFolder {
  GTLRDriveService *service = self.driveService;

  GTLRDrive_File *folder = [GTLRDrive_File object];
  folder.name = @"New Folder Name"
  folder.mimeType = @"application/vnd.google-apps.folder";

  GTLRDriveQuery_FilesCreate *query =
    [GTLRDriveQuery_FilesCreate queryWithObject:folderObj
                               uploadParameters:nil];
  [service executeQuery:query
      completionHandler:^(GTLRServiceTicket *callbackTicket,
                          GTLRDrive_File *folderItem,
                          NSError *callbackError) {
    // Callback
    if (callbackError == nil) {
      // Succeeded.
    }
  }];
}

Uploading Files

Queries that can upload files will take a GTLRUploadParameters object. The upload parameters object requires a MIME type describing the file data, and either an NSData with the file's contents, or an NSURL for reading from the local file.

Here is an example of uploading a file to the user’s Google Drive account.

- (void)uploadFileURL:(NSURL *)fileToUploadURL {
  GTLRDriveService *service = self.driveService;

  GTLRUploadParameters *uploadParameters =
      [GTLRUploadParameters uploadParametersWithFileURL:fileToUploadURL
                                               MIMEType:@"text/plain"];
                                               
  GTLRDrive_File *newFile = [GTLRDrive_File object];
  newFile.name = path.lastPathComponent;

  GTLRDriveQuery_FilesCreate *query =
    [GTLRDriveQuery_FilesCreate queryWithObject:newFile
                               uploadParameters:uploadParameters];

  GTLRServiceTicket *uploadTicket =
    [service executeQuery:query
        completionHandler:^(GTLRServiceTicket *callbackTicket,
                            GTLRDrive_File *uploadedFile,
                            NSError *callbackError) {
    if (callbackError == nil) {
      // Succeeded
    }
  }];
}

The library by default uses Google's resumable (chunked) upload protocol for transferring the file to the server. The chunked upload requires one or more additional server requests. When uploading a large file (perhaps over a megabyte), the chunk transfer can be done with a background NSURLSession by settings the property uploadParameters.useBackgroundSession.

Conversely, small uploads (perhaps under 500K) can be done even more quickly with a single server request by setting the property

uploadParameters.shouldUploadWithSingleRequest = YES;

Use the libary’s http logging feature to see and understand what server requests are used for uploading.

Progress Monitoring

The application can supply a block to be called for displaying progress during uploads.

query.executionParameters.uploadProgressBlock =
  ^(GTLRServiceTicket *ticket,
    unsigned long long numberOfBytesRead,
    unsigned long long dataLength) {
  [uploadProgressIndicator setDoubleValue:(double)numberOfBytesRead];
  [uploadProgressIndicator setMaxValue:(double)dataLength];
};

Pause and Resume

Uploads in progress can be paused and resumed.

if (ticket.uploadPaused) {
  [ticket resumeUpload];
} else {
  [ticket pauseUpload];
}

Pause and resume are not supported for uploads sent with the shouldUploadWithSingleRequest property set.

Uploads can be cancelled with the ticket's cancelTicket method.

Downloading Files

The library supports two ways to download files. Files that are expected to be small may be downloaded with a library query. Larger files should instead be downloaded with a GTMSessionFetcher, a class that supports convenient http uploading and downloading.

Downloading with a Query

APIs that support direct download of media files will pass the downloaded file to the query callback as a GTLRDataObject instance, as shown here:

GTLRQuery *query = [GTLRDriveQuery_FilesGet queryForMediaWithFileId:fileID];
[service executeQuery:query
    completionHandler:^(GTLRServiceTicket *callbackTicket,
                        GTLRDataObject *dataObject,
                        NSError *callbackError) {
  if (callbackError == nil) {
    // The file downloaded successfully; its data is available as dataObject.data
  }
}];

Downloading with a GTMSessionFetcher

A GTMSessionFetcher can download any NSURLRequest. The service method requestForQuery: will convert a library query into an NSURLRequest.

Download of any individual user’s data from Google services requires that the request be authorized. A fetcher created from the GTLRService object’s fetcher service will authorize the download request.

Here is an example of an authorized file download using a fetcher:

GTLRQuery *query = [GTLRDriveQuery_FilesGet queryForMediaWithFileId:fileID];
NSURLRequest *downloadRequest = [service requestForQuery:query];
GTMSessionFetcher *fetcher =
  [service.fetcherService fetcherWithRequest:downloadRequest];

[fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *fetchError) {
  if (fetchError == nil) {
    // Download succeeded.
  }
}];

The class GTMSessionFetcher has many more features for monitoring and controlling http downloads and uploads, and can download directly to a file instead of to an NSData. See the fetcher header file for details.

Performance and Memory Optimizations

Partial Responses

Applications can avoid fetching unneeded data by setting a query's fields property. Google APIs support a syntax similar to XPath for selecting fields to be included in the response.

For example, to request only the IDs and author email addresses for each item in the collection, use this request:

query.fields = @"items(id,author/email,kind),kind,nextPageToken";

Note: The above is just an example, the exact value you need will depend on the api/method you are using and what fields you want included in the reply. GTLRObject provides a fieldsDescription method which returns a string describing all of the set fields in an object. The string may be useful as a starting point when making a query to request specific fields. You'll want to use it on the root object from a query to get the full hierarchy.

The library may use the kind fields of responses to choose the proper object classes, so requests for partial responses should always include the kind fields for both items and collections, as shown above. Otherwise, the library may be forced to incorrectly instantiate the GTLRObject base class for the response.

Queries for collections should also accept the nextPageToken field, as that field is returned when an additional result page should be fetched. The service API documentation should specify if a nextPageToken field is returned with collection responses.

Refer to the documentation for details on the fields parameter.

Partial Updates

An update query will replace an entire item of a collection. Typically, it is more useful to replace only one or a few fields of an item. A patch query accomplishes that: it replaces only the fields specified by the patch object.

Here is how to use a partial update to change only the name of a calendar:

GTLRCalendar_Calendar *patchObject = [GTLRCalendar_Calendar object];
patchObject.summary = newCalendarName;

GTLRCalendarQuery_CalendarsPatch *query =
    [GTLRCalendarQuery_CalendarsPatch queryWithObject:patchObject
                                           calendarId:calendarID];
[service executeQuery:query ...

An explicit null value indicates that a field should be deleted. For example, removing just the location from a calendar looks like this:

GTLRCalendar_Calendar *patchObject = [GTLRCalendar_Calendar object];
patchObject.location = [GTLRObject nullValue];

GTLRCalendarQuery_CalendarsPatch *query =
    [GTLRCalendarQuery_CalendarsPatch queryWithObject:patchObject
                                           calendarId:calendarID];
[service executeQuery:query ...

An array in a patch object field always replaces the entire array for the object on the server, as described in the documentation for partial updates.

GTLRObject includes a helpful method patchObjectFromOriginal: for making a patch object that contains just the changes from a previous version of that object.

Batch Operations

Several unrelated queries may be executed together in a batch. Batch execution is faster than is executing queries individually.

There are two ways to obtain the results of batch queries: the unified completion handler callback for the batch query execution, with results for all queries, and individual completion blocks for each query.

Typically, it is easier to use individual query completion blocks for unrelated methods (such as requesting a variety of different attributes of a single item), and the unified completion handler for batches of related methods (such as requesting the same attributes of an array of one class of item).

Both types of callbacks may be used. The individual query completion blocks are always called before the completion handler for the batch query execution. Individual query completion blocks are optional, and may be omitted.

Individual Query Completion Blocks

A completion block can be specified for each query, as shown here:

GTLRCalendarQuery_EventsList *eventsQuery =
    [GTLRCalendarQuery_EventsList queryWithCalendarId:calendarID];
eventsQuery.completionBlock = ^(GTLRServiceTicket *callbackTicket,
                                GTLRCalendar_Events *events,
                                NSError *callbackError) {
  If (callbackError == nil) {
    // This query succeeded.
  }
};

GTLRBatchQuery *batch = [GTLRBatchQuery batchQuery];
[batch addQuery:eventsQuery];

Errors passed to the query's completion block will have an underlying GTLRErrorObject when execution succeeded but the server returned an error for this specific query:

GTLRErrorObject *errorObj = [GTLRErrorObject underlyingObjectForError:error];
if (errorObj) {
    // The server returned this error for this specific query.
  } else {
    // This error occurred because the batch execution failed.
}

Unified Batch Completion Handler

The unified result of executing a batch query is a GTLRBatchResult object with two dictionaries, one for the results of successful queries, and one for the error objects returned by unsuccessful queries.

Note that in addition to the dictionary of error results, there is also an NSError passed to the completion handler which indicates if the execution did not succeed.

GTLRBatchQuery *batchQuery = [GTLRBatchQuery batchQuery];
[batchQuery addQuery:query1];
[batchQuery addQuery:query2];

[service executeQuery:batchQuery
    completionHandler:^(GTLRServiceTicket *callbackTicket,
                        GTLRBatchResult *batchResult,
                        NSError *callbackError) {
    if (callbackError == nil) {
      // Execute succeeded: step through the query successes
      // and failures in the result.
      NSDictionary *successes = batchResult.successes;
      for (NSString *requestID in successes) {
        GTLRObject *result = [successes objectForKey:requestID];
      }

      NSDictionary *failures = batchResults.failures;
      for (NSString *requestID in failures) {
        GTLRErrorObject *errorObj = [failures objectForKey:requestID];
      }
    } else {
      // Here, callbackError is non-nil so the execute failed: 
      // no success or failure results were obtained from the server.
    }
  }];

Each query object is created with a unique requestID, though your application may set a custom requestID string for the query prior to execution. The requestID string must be non-empty, and all queries in a batch must have unique requestIDs.

Threading

Typically, applications will execute queries on the main thread, and the query completion handler will be called back on the main thread.

To have callbacks performed on a different queue than the main queue, specify a dispatch queue for the service’s callbackQueue property.

Convenience Features

Converting a Query to an NSURLRequest

A service object can convert a GTLRQuery to a plain NSMutableURLRequest with the service method requestForQuery:. That may be useful for downloading media files, or for performing an API request without using the service’s executeQuery: method.

Requests that retrieve user data require authorization. The GTMSessionFetcher authorizer property handles authorization for requests fetched with the fetcher. Alternatively, a GTMAppAuth authorizer object can authorize a plain NSMutableURLRequest.

Query Execution Notifications

Apps that want to monitor performance or network activity may observe the notifications kGTLRServiceTicketStartedNotification and kGTLRServiceTicketStoppedNotification.

Saving Objects (Serialization)

Archiving

GTLRObject conforms to NSSecureCoding so may be serialized with NSKeyedArchiver and deserialized with NSKeyedUnarchiver. The object’s userProperties dictionary is not encoded into the archive.

Other Ways to Serialize

A GTLRObject has an underlying NSDictionary accessible through its JSON property. The dictionary may be saved as a property list using NSPropertyListSerialization or as JSON using NSJSONSerialization. Here is an example of serializing with a property list:

// Saving to disk.
NSError *serializeError;
NSData *data = [NSPropertyListSerialization dataWithPropertyList:events.JSON
                    format:NSPropertyListBinaryFormat_v1_0
                   options:0
                     error:&serializeError];
if (data) {
  NSError *writeError;
  BOOL didWrite = [data writeToURL:fileURL
                           options:0
                             error:&writeError];
}

An object's JSON can also be expressed as a string with the JSONString method.

The object may later be recreated with the class methods +objectWithJSON: or +objectWithJSON:objectClassResolver:. The JSON dictionary should be constructed with mutable containers:

NSData *data = [NSData dataWithContentsOfURL:fileURL
                                     options:0
                                       error:&error];
if (data) {
  NSMutableDictionary *dict =
    [NSPropertyListSerialization propertyListWithData:data
                                              options:NSPropertyListMutableContainers
                                               format:NULL
                                                error:&error];
  if (dict) {
    GTLRCalendar_Events *eventsList =
      [GTLRCalendar_Events objectWithJSON:dict];
    // or
    GTLRCalendar_Events *eventsList2 =
      [GTLRCalendar_Events objectWithJSON:dict
                      objectClassResolver:calendarService.objectClassResolver];
  }
}

The version taking an objectClassResolver will do a better job of finding the correct object classes to use based on the kind properties in the JSON.

Adding Custom Data

Often it is useful to add data locally to a GTLRObject. For example, an entry used to represent a file being uploaded would be more convenient if it also carried a path to the file on the local disk.

Your application can add data to any instance of a GTLRObject in two ways. These techniques only add data to objects locally for the Objective-C code; the data will not be retained on the server, nor serialized by NSKeyedArchiver.

User Properties

An application can set and retrieve a dictionary with an object’s userProperties property, such as this:

GTLRDrive_File *file = [GTLRDrive_File object];
file.userProperties = @{ @”LocalFileURL” : fileLocalURL };

Subclassing GTLRObjects

Alternatively, applications may subclass GTLRObjects to add properties and methods. To have your subclasses be instantiated in place of the standard object class during the parsing of JSON as part of query execution, set the objectClassResolver property of the service:

GTLRDriveService *service = [[GTLRDriveService alloc] init];
NSDictionary *surrogates = @{
  [GTLRDrive_File class] : [MyFile class],
  [GTLRDrive_FileList class] : [MyFileList class]
};
NSDictionary *serviceKindMap = [[service class] kindStringToClassMap];
GTLRObjectClassResolver *updatedResolver =
  [GTLRObjectClassResolver resolverWithKindMap:serviceKindMap
                                    surrogates:surrogates];
service.objectClassResolver = updatedResolver;

The surrogates may also be set for a single query using the query’s executionParameters property:

GTLRDriveQuery_FilesList *query = [GTLRDriveQuery_FilesList query];
query.executionProperties.objectClassResolver = updatedResolver;

Passing Data to Query Callbacks

It is often useful to pass data to a callback method, particularly when using selector callbacks rather than blocks.

Each ticket has a property ticketProperties which is initialized to the service’s serviceProperties. An app may set the serviceProperties or ticketProperties to provide access to application-specific data:

query.executionParameters.ticketProperties = @{ @"file source", localFileURL };
ticket = [service executeQuery:query
                   delegate:self
          didFinishSelector:@selector(serviceTicket:finishedWithObject:error:)];

The callback can then access the data:

- (void)serviceTicket:(GTLRServiceTicket *)callbackTicket
   finishedWithObject:(Test_GTLRDrive_File *)object
                error:(NSError *)callbackError {
  if (callbackError == nil) {
    NSURL *localFileURL = callbackTicket.ticketProperties[@"file source"];
  }
}

Automatic Retry

GTRL service classes and the GTMSessionFetcher 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 GTLR service by setting the retryEnabled property.

// Turn on automatic retry of some common error results
service.retryEnabled = YES;

The default errors retried are http status 408 (request timeout), 502 (gateway failure), 503 (service unavailable), and 504 (gateway timeout), NSURLErrorNetworkConnectionLost, and NSURLErrorTimedOut. You may specify a maximum retry interval other than the default of 1 minute, and can provide an optional retryBlock for the service or for a single query to customize the criteria for each retry attempt.

Testing

For unit tests, queries can be executed without any network activity.

When testing, set the testBlock property of a service object or a query’s execution parameters. The testBlock should call its response parameter with an object or error, like this:

query.executionParameters.testBlock = ^(GTLRServiceTicket *ticket,
                                        GTLRQueryTestResponse testResponse) {
  // The query is available from the ticket.
  GTLRQuery *testQuery = ticket.originalQuery;

  // The testBlock can create a GTLRObject or GTLRBatchResult, or an NSError.
  //
  // Here, we will make a GTLRObject as the test response.
  GTLRDrive_File *fileObj = [GTLRDrive_File object];
  fileObj.name = @"My Fake File";

  NSError *testError = nil;

  testResponse(fileObj, testError);
};

As a convenient alternative to creating a test block, the test code can just create a service object with +[GTLRService mockServiceWithFakedObject:fakedError:] and pass the desired result object or error. That creates a service class instance with an appropriate test block.

When a service or query has a testBlock, the block will be called instead of the library performing the normal network activity. Since the executeQuery: invocation is asynchronous, the unit test should use either [GTRLService waitForTicket:timeout:] or [XCTestCase waitForExpectationsWithTimeout:] to wait for the completion handler to be called.

Logging HTTP Server Traffic

Debugging query execution is often easier when you can browse the JSON and headers 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

[GTMSessionFetcher setLoggingEnabled:YES]

to turn on logging. The project building the fetcher class must also include GTMSessionFetcherLogging.h and .m.

Normally, logs are written to the directory GTMSessionDebugLogs in the logs directory. The logs directory is in the current user's Desktop folder for Mac applications. 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.

When http logging begins on iOS, the logs folder path is written to the console, like

GTMSessionFetcher logging to "/Users/username/Library/Developer/CoreSimulator/Devices/3B2F01FE-9D64-4C20/data/Containers/Data/Application/FB8E3483-23022D135FB4/GTMHTTPDebugLogs"

The path (including the quotes) can be pasted into a terminal window to open the logs folder:

open "/Users/username/Library/Developer/.../GTMHTTPDebugLogs"

The path to the logs folder can be specified with the +setLoggingDirectory: method.

To view the most recently saved logs, use a web browser to open the symlink named MyAppName_log_newest.html (for whatever your application's name is) in the logging directory.

For each executed query, when logging is enabled, the http log is also available as the property ticket.objectFetcher.log

iOS Note: Logging support is stripped out in non-DEBUG builds by default. This can be overridden by explicitly setting STRIP_GTM_FETCH_LOGGING=0 for the project.

Tip: Providing a convenient way for your users to enable logging is often helpful in diagnosing problems when using the API.

Questions and Comments

If you have any questions or comments about the library or this documentation, please join the discussion group.