Google Drive SDK
Feedback on this document

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.

DrEdit iOS

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.

MainStoryboard.storybaord

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:

  1. A code retrieved from the OAuth 2.0 authorization flow using a WebView
  2. 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:

  1. Client ID
  2. 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.

Additional resources

Authentication required

You need to be signed in with Google+ to do that.

Signing you in...

Google Developers needs your permission to do that.