Example Drive App: DrEdit for Objective-C / iOS
DrEdit is a sample Google Drive app written in Objective-C for
iOS. It is a text editor capable of editing files with the MIME type
text/*
that are stored in a user's Google Drive. This article describes the complete
application to help you in your integrations.
Setting up this sample application
Setting up DrEdit requires using the iOS SDK and performing some configuration. Follow the instructions in the repository's README to set up the application.
Features
DrEdit is an iOS application using a Navigation Controller and two View Controllers:
-
DrEditFilesListViewController
Custom
UITableViewController
implementation that is responsible for authentication and retrieving/listing the user's Drive text files that the app has access to. -
DrEditFileEditViewController
Custom
UIViewController
implementation that is responsible for retrieving the Drive file's content and saving changes to Drive.
Authorization
In order for DrEdit to be able to make calls to the Drive API, an authorized API client must be created. When using the Google APIs Client Library for Objective-C, the first step is to get the credentials of the user to authorize the client.
These credentials can be loaded from:
- A code retrieved from the OAuth 2.0 authorization flow using a WebView
- iOS Keychain Services if the user has already authorized the app
Setting up the client ID and client secret
Google Drive applications need these two values for authorization:
- Client ID
- Client secret
The client ID and client secret for an application are created when an
application is registered in the Google APIs
Console and the OAuth 2.0 client
IDs are generated.
These can be viewed in API Access tab of a project. In DrEdit for Objective-C/iOS, these values
are stored in DrEdit/DrEdit/DrEditFilesListViewController.m
.
// Constants used for OAuth 2.0 authorization.
static NSString *const kKeychainItemName = @"iOSDriveSample: Google Drive";
static NSString *const kClientId = @"<CLIENT_ID>";
static NSString *const kClientSecret = @"<CLIENT_SECRET>";
Authorizing your app
The
Google APIs client library for Objective-C
uses the
Google Toolbox for Mac OAuth 2 Controllers
to handle OAuth 2.0 sign in for your app. It providees a
custom UIViewController
implementation to show the authorization flow to
the user and process the provided authorization code from Google's
authorization servers.
The authorization flow is started when the user clicks on the
authButton
sending an authButtonClicked
action to our
UIViewController
. This action can have two effects:
- If the user is not signed in, it starts the authorization flow.
- if the user is already signed in, it signs out the user and removes the credentials from iOS Keychain Services.
Implementation:
- (IBAction)authButtonClicked:(id)sender {
if (!self.isAuthorized) {
// Sign in.
SEL finishedSelector = @selector(viewController:finishedWithAuth:error:);
GTMOAuth2ViewControllerTouch *authViewController =
[[GTMOAuth2ViewControllerTouch alloc] initWithScope:kGTLAuthScopeDriveFile
clientID:kClientId
clientSecret:kClientSecret
keychainItemName:kKeychainItemName
delegate:self
finishedSelector:finishedSelector];
[self presentModalViewController:authViewController
animated:YES];
} else {
// Sign out
[GTMOAuth2ViewControllerTouch removeAuthFromKeychainForName:kKeychainItemName];
[[self driveService] setAuthorizer:nil];
self.authButton.title = @"Sign in";
self.isAuthorized = NO;
[self toggleActionButtons:NO];
[self.driveFiles removeAllObjects];
[self.tableView reloadData];
}
}
Once the authorization flow is completed by the user, the
GTMOAuth2ViewControllerTouch
UIViewController
calls the provied
finishedSelector
that is implemented in our View Controller:
- (void)viewController:(GTMOAuth2ViewControllerTouch *)viewController
finishedWithAuth:(GTMOAuth2Authentication *)auth
error:(NSError *)error {
[self dismissModalViewControllerAnimated:YES];
if (error == nil) {
[self isAuthorizedWithAuthentication:auth];
}
}
Loading credentials from iOS Keychain Services
Once the DrEditFilesListViewController
has loaded, it checks for
existing OAuth 2.0 credentials from iOS Keychain Services. This flow makes the app
more user friendly as it doesn't ask for the user to authenticate
every time the app is used:
- (void)viewDidLoad
{
[super viewDidLoad];
// Check for authorization.
GTMOAuth2Authentication *auth =
[GTMOAuth2ViewControllerTouch authForGoogleFromKeychainForName:kKeychainItemName
clientID:kClientId
clientSecret:kClientSecret];
if ([auth canAuthorize]) {
[self isAuthorizedWithAuthentication:auth];
}
}
Making authorized API requests
Once the access token has been retrieved, it is used to authorize the
GTLServiceDrive
to make authorized requests to the Google Drive API.
- (void)isAuthorizedWithAuthentication:(GTMOAuth2Authentication *)auth
{
[[self driveService] setAuthorizer:auth];
self.authButton.title = @"Sign out";
self.isAuthorized = YES;
[self toggleActionButtons:YES];
[self loadDriveFiles];
}
[self driveService]
is a property that returns a static
GTLServiceDrive
that is used to send requests to the Google Drive
API:
- (GTLServiceDrive *)driveService {
static GTLServiceDrive *service = nil;
if (!service) {
service = [[GTLServiceDrive alloc] init];
// Have the service object set tickets to fetch consecutive pages
// of the feed so we do not need to manually fetch them.
service.shouldFetchNextPages = YES;
// Have the service object set tickets to retry temporary error conditions
// automatically.
service.retryEnabled = YES;
}
return service;
}
Sending Requests to Drive
This section describes how DrEdit sends requests to Google Drive to manage the user's text files.
Loading existing Drive text files
In DrEditFilesListViewController
, once the user is authenticated,
existing text files from Google Drive can be loaded. This
UIViewController
only needs metadata in order to show a list of files
titles to the user:
- (void)loadDriveFiles {
GTLQueryDrive *query = [GTLQueryDrive queryForFilesList];
query.q = @"mimeType = 'text/plain'";
UIAlertView *alert =
[DrEditUtilities showLoadingMessageWithTitle:@"Loading files"
delegate:self];
[self.driveService executeQuery:query completionHandler:^(GTLServiceTicket *ticket,
GTLDriveFileList *files,
NSError *error) {
[alert dismissWithClickedButtonIndex:0 animated:YES];
if (error == nil) {
if (self.driveFiles == nil) {
self.driveFiles = [[NSMutableArray alloc] init];
}
[self.driveFiles removeAllObjects];
[self.driveFiles addObjectsFromArray:files.items];
[self.tableView reloadData];
} else {
NSLog(@"An error occurred: %@", error);
[DrEditUtilities showErrorMessageWithTitle:@"Unable to load files"
message:[error description]
delegate:self];
}
}];
}
DrEditFilesListViewController
overrides the
UITableViewDataSource
's cellForRowAtIndexPath
method to display a
list of Drive text file titles:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
GTLDriveFile *file = [self.driveFiles objectAtIndex:indexPath.row];
cell.textLabel.text = file.title;
return cell;
}
Loading Drive file's content
When a user clicks on a file, the DrEditFileEditViewController
comes into view and loads the actual Drive file's content and
displays it on the UITextView
by making a simple authorized GET
request to the file's downloadUrl
:
- (void)loadFileContent {
UIAlertView *alert = [DrEditUtilities showLoadingMessageWithTitle:@"Loading file content"
delegate:self];
GTMHTTPFetcher *fetcher =
[self.driveService.fetcherService fetcherWithURLString:self.driveFile.downloadUrl];
[fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
[alert dismissWithClickedButtonIndex:0 animated:YES];
if (error == nil) {
NSString* fileContent = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
self.textView.text = fileContent;
self.originalContent = [fileContent copy];
} else {
NSLog(@"An error occurred: %@", error);
[DrEditUtilities showErrorMessageWithTitle:@"Unable to load file"
message:[error description]
delegate:self];
}
}];
}
Saving files
When the user clicks Save
from the DrEditFileEditViewController
custom UIViewController
implementation, an authorized request is sent to
the Google Drive API to insert the
file if it is new or update the
file's metadata and content if it is an existing file:
- (void)saveFile {
GTLUploadParameters *uploadParameters = nil;
// Only update the file content if different.
if (![self.originalContent isEqualToString:self.textView.text]) {
NSData *fileContent = [self.textView.text dataUsingEncoding:NSUTF8StringEncoding];
uploadParameters = [GTLUploadParameters uploadParametersWithData:fileContent
MIMEType:@"text/plain"];
}
self.driveFile.title = self.updatedTitle;
GTLQueryDrive *query = nil;
if (self.driveFile.identifier == nil || self.driveFile.identifier.length == 0) {
// This is a new file, instantiate an insert query.
query = [GTLQueryDrive queryForFilesInsertWithObject:self.driveFile
uploadParameters:uploadParameters];
} else {
// This file already exists, instantiate an update query.
query = [GTLQueryDrive queryForFilesUpdateWithObject:self.driveFile
fileId:self.driveFile.identifier
uploadParameters:uploadParameters];
}
UIAlertView *alert = [DrEditUtilities showLoadingMessageWithTitle:@"Saving file"
delegate:self];
[self.driveService executeQuery:query completionHandler:^(GTLServiceTicket *ticket,
GTLDriveFile *updatedFile,
NSError *error) {
[alert dismissWithClickedButtonIndex:0 animated:YES];
if (error == nil) {
self.driveFile = updatedFile;
self.originalContent = [self.textView.text copy];
self.updatedTitle = [updatedFile.title copy];
[self toggleSaveButton];
[self.delegate didUpdateFileWithIndex:self.fileIndex
driveFile:self.driveFile];
[self doneEditing:nil];
} else {
NSLog(@"An error occurred: %@", error);
[DrEditUtilities showErrorMessageWithTitle:@"Unable to save file"
message:[error description]
delegate:self];
}
}];
}
On success, the DrEditFilesListViewController
is notified of the
change to update the list of files it holds.
Deleting files
The custom DrEditFileEditViewController
UIViewController
has a
deleteButton
that is used to send a request to Google Drive to
delete the current file:
- (void)deleteFile {
GTLQueryDrive *deleteQuery =[GTLQueryDrive queryForFilesDeleteWithFileId:self.driveFile.identifier];
UIAlertView *alert = [DrEditUtilities showLoadingMessageWithTitle:@"Deleting file"
delegate:self];
[self.driveService executeQuery:deleteQuery completionHandler:^(GTLServiceTicket *ticket,
id object,
NSError *error) {
[alert dismissWithClickedButtonIndex:0 animated:YES];
if (error == nil) {
self.fileIndex = [self.delegate didUpdateFileWithIndex:self.fileIndex
driveFile:nil];
[self.navigationController popViewControllerAnimated:YES];
} else {
NSLog(@"An error occurred: %@", error);
[DrEditUtilities showErrorMessageWithTitle:@"Unable to delete file"
message:[error description]
delegate:self];
}
}];
}
On success, the DrEditFilesListViewController
is notified of the
change to update the list of files it holds.