iOS Objective-C wrapper para Google Docs

¿Alguien ha integrado google docs en su aplicación para iOS? Después de haber pasado por el código de ejemplo, la API para Google Docs es mucho más complicada de lo que esperaba, y los ejemplos son todos MacOS. Sí, hay soporte de iOS, pero hay una clara falta de código de ejemplo sobre cómo usarlo, y la documentation es un tanto falta.

Encontré una class de interfaz en la web, pero se basó en una versión antigua y en desuso de la API de Google Docs, y no se comstack con XCode 4.2.

Lo que busco es una interfaz relativamente simple que permita:

  1. Inicio de session / salida desde una count de google docs.
  2. Obtener una list de documentos dentro de esa count (opcionalmente de un tipo específico), posiblemente con la capacidad de navegar por una estructura de carpetas.
  3. Ser capaz de download un documento específico al almacenamiento local.
  4. Ser capaz de upload un documento específico a google docs.

Empecé a escribir una interfaz de este tipo, pero hasta ahora está mucho más involucrado de lo que había permitido. Si alguien tiene alguna sugerencia, o muestras a las que puedan dirigirme, realmente lo agradecería.

Mi preference es que el envoltorio sea neutral al sistema operativo; lo que significa que quiero poder usar la misma interfaz tanto en MacOS como en iOS. Una vez más, esto es lo que he comenzado a escribir, pero no puedo evitar sentir que debo redevise la rueda aquí.

Gracias

    De acuerdo, entonces, en ausencia de respuestas de los demás, mordí la bala y escribí un envoltorio yo mismo.

    Ahora tengo un contenedor simple que funciona tanto para Mac OS como para iOS que simplifica enormemente la interfaz con Google Docs.

    A continuación se muestra todo el código de la interfaz real. Debo señalar que esta class actúa como un singleton, y debe personalizarlo ligeramente para cada proyecto actualizando las líneas:

    #define GOOGLE_DATA_CLIENT_ID @"<client id>.apps.googleusercontent.com" #define GOOGLE_DATA_SECRET @"<google data secret>" #define GOOGLE_DATA_USERNAME @"googleDocsUsername" #define GOOGLE_DATA_PASSWORD @"googleDocsPassword" 

    con los valores apropiados que obtienes de Google.

    También señalo que la class almacena passwords a través de NSUserDefaults, y usa una class de utilidad separada para hacerlo de forma cifrada. En lugar de obstruir esta respuesta aquí con todo ese código adicional, he creado un repository en bitbucket en:

    https://bitbucket.org/pkclsoft/gdatainterface

    que contiene un proyecto XCode completo que construye dos objectives, uno para Mac OS y otro para iOS. Los he usado tanto en una aplicación que está en la tienda de aplicaciones ahora con excelentes resultados. Puede ser que mientras este proyecto se construye para mí, tienes que modificarlo para tus propósitos. El proyecto contiene un set completo del SDK de Google que se comstack y ejecuta con mi código. Lo incluí para tratar de networkingucir el riesgo de incompatibilidades con las versiones más recientes del SDK para cualquiera que lo capturara.

    Aquí está la especificación de la interfaz, tal como está actualmente:

     // // GDataInterface.h // GDataInterface // // Some of the code in this class is from the original GData sample code, but it has been // enhanced somewhat and made to work on both iOS and MacOS transparently. // // Created by Peter Easdown on 19/12/11. // Copyright (c) 2011 PKCLsoft. All rights reserved. // #import <Foundation/Foundation.h> #if TARGET_OS_IPHONE #import "GDataDocs.h" #import <UIKit/UIKit.h> #else #import "GData/GData.h" #endif @interface GDataInterfaceTypes // This handler is used by methods that have no explicit result. The boolean value indicates // the success or failure of the the methods action. // typedef void (^CompletionHandler)(BOOL successful); // This handler is called to update a progress indicator as a file is uploaded. // typedef void (^UploadProgressHandler)(double min, double max, double value); // This handler is called to update a progress indicator as a file is downloaded. // typedef void (^DownloadProgressHandler)(double min, double max, double value); @end @interface GDataInterface : NSObject { #if TARGET_OS_IPHONE // Needed so that when authenticating under iOS, we can push the google authentication // view, and later pop it. // UIViewController *rootController_; #endif GDataFeedDocList *mDocListFeed; GDataServiceTicket *mDocListFetchTicket; NSError *mDocListFetchError; GDataFeedDocRevision *mRevisionFeed; GDataServiceTicket *mRevisionFetchTicket; NSError *mRevisionFetchError; GDataEntryDocListMetadata *mMetadataEntry; GDataServiceTicket *mUploadTicket; id uploadWindow; CompletionHandler uploadCompletionHandler; NSString *username_; NSString *password_; } // This handler is used when a list of documents has been requested. The results parameter // will be nil if the request failed. If successful, then it will contain an array of // GDataEntryDocBase objects. // typedef void (^RetrievalCompletionHandler)(GDataFeedDocList* results, BOOL successful); // This handler is used when a document has been downloaded. If something prevented the // download from succeeding, then error parameter will be non-nil. // typedef void (^DocumentDownloadCompletionHandler)(NSData* fileContents, BOOL successful); // Initializer that provides the username and password. // - (id) initWithUsername:(NSString*)username andPassword:(NSString*)password; // Returns the shanetworking instance of the class. There will only ever be a single instance // of this class. // + (GDataInterface*) shanetworkingInstance; // Returns YES if currently signed in. // - (BOOL) isSignedIn; // Signs in or out depending on current state, and executes the options completion handler // block. The window parameter is used to specify the root viewController object used when // displaying login windows via GData, or error dialogs. // - (void) signInOrOutWithCompletionHandler:(CompletionHandler)handler forWindow:(id)window; // Will retrieve a list of documents using the cached connection, and call the specified // handler block, providing the list of documents, and a success/fail indication. // - (void) retrieveDocumentListWithCompletionHandler:(RetrievalCompletionHandler)handler; // Will download the file at the specified URL. This is not Google Docs specific and will work // for any URL. Be careful not to try and retrieve large files and the result is stonetworking // in memory. // - (void) downloadURL:(NSURL*)url withProgressHandler:(DownloadProgressHandler)progressHandler andCompletionHandler:(DocumentDownloadCompletionHandler)handler; // Will download the specified google docs document. // - (void) downloadDocument:(GDataEntryDocBase*)document withProgressHandler:(DownloadProgressHandler)progressHandler andCompletionHandler:(DocumentDownloadCompletionHandler)handler; // Uploads the document entry, optionally updating it with a new revision. // - (void) uploadEntry:(GDataEntryDocBase*)docEntry asNewRevision:(BOOL)newRevision forWindow:(id)window withProgressHandler:(UploadProgressHandler)progressHandler andCompletionHandler:(CompletionHandler)handler; // Uploads the specified file to the authenticated google docs account. // - (void)uploadFileAtPath:(NSString *)path forWindow:(id)window withProgressHandler:(UploadProgressHandler)progressHandler andCompletionHandler:(CompletionHandler)handler; // More for internal use than anything else. Used to determine the mime type based on the google docs class // and/or file extension. // - (void)getMIMEType:(NSString **)mimeType andEntryClass:(Class *)class forExtension:(NSString *)extension; // Getter and Setter for username, // - (void) setUsername:(NSString*)newUsername; - (NSString*) username; // Getter and Setter for password. The password will be encrypted before storing it in user preferances. // - (void) setPassword:(NSString*)newPassword; - (NSString*) password; // Returns the username that google is given for signing in. // - (NSString *)signedInUsername; // Returns a static instance of the docs service. // + (GDataServiceGoogleDocs *)docsService; @end 

    Y aquí está la implementación:

     // // GDataInterface.m // GDataInterface // // Created by Peter Easdown on 19/12/11. // Copyright (c) 2011 PKCLsoft. All rights reserved. // #import "GDataInterface.h" #import "Util.h" #if TARGET_OS_IPHONE #import "GTMOAuth2ViewControllerTouch.h" #import "GData.h" #else #import "GData/GTMOAuth2WindowController.h" //#import "GDataServiceGoogleSpreadsheet.h" #endif #define GOOGLE_DATA_CLIENT_ID @"<client id>.apps.googleusercontent.com" #define GOOGLE_DATA_SECRET @"<google data secret>" #define GOOGLE_DATA_USERNAME @"googleDocsUsername" #define GOOGLE_DATA_PASSWORD @"googleDocsPassword" @interface GDataInterface (PrivateMethods) - (GDataServiceTicket *) uploadTicket; - (void) setUploadTicket:(GDataServiceTicket *)ticket; - (GDataFeedDocList *)docListFeed; - (void)setDocListFeed:(GDataFeedDocList *)feed; - (NSError *)docListFetchError; - (void)setDocListFetchError:(NSError *)error; - (GDataServiceTicket *)docListFetchTicket; - (void)setDocListFetchTicket:(GDataServiceTicket *)ticket; @end @implementation GDataInterface static NSString *const kKeychainItemName = @"GDataInterface: Google Docs"; // Initializer that provides the username and password. // - (id) initWithUsername:(NSString*)username andPassword:(NSString*)password { self = [super init]; if (self != nil) { username_ = [username retain]; password_ = [password retain]; [[GDataInterface docsService] setUserCnetworkingentialsWithUsername:username_ password:password_]; } return self; } - (void) setUsername:(NSString*)newUsername { username_ = [newUsername retain]; [[GDataInterface docsService] setUserCnetworkingentialsWithUsername:newUsername password:password_]; } - (NSString*) username { return username_; } - (void) setPassword:(NSString*)newPassword { password_ = [newPassword retain]; [[GDataInterface docsService] setUserCnetworkingentialsWithUsername:username_ password:newPassword]; [Util setPassword:newPassword forKey:GOOGLE_DATA_PASSWORD]; } - (NSString*) password { return password_; } static GDataInterface *shanetworking_instance_; // Returns the shanetworking instance of the class. There will only ever be a single instance // of this class. // + (GDataInterface*) shanetworkingInstance { if (shanetworking_instance_ == nil) { shanetworking_instance_ = [[GDataInterface alloc] initWithUsername:[[NSUserDefaults standardUserDefaults] valueForKey:GOOGLE_DATA_USERNAME] andPassword:[Util getPassword:GOOGLE_DATA_PASSWORD]]; // Load the OAuth token from the keychain, if it was previously saved NSString *clientID = GOOGLE_DATA_CLIENT_ID; NSString *clientSecret = GOOGLE_DATA_SECRET; GTMOAuth2Authentication *auth; #if TARGET_OS_IPHONE auth = [GTMOAuth2ViewControllerTouch authForGoogleFromKeychainForName:kKeychainItemName clientID:clientID clientSecret:clientSecret]; #else auth = [GTMOAuth2WindowController authForGoogleFromKeychainForName:kKeychainItemName clientID:clientID clientSecret:clientSecret]; #endif [[GDataInterface docsService] setAuthorizer:auth]; } return shanetworking_instance_; } - (NSString *)signedInUsername { // Get the email address of the signed-in user GTMOAuth2Authentication *auth = [[GDataInterface docsService] authorizer]; BOOL isSignedIn = auth.canAuthorize; if (isSignedIn) { return auth.userEmail; } else { return nil; } } - (BOOL) isSignedIn { return ([self signedInUsername] != nil); } - (void)runSigninThenInvokeHandler:(CompletionHandler)handler forWindow:(id)window { // Applications should have client ID and client secret strings // hardcoded into the source, but the sample application asks the // developer for the strings NSString *clientID = GOOGLE_DATA_CLIENT_ID; NSString *clientSecret = GOOGLE_DATA_SECRET; // Show the OAuth 2 sign-in controller NSString *scope = [GTMOAuth2Authentication scopeWithStrings: [GDataServiceGoogleDocs authorizationScope], [GDataServiceGoogleSpreadsheet authorizationScope], nil]; #if TARGET_OS_IPHONE NSAssert((window != nil), @"window must be a non-nil navigation controller"); GTMOAuth2ViewControllerTouch *viewController; viewController = [GTMOAuth2ViewControllerTouch controllerWithScope:scope clientID:clientID clientSecret:clientSecret keychainItemName:kKeychainItemName completionHandler:^(GTMOAuth2ViewControllerTouch *viewController, GTMOAuth2Authentication *auth, NSError *error) { [rootController_ dismissModalViewControllerAnimated:YES]; [rootController_ release]; rootController_ = nil; // callback if (error == nil) { [[GDataInterface docsService] setAuthorizer:auth]; username_ = [self signedInUsername]; handler(YES); } else { NSLog(@"Authentication error: %@", error); NSData *responseData = [[error userInfo] objectForKey:@"data"]; // kGTMHTTPFetcherStatusDataKey if ([responseData length] > 0) { // show the body of the server's authentication failure response NSString *str = [[[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding] autorelease]; NSLog(@"%@", str); } handler(NO); } }]; // Optional: display some html briefly before the sign-in page loads NSString *html = @"<html><body bgcolor=silver><div align=center>Loading sign-in page...</div></body></html>"; viewController.initialHTMLString = html; // For iOS, window is a navigation controller. // rootController_ = [(UIViewController*)window retain]; [rootController_ presentModalViewController:viewController animated:YES]; #else NSBundle *frameworkBundle = [NSBundle bundleForClass:[GTMOAuth2WindowController class]]; GTMOAuth2WindowController *windowController; windowController = [GTMOAuth2WindowController controllerWithScope:scope clientID:clientID clientSecret:clientSecret keychainItemName:kKeychainItemName resourceBundle:frameworkBundle]; [windowController signInSheetModalForWindow:window completionHandler:^(GTMOAuth2Authentication *auth, NSError *error) { // callback if (error == nil) { [[GDataInterface docsService] setAuthorizer:auth]; username_ = [auth userEmail]; handler(YES); } else { handler(NO); } }]; #endif } - (void) signInOrOutWithCompletionHandler:(CompletionHandler)handler forWindow:(id)window { if (![self isSignedIn]) { // Sign in [self runSigninThenInvokeHandler:handler forWindow:window]; } else { // Sign out GDataServiceGoogleDocs *service = [GDataInterface docsService]; #if TARGET_OS_IPHONE [GTMOAuth2ViewControllerTouch removeAuthFromKeychainForName:kKeychainItemName]; #else [GTMOAuth2WindowController removeAuthFromKeychainForName:kKeychainItemName]; #endif [service setAuthorizer:nil]; handler(YES); } } - (void) retrieveDocumentListWithCompletionHandler:(RetrievalCompletionHandler)handler { [self setDocListFeed:nil]; [self setDocListFetchError:nil]; [self setDocListFetchTicket:nil]; GDataServiceGoogleDocs *service = [GDataInterface docsService]; GDataServiceTicket *ticket; // Fetching a feed gives us 25 responses by default. We need to use // the feed's "next" link to get any more responses. If we want more than 25 // at a time, instead of calling fetchDocsFeedWithURL, we can create a // GDataQueryDocs object, as shown here. NSURL *feedURL = [GDataServiceGoogleDocs docsFeedURL]; GDataQueryDocs *query = [GDataQueryDocs documentQueryWithFeedURL:feedURL]; [query setMaxResults:1000]; [query setShouldShowFolders:NO]; ticket = [service fetchFeedWithQuery:query completionHandler:^(GDataServiceTicket *ticket, GDataFeedBase *feed, NSError *error) { // callback [self setDocListFeed:(GDataFeedDocList *)feed]; [self setDocListFetchError:error]; [self setDocListFetchTicket:nil]; if (handler != nil) { handler((GDataFeedDocList *)feed, (error == nil)); } }]; [self setDocListFetchTicket:ticket]; } - (void) downloadDocument:(GDataEntryDocBase*)document withProgressHandler:(DownloadProgressHandler)progressHandler andCompletionHandler:(DocumentDownloadCompletionHandler)handler { // the content src attribute is used for downloading NSURL *exportURL = [[document content] sourceURL]; if (exportURL != nil) { GDataQuery *query = [GDataQuery queryWithFeedURL:exportURL]; [query addCustomParameterWithName:@"exportFormat" value:@"txt"]; NSURL *downloadURL = [query URL]; // Read the document's contents asynchronously from the network // requestForURL:ETag:httpMethod: sets the user agent header of the // request and, when using ClientLogin, adds the authorization header NSURLRequest *request = [[GDataInterface docsService] requestForURL:downloadURL ETag:nil httpMethod:nil]; GTMHTTPFetcher *fetcher = [GTMHTTPFetcher fetcherWithRequest:request]; [fetcher setAuthorizer:[[GDataInterface docsService] authorizer]]; __block double maxSize = 10240.0; if (progressHandler != nil) { [fetcher setReceivedDataBlock:^(NSData *dataReceivedSoFar) { if ([[fetcher response] expectedContentLength] > 0) { maxSize = [[fetcher response] expectedContentLength]; } else if ([dataReceivedSoFar length] > maxSize) { maxSize += 5120.0; } progressHandler(0.0, maxSize, (double)[dataReceivedSoFar length]); }]; } [fetcher setCommentWithFormat:@"downloading \"%@\"", [[document title] stringValue]]; [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { if (progressHandler != nil) { // Update the progress handler with a "complete" progress. // progressHandler(0.0, (double)[data length], (double)[data length]); } // callback if (error == nil) { // Successfully downloaded the document // if (handler != nil) { handler(data, YES); } } else { if (handler != nil) { handler(nil, NO); } } }]; } } - (void) downloadURL:(NSURL*)url withProgressHandler:(DownloadProgressHandler)progressHandler andCompletionHandler:(DocumentDownloadCompletionHandler)handler { NSURL *downloadURL = [url copy]; // Read the document's contents asynchronously from the network NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL]; GTMHTTPFetcher *fetcher = [GTMHTTPFetcher fetcherWithRequest:request]; __block double maxSize = 10240.0; if (progressHandler != nil) { [fetcher setReceivedDataBlock:^(NSData *dataReceivedSoFar) { if ([[fetcher response] expectedContentLength] > 0) { maxSize = [[fetcher response] expectedContentLength]; } else if ([dataReceivedSoFar length] > maxSize) { maxSize += 5120.0; } progressHandler(0.0, maxSize, (double)[dataReceivedSoFar length]); }]; } [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { if (progressHandler != nil) { progressHandler(0.0, (double)[data length], (double)[data length]); } // callback if (error == nil) { // Successfully downloaded the document // if (handler != nil) { handler(data, YES); } } else { if (handler != nil) { handler(nil, NO); } } }]; // Block, waiting for 60 seconds for the download. // [fetcher waitForCompletionWithTimeout:60.0]; if ([fetcher isFetching] == YES) { // OK, so this looks like we've timed out waiting for the download to complete. Cancel the // fetch. // [fetcher stopFetching]; if (handler != nil) { handler(nil, NO); } } } #pragma mark Upload - (void)getMIMEType:(NSString **)mimeType andEntryClass:(Class *)class forExtension:(NSString *)extension { // Mac OS X's UTI database doesn't know MIME types for .doc and .xls // so GDataEntryBase's MIMETypeForFileAtPath method isn't helpful here struct MapEntry { NSString *extension; NSString *mimeType; NSString *className; }; static struct MapEntry sMap[] = { { @"csv", @"text/csv", @"GDataEntryStandardDoc" }, { @"doc", @"application/msword", @"GDataEntryStandardDoc" }, { @"docx", @"application/vnd.openxmlformats-officedocument.wordprocessingml.document", @"GDataEntryStandardDoc" }, { @"ods", @"application/vnd.oasis.opendocument.spreadsheet", @"GDataEntrySpreadsheetDoc" }, { @"odt", @"application/vnd.oasis.opendocument.text", @"GDataEntryStandardDoc" }, { @"pps", @"application/vnd.ms-powerpoint", @"GDataEntryPresentationDoc" }, { @"ppt", @"application/vnd.ms-powerpoint", @"GDataEntryPresentationDoc" }, { @"rtf", @"application/rtf", @"GDataEntryStandardDoc" }, { @"sxw", @"application/vnd.sun.xml.writer", @"GDataEntryStandardDoc" }, { @"txt", @"text/plain", @"GDataEntryStandardDoc" }, { @"xls", @"application/vnd.ms-excel", @"GDataEntrySpreadsheetDoc" }, { @"xlsx", @"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", @"GDataEntrySpreadsheetDoc" }, { @"jpg", @"image/jpeg", @"GDataEntryStandardDoc" }, { @"jpeg", @"image/jpeg", @"GDataEntryStandardDoc" }, { @"png", @"image/png", @"GDataEntryStandardDoc" }, { @"bmp", @"image/bmp", @"GDataEntryStandardDoc" }, { @"gif", @"image/gif", @"GDataEntryStandardDoc" }, { @"html", @"text/html", @"GDataEntryStandardDoc" }, { @"htm", @"text/html", @"GDataEntryStandardDoc" }, { @"tsv", @"text/tab-separated-values", @"GDataEntryStandardDoc" }, { @"tab", @"text/tab-separated-values", @"GDataEntryStandardDoc" }, { @"pdf", @"application/pdf", @"GDataEntryPDFDoc" }, { nil, nil, nil } }; NSString *lowerExtn = [extension lowercaseString]; for (int idx = 0; sMap[idx].extension != nil; idx++) { if ([lowerExtn isEqual:sMap[idx].extension]) { *mimeType = sMap[idx].mimeType; *class = NSClassFromString(sMap[idx].className); return; } } *mimeType = nil; *class = nil; return; } - (void) uploadEntry:(GDataEntryDocBase*)docEntry asNewRevision:(BOOL)newRevision forWindow:(id)window withProgressHandler:(UploadProgressHandler)progressHandler andCompletionHandler:(CompletionHandler)handler { uploadWindow = [window retain]; uploadCompletionHandler = [handler copy]; NSURL *uploadURL; if (newRevision == YES) { GDataQueryDocs *query = [GDataQueryDocs queryWithFeedURL:[[docEntry uploadEditLink] URL]]; [query setShouldCreateNewRevision:YES]; uploadURL = [query URL]; } else { uploadURL = [GDataServiceGoogleDocs docsUploadURL]; } // make service tickets call back into our upload progress selector GDataServiceGoogleDocs *service = [GDataInterface docsService]; [service setServiceUploadProgressHandler:^(GDataServiceTicketBase *ticket, unsigned long long numberOfBytesRead, unsigned long long dataLength) { if (progressHandler != nil) { progressHandler(0.0, (double)dataLength, (double)numberOfBytesRead); } }]; // insert the entry into the docList feed // // to update (replace) an existing entry by uploading a new file, // use the fetchEntryByUpdatingEntry:forEntryURL: with the URL from // the entry's uploadEditLink GDataServiceTicket *ticket; if (newRevision == YES) { ticket = [service fetchEntryByUpdatingEntry:docEntry forEntryURL:uploadURL delegate:self didFinishSelector:@selector(uploadFileTicket:finishedWithEntry:error:)]; } else { ticket = [service fetchEntryByInsertingEntry:docEntry forFeedURL:uploadURL delegate:self didFinishSelector:@selector(uploadFileTicket:finishedWithEntry:error:)]; } [ticket setUploadProgressHandler:^(GDataServiceTicketBase *ticket, unsigned long long numberOfBytesRead, unsigned long long dataLength) { // progress callback if (progressHandler != nil) { progressHandler(0.0, (double)dataLength, (double)numberOfBytesRead); } }]; // we turned automatic retry on when we allocated the service, but we // could also turn it on just for this ticket [self setUploadTicket:ticket]; [service setServiceUploadProgressHandler:nil]; } - (void)uploadFileAtPath:(NSString *)path forWindow:(id)window withProgressHandler:(UploadProgressHandler)progressHandler andCompletionHandler:(CompletionHandler)handler { NSString *errorMsg = nil; // make a new entry for the file NSString *mimeType = nil; Class entryClass = nil; NSString *extn = [path pathExtension]; [self getMIMEType:&mimeType andEntryClass:&entryClass forExtension:extn]; if (!mimeType) { // for other file types, see if we can get the type from the Mac OS // and use a generic file document entry class mimeType = [GDataUtilities MIMETypeForFileAtPath:path defaultMIMEType:nil]; entryClass = [GDataEntryFileDoc class]; } if (mimeType && entryClass) { GDataEntryDocBase *newEntry = [entryClass documentEnt