In this article by Hossam Ghareeb, the author of the book, iOS Programming Cookbook, we will cover the recipe Integrating iMessage app with iMessage app.
(For more resources related to this topic, see here.)
Using iMessage apps will let users use your apps seamlessly from iMessage without having to leave the iMessage. Your app can share content in the conversation, make payment, or do any specific job that seems important or is appropriate to do within a Messages app.
Similar to the Stickers app we created earlier, you need Xcode 8.0 or later version to create an iMessage app extension and you can test it easily in the iOS simulator. The app that we are going to build is a Google drive picker app. It will be used from an iMessage extension to send a file to your friends just from Google Drive.
Before starting, ensure that you follow the instructions in Google Drive API for iOS from https://developers.google.com/drive/ios/quickstart to get a client key to be used in our app.
Installing the SDK in Xcode will be done via CocoaPods. To get more information about CocoaPods and how to use it to manage dependencies, visit https://cocoapods.org/ .
cd path_to_directory
Pod init
use_frameworks!
target 'PDFPicker' do
end
target 'MessagesExtension' do
pod 'GoogleAPIClient/Drive', '~> 1.0.2'
pod 'GTMOAuth2', '~> 1.1.0'
end
import GoogleAPIClient
import GTMOAuth2
private let kKeychainItemName = "Drive API"
private let kClientID = "Client_Key_Goes_HERE"
private let scopes = [kGTLAuthScopeDrive]
private let service = GTLServiceDrive()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
if let auth =
GTMOAuth2ViewControllerTouch.authForGoogleFromKeychain(forName:
kKeychainItemName,
clientID: kClientID,
clientSecret: nil)
{
service.authorizer = auth
}
}
// When the view appears, ensure that the Drive API service is
authorized
// and perform API calls
override func viewDidAppear(_ animated: Bool) {
if let authorizer = service.authorizer,
canAuth = authorizer.canAuthorize where canAuth {
fetchFiles()
} else {
present(createAuthController(), animated: true, completion: nil)
}
}
// Construct a query to get names and IDs of 10 files using the Google
Drive API
func fetchFiles() {
print("Getting files...")
if let query = GTLQueryDrive.queryForFilesList(){
query.fields = "nextPageToken, files(id, name, webViewLink,
webContentLink, fileExtension)"
service.executeQuery(query, delegate: self, didFinish:
#selector(MessagesViewController.displayResultWithTicket(ticket:finishedWit
hObject:error:)))
}
}
// Parse results and display
func displayResultWithTicket(ticket : GTLServiceTicket,
finishedWithObject response :
GTLDriveFileList,
if let error = error {
showAlert(title: "Error", message: error.localizedDescription)
return
}
var filesString = ""
let files = response.files as! [GTLDriveFile]
if !files.isEmpty{
filesString += "Files:n"
for file in files{
filesString += "(file.name) ((file.identifier)
((file.webViewLink) ((file.webContentLink))n"
}
} else {
filesString = "No files found."
}
print(filesString)
}
// Creates the auth controller for authorizing access to Drive API
private func createAuthController() -> GTMOAuth2ViewControllerTouch {
let scopeString = scopes.joined(separator: " ")
return GTMOAuth2ViewControllerTouch(
scope: scopeString,
clientID: kClientID,
clientSecret: nil,
keychainItemName: kKeychainItemName,
delegate: self,
finishedSelector:
#selector(MessagesViewController.viewController(vc:finishedWithAuth:error:)
)
)
}
// Handle completion of the authorization process, and update the Drive
API
// with the new credentials.
func viewController(vc : UIViewController,
finishedWithAuth authResult :
GTMOAuth2Authentication, error : NSError?) {
if let error = error {
service.authorizer = nil
showAlert(title: "Authentication Error", message:
error.localizedDescription)
return
}
service.authorizer = authResult
dismiss(animated: true, completion: nil)
fetchFiles()
}
// Helper for showing an alert
func showAlert(title : String, message: String) {
let alert = UIAlertController(
title: title,
message: message,
preferredStyle: UIAlertControllerStyle.alert
)
let ok = UIAlertAction(
title: "OK",
style: UIAlertActionStyle.default,
handler: nil
)
alert.addAction(ok)
self.present(alert, animated: true, completion: nil)
}
private var currentFiles = [GTLDriveFile]()
// Parse results and display
func displayResultWithTicket(ticket : GTLServiceTicket,
finishedWithObject response :
GTLDriveFileList,
error : NSError?) {
if let error = error {
showAlert(title: "Error", message: error.localizedDescription)
return
}
var filesString = ""
let files = response.files as! [GTLDriveFile]
self.currentFiles = files
if !files.isEmpty{
filesString += "Files:n"
for file in files{
filesString += "(file.name) ((file.identifier)
((file.webViewLink) ((file.webContentLink))n"
}
} else {
filesString = "No files found."
}
print(filesString)
self.filesTableView.reloadData()
}
// MARK: - Table View methods -
func tableView(_ tableView: UITableView, numberOfRowsInSection section:
Int) -> Int {
return self.currentFiles.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath:
IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
let file = self.currentFiles[indexPath.row]
cell?.textLabel?.text = file.name
return cell!
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath:
IndexPath) {
let file = self.currentFiles[indexPath.row]
// Download File here to send as attachment.
if let downloadURLString = file.webContentLink{
let url = NSURL(string: downloadURLString)
if let name = file.name{
let downloadedPath = (documentsPath() as
NSString).appendingPathComponent("(name)")
let fetcher = service.fetcherService.fetcher(with: url as!
URL)
let destinationURL = NSURL(fileURLWithPath: downloadedPath)
as URL
fetcher.destinationFileURL = destinationURL
fetcher.beginFetch(completionHandler: { (data, error) in
if error == nil{
self.activeConversation?.insertAttachment(destinationURL,
withAlternateFilename: name, completionHandler: nil)
}
})
}
}
}
private func documentsPath() -> String{
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory,
.userDomainMask, true)
return paths.first ?? ""
}
We started by installing the Google Drive SDK to the Xcode project. This SDK has all the APIs that we need to manage drive files and user authentication. When you visit the Google developers' website, you will see two options to install the SDK: manually or using CocoaPods. I totally recommend using CocoaPods to manage your dependencies as it is simple and efficient.
Once the SDK has been installed via CocoaPods, we added some variables to be used for the Google Drive API and the most important one is the client key. You can access this value from the project you have created in the Google Developers Console.
In the viewDidLoad function, first, we check if we have an authentication saved in KeyChain, and then, we use it. We can do that by calling GTMOAuth2ViewControllerTouch.authForGoogleFromKeychain, which takes the Keychain name and client key as parameters to search for authentication. It's useful as it helps you remember the last authentication and there is no need to ask for user authentication again if a user has already been authenticated before.
In viewDidAppear, we check if a user is already authenticated; so, in that case, we start fetching files from the drive and, if not, we display the authentication controller, which asks a user to enter his Google account credentials.
To display the authentication controller, we present the authentication view controller created in the createAuthController() function. In this function, the Google Drive API provides us with the GTMOAuth2ViewControllerTouch class, which encapsulates all logic for Google account authentication for your app. You need to pass the client key for your project, keychain name to save the authentication details there, and the finished viewController(vc : UIViewController,
finishedWithAuth authResult : GTMOAuth2Authentication, error : NSError?) selector that will be called after the authentication is complete. In that function, we check for errors and if something wrong happens, we display an alert message to the user. If no error occurs, we start fetching files using the fetchFiles() function.
In the fetchFiles() function, we first create a query by calling GTLQueryDrive.queryForFilesList(). The GTLQueryDrive class has all the information you need about your query, such as which fields to read, for example, name, fileExtension, and a lot of other fields that you can fetch from the Google drive. You can specify the page size if you are going to call with pagination, for example, 10 by 10 files. Once you are happy with your query, execute it by calling service.executeQuery, which takes the query and the finished selector to be called when finished. In our example, it will call the displayResultWithTicket function, which prepares the files to be displayed in the table view. Then, we call self.filesTableView.reloadData() to refresh the table view to display the list of files.
In the delegate function of table view didSelectRowAt indexPath:, we first read the webContentLink property from the GTLDriveFile instance, which is a download link for the selected file. To fetch a file from the Google drive, the API provides us with GTMSessionFetcher that can fetch a file and write it directly to a device's disk locally when you pass a local path to it. To create GTMSessionFetcher, use the service.fetcherService factory class, which gives you instance to a fetcher via the file URL. Then, we create a local path to the downloaded file by appending the filename to the documents path of your app and then, pass it to fetcher via the following command:
fetcher.destinationFileURL = destinationURL
Once you set up everything, call fetcher.beginFetch and pass a completion handler to be executed after finishing the fetching. Once the fetching is completed successfully, you can get a reference to the current conversation so that you can insert the file to it as an attachment. To do this, just call the following function:
self.activeConversation?.insertAttachment(destinationURL,
withAlternateFilename: name, completionHandler: nil)
Yes, there's more that you can do in the preceding example to make it fancier and more appealing to users. Check the following options to make it better:
As we said, one of the features that we can add in the preceding example is the ability to show a progress bar indicating the downloading progress of a file. Before starting how to show a progress bar, let's install a library that is very helpful in managing/showing HUD indicators, which is MBProgressHUD. This library is available in GitHub at https://github.com/jdg/MBProgressHUD.
As we agreed before, all packages are managed via CocoaPods, so now, let's install the library via CocoaPods, as shown:
use_frameworks!
target 'PDFPicker' do
end
target 'MessagesExtension' do
pod 'GoogleAPIClient/Drive', '~> 1.0.2'
pod 'GTMOAuth2', '~> 1.1.0'
pod 'MBProgressHUD', '~> 1.0.0'
end
pod install
func tableView(_ tableView: UITableView, didSelectRowAt indexPath:
IndexPath) {
let file = self.currentFiles[indexPath.row]
// Download File here to send as attachment.
if let downloadURLString = file.webContentLink{
let url = NSURL(string: downloadURLString)
if let name = file.name{
let downloadedPath = (documentsPath() as
NSString).appendingPathComponent("(name)")
let fetcher = service.fetcherService.fetcher(with: url as!
URL)
let destinationURL = NSURL(fileURLWithPath: downloadedPath)
as URL
fetcher.destinationFileURL = destinationURL
var progress = Progress()
let hud = MBProgressHUD.showAdded(to: self.view, animated:
true)
hud.mode = .annularDeterminate;
hud.progressObject = progress
fetcher.beginFetch(completionHandler: { (data, error) in
if error == nil{
hud.hide(animated: true)
self.activeConversation?.insertAttachment(destinationURL,
withAlternateFilename: name, completionHandler: nil)
}
})
fetcher.downloadProgressBlock = { (bytes, written,
expected) in
let p = Double(written) * 100.0 / Double(expected)
print(p)
progress.totalUnitCount = expected
progress.completedUnitCount = written
}
}
}
}
As you can see, the progressive view is updated with the percentage of download to give the user an overview of what is going on.
Loading all files at once is easy from the development side, but it's incorrect from the user experience side. It will take too much time at the beginning when you get the list of all the files and it would be great if we could request only 10 files at a time with pagination. In this section, we will see how to add the pagination concept to our example and request only 10 files at a time. When a user scrolls to the end of the list, we will display a loading indicator, call the next page, and append the results to our current results. Implementation of pagination is pretty easy and requires only a few changes in our code. Let's see how to do it:
private var doneFetchingFiles = false
private var nextPageToken: String!
func fetchFiles() {
print("Getting files...")
if let query = GTLQueryDrive.queryForFilesList(){
query.fields = "nextPageToken, files(id, name, webViewLink,
webContentLink, fileExtension)"
query.mimeType = "application/pdf"
query.pageSize = 10
query.pageToken = nextPageToken
service.executeQuery(query, delegate: self, didFinish:
#selector(MessagesViewController.displayResultWithTicket(ticket:finishedWit
hObject:error:)))
}
}
// Parse results and display
func displayResultWithTicket(ticket : GTLServiceTicket,
finishedWithObject response :
GTLDriveFileList,
error : NSError?) {
if let error = error {
showAlert(title: "Error", message: error.localizedDescription)
return
}
var filesString = ""
nextPageToken = response.nextPageToken
let files = response.files as! [GTLDriveFile]
doneFetchingFiles = files.isEmpty
self.currentFiles += files
if !files.isEmpty{
filesString += "Files:n"
for file in files{
filesString += "(file.name) ((file.identifier)
((file.webViewLink) ((file.webContentLink))n"
}
} else {
filesString = "No files found."
}
print(filesString)
self.filesTableView.reloadData()
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell,
forRowAt indexPath: IndexPath) {
if !doneFetchingFiles && indexPath.row == self.currentFiles.count {
// Refresh cell
fetchFiles()
return
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section:
Int) -> Int {
return doneFetchingFiles ? self.currentFiles.count :
self.currentFiles.count + 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath:
IndexPath) -> UITableViewCell {
if !doneFetchingFiles && indexPath.row == self.currentFiles.count{
return tableView.dequeueReusableCell(withIdentifier:
"progressCell")!
}
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
let file = self.currentFiles[indexPath.row]
cell?.textLabel?.text = file.name
return cell!
}
Now, everything seems perfect. When you build and run, you will see only 10 files listed, as shown:
And when you scroll down you would see the progress cell that and 10 more files will be called.
In this article, we learned how to integrate iMessage app with iMessage app.
Further resources on this subject: