ОК, поэтому, в отсутствие каких-либо ответов от других, я укусил пулю и сам написал обертку.
Теперь у меня есть одна оболочка, которая работает как для Mac OS, так и для iOS, что значительно упрощает интерфейс с Google Docs.
Ниже приведен весь код для реального интерфейса.Я должен отметить, что этот класс действует как одноэлементный, и вам нужно немного настроить его для каждого проекта, обновив строки:
#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"
соответствующими значениями, полученными из Google.
Я также отмечаю, что класс хранит пароли через NSUserDefaults и использует отдельный служебный класс, чтобы сделать это в зашифрованном виде.Вместо того, чтобы засорять этот ответ здесь всем этим дополнительным кодом, я создал хранилище в bitbucket по адресу:
https://bitbucket.org/pkclsoft/gdatainterface
, которое содержит весь проект XCode, который создает две цели, однудля Mac OS и один для iOS.Я использовал оба этих приложения в приложении, которое теперь находится в магазине приложений, с отличными результатами.Возможно, что пока этот проект создается для меня, вы должны настроить его для своих целей.Проект содержит полный набор Google SDK, который собирается и работает с моим кодом.Я включил его, чтобы уменьшить риск несовместимости с более новыми версиями SDK для тех, кто его захватывает.
Вот спецификация интерфейса в ее нынешнем виде:
//
// 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 shared instance of the class. There will only ever be a single instance
// of this class.
//
+ (GDataInterface*) sharedInstance;
// 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 stored
// 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
И вотреализация:
//
// 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] setUserCredentialsWithUsername:username_ password:password_];
}
return self;
}
- (void) setUsername:(NSString*)newUsername {
username_ = [newUsername retain];
[[GDataInterface docsService] setUserCredentialsWithUsername:newUsername password:password_];
}
- (NSString*) username {
return username_;
}
- (void) setPassword:(NSString*)newPassword {
password_ = [newPassword retain];
[[GDataInterface docsService] setUserCredentialsWithUsername:username_ password:newPassword];
[Util setPassword:newPassword forKey:GOOGLE_DATA_PASSWORD];
}
- (NSString*) password {
return password_;
}
static GDataInterface *shared_instance_;
// Returns the shared instance of the class. There will only ever be a single instance
// of this class.
//
+ (GDataInterface*) sharedInstance {
if (shared_instance_ == nil) {
shared_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 shared_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