AWS Mobile Blog

Introducing AWS Amplify for iOS and Android

Until today, building iOS and Android apps powered by AWS involved using the AWS service-centric handwritten or low level generated SDKs. To set up the backend, you would go to the AWS service console or use the Amplify CLI and use the AWS Mobile SDKs to connect and interact with the backend you created. This included understanding the details of each AWS service needed to build your use cases and calling the appropriate functions using the respective service SDKs. In most cases, this also meant combining one or more SDKs when you build out your use case.

Today, AWS announced that you can now build iOS and Android applications using the Amplify Framework with the same category-based programming model that you use for JavaScript apps. The Amplify Framework is an open source project for building cloud-enabled mobile and web applications, consisting of libraries, UI components, and a CLI toolchain. You can easily add capabilities organized in to categories such as Analytics, AI/ML, APIs (GraphQL and REST), DataStore, and Storage to your mobile applications.

Like the JavaScript library, the Amplify iOS and Android libraries (preview) are use case-centric, in contrast to the AWS service-centric Mobile SDKs, and provide a declarative interface that enables you to programmatically apply best practices with abstractions. This results in a faster development cycle, less lines of code, and is our recommended way to build mobile applications powered by AWS services. You can use these libraries with backends created using the Amplify CLI or with existing AWS backends. Amplify also provides escape hatches that allows you to use the handwritten or low level generated iOS and Android AWS Mobile SDKs for additional use cases.

We are also introducing a new DataStore category in the Amplify Framework which enables mobile and web developers to use a query-able, on-device persistent storage engine that seamlessly synchronizes data between apps and the cloud with built-in versioning, conflict detection and resolution capabilities. Using a local-first programming model, developers can easily reason about consistency and data integrity, and can interact with data seamlessly whether online or offline. These capabilities allow you to interact with domain objects in Java, JavaScript, and Swift which are automatically converted to GraphQL behind the scenes, effectively using GraphQL as a network protocol to securely synchronize the data using a scalable AWS AppSync API. DataStore has a fluent interface for applying conditions when filtering queries or updates to objects which is dynamically generated from your data model defined via GraphQL. This code generation process is fully integrated into Xcode build phases and Gradle with Android Studio, as well as npx scripts for JavaScript applications. When syncing with the cloud, DataStore leverages new “sync enabled” resolvers in AWS AppSync for performing Delta Synchronization as well as applying different conflict resolution strategies. This includes Optimistic Concurrency, AWS Lambda, and Automerge – a new technique using GraphQL types to perform intelligent merge operations.

In this post, you build an iOS application that stores pictures from a photo album to the cloud in an Amazon S3 bucket. You use the Amplify CLI to set up the backend cloud resources and the Amplify iOS libraries to connect and interact with the backend. You also add fine grained authorization for your S3 buckets from the Amplify CLI, and use them in your application. This post assumes that you are familiar with Swift programming language and tools for building an iOS app.

The source code for the application that we build in this blog is available here.

Concepts:

Auth – I use the term Auth to denote both Authentication and Authorization.

· Authentication refers to verifying the identity of your users.

· Authorization refers to the having permissions to access a resource.

Amazon Cognito Identity Pool – Identity pools provide temporary AWS credentials for guests (unauthenticated) as well as or users who have been authenticated and have received a token to access AWS resources.

Background

The sample app in this post has the frontend Swift code already plugged in. Just set up the backend using the Amplify CLI, as described in the following steps. If you choose to build your own app, the following flow helps you understand how to set up the backend and use the Amplify Libraries.

Clone the GitHub repo

In a terminal, run the following command:

$ git clone https://github.com/nikhil-dabhade/amplify-ios-samples

Install and initialize CocoaPods

Amplify iOS uses CocoaPods to serve the CLI and libraries. From the root of your application folder, run the following command:

$ cd <root of application folder>
$ sudo gem install cocoapods
$ pod setup
$ pod init

Install the Amplify CLI

Now, install the Amplify CLI using the following commands:

$ npm install -g @aws-amplify/cli
$ amplify configure

Close the Xcode project if it is open and re-open the Xcode workspace for this sample.

Note: The Amplify CLI is also available through CocoaPods and is the recommended way to install the CLI when using the DataStore or API categories in Amplify iOS.

Configure and initialize your project

Run the following commands:

$ cd <root of your application folder>
$ amplify init

This initializes the project with amplify backend configuration. After the command completes, you can see two files called amplifyconfiguration.json and awsconfiguration.json created in the root of your application folder.

What are amplifyconfiguration.json and awsconfiguration.json?

Rather than configuring each service through a constructor or constants file, Amplify supports configuration through centralized files called amplifyconfiguration.json and awsconfiguration.json which define all the regions and service endpoints to communicate. The configuration related to Auth is present under awsconfiguration.json and for all other categories is present under amplifyconfiguration.json.

Set up Storage and Auth in your backend

You can set up storage and fine-grained access to your S3 buckets using the Amplify CLI. The CLI can configure three different access levels to the storage bucket: public, protected, and private. When you run the amplify add storage command, the CLI configures the appropriate IAM policies on the bucket using an Amazon Cognito Identity Pool Role.

The access levels on your bucket have the following functions:

· Guest: All users of your app can access this bucket. The files are stored under /guest path.
· Protected: Readable by all users but writable only to the creator of the bucket. Files are stored under protected/user_identity_pool where user_identity_pool is the Amazon Cognito Identity ID for that user.
· Private: Only accessible for individual authorized user. Files are stored under private/user_identity_id

From root of your application folder, run the following command

$ amplify add storage

This prompts you to add Auth to your project as well. The following flow shows how to set this up:

? Please select from one of the below mentioned services: Content (Images, audio, video, etc.)
? You need to add auth (Amazon Cognito) to your project in order to add storage for user files. Do you want to add auth now? Yes
 Do you want to use the default authentication and security configuration? Default configuration
 Warning: you will not be able to edit these selections.
 How do you want users to be able to sign in? Username
 Do you want to configure advanced settings? No, I am done.
Successfully added auth resource
? Please provide a friendly name for your resource that will be used to label this category in the project: myamplifystoragebucket
? Please provide bucket name: myamplifystoragebucket
? Who should have access: Auth and guest users
? What kind of access do you want for Authenticated users? create/update, read, delete
? What kind of access do you want for Guest users? create/update, read, delete
? Do you want to add a Lambda Trigger for your S3 Bucket? No

Push your changes

Run the following command:

$ amplify push

The push command provisions your backend in the cloud. It creates the S3 bucket as per the CLI questions and assign policies for CRUD permissions you assigned for authenticated and unauthenticated users.

Integrating Auth library

The sample application by default lets guest users upload images to the guest access folder. However, it does not let guest users upload images to protected and private access folders unless you are logged in. You can go to the Profile section and Login to authenticate.

Open the Podfile and add the following pod files under the target of your app:

pod "AWSMobileClient", "~> #{AWS_SDK_VERSION}"
pod 'AWSAuthUI', "~> #{AWS_SDK_VERSION}"
pod 'AWSUserPoolsSignIn', "~> #{AWS_SDK_VERSION}"

Add Auth to your application. Do this using AWSMobileClient in the application() function of the AppDelegate.swift file as shown in the following code snippet:

AWSMobileClient.default().initialize { (userState, error) in
    guard error == nil else {
        print("Error initializing AWSMobileClient. Error: \(error!.localizedDescription)")
        return
    }
    guard let userState = userState else {
        print("userState is unexpectedly empty initializing AWSMobileClient")
        return
    }
    print("AWSMobileClient initialized, userstate: \(userState)")
}

Add Sign-up/Sign-in

You use the drop-in UI for Auth feature available in Amplify for iOS to add sign-up/sign-in to our app.

You invoke Sign-up flow using AWSMobileClient.default.showSignIn() function.

The code snippet for adding sign-up to your app is under the function handleShowLoginOrSignUpButton () in AmplifyStorageSampleApp/Tabs/ProfileViewController.swift:

AWSMobileClient.default().showSignIn(navigationController: navigationController!, { (signInState, error) in
    if let signInState = signInState {
        print("Sign in flow completed: \(signInState)")
        DispatchQueue.main.async {
            self.setupViewController()
        } 
    }
    if let error = error as? AWSMobileClientError {
        print("error logging in: \(error.message)")
    }
})

After wiring in Auth in the frontend our sample shows the following login page when the app is started. Click on “Create an Account” and enter the details. A code will be sent to you the email you use for registration. Enter the code on the confirmation page. After sign up is complete, login.

Integrating the Amplify Storage library

Open the Podfile and add the following pod files under the target of your app:

pod 'Amplify'
pod 'AWSPluginsCore', 
pod 'AmplifyPlugins/AWSS3StoragePlugin'

Amplify iOS has a pluggable interface. For every category, we specify the provider explicitly. In this case, we are using Amazon S3. Add the following code to the AppDelegate.swift:

let storagePlugin = AWSS3StoragePlugin()
do {
    try Amplify.add(plugin: storagePlugin)
    try Amplify.configure()
} catch {
    print("Failed to initialize Amplify \(error)")
}

The call to Amplify.configure() reads the amplifyconfiguration.json file that contains the bucket and region information.

Uploading an image to the S3 bucket

The sample application has five options in the bottom menu, as shown in the following picture:

 

The first three are different access level folders, upload option is used to upload images, and profile option let’s you login/logout.

Let’s say you want to add code to upload an image with “Guest” access level to our S3 bucket. The upload code is invoked by choosing Upload –> Select image –> Next –> Select access level –> Share.

When you select Share, the following function under AmplifyStorageSampleApp/AddPhotoSharePhotoController.swift is called:


@objc func handleShare() {
        guard let image = selectedImage else { return }
        
        guard let uploadData = image.jpegData(compressionQuality: 0.5) else { return }
        
        navigationItem.rightBarButtonItem?.isEnabled = false

        let filename = NSUUID().uuidString

        let accessLevel = StorageAccessLevel.init(rawValue: segmentedControl.titleForSegment(at: segmentedControl.selectedSegmentIndex)!)
        let options = StorageUploadDataOperation.Request.Options(accessLevel: accessLevel!)

        var storageUploadDataOperation: StorageUploadDataOperation?
        storageUploadDataOperation = Amplify.Storage.uploadData(key: filename, data: uploadData, options: options) { (event) in
            switch event {
            case .completed(let completed):
                print("completed \(completed)")
                DispatchQueue.main.async {
                    self.dismiss(animated: true, completion: nil)

                    switch options.accessLevel {
                    case .guest:
                        NotificationCenter.default.post(name: SharePhotoController.updatePublicNotificationName, object: nil)
                    case .protected:
                        NotificationCenter.default.post(name: SharePhotoController.updateProtectedNotificationName, object: nil)
                    case .private:
                        NotificationCenter.default.post(name: SharePhotoController.updatePrivateNotificationName, object: nil)
                    }

                }

                storageUploadDataOperation?.removeListener()
                break
            case .failed(let storagePutError):
                print("failed: \(storagePutError)")
                DispatchQueue.main.async {
                    let errorDescription: String = storagePutError.errorDescription
                    self.errorMessageLabel.text = "\(errorDescription)"
                }
                storageUploadDataOperation?.removeListener()
                break
            case .inProcess(let progress):
                print("progress: \(progress)")
                DispatchQueue.main.async {
                    self.progressView.progress = Float(progress.fractionCompleted)
                }

            default:
                break
            }
        }
    }

Let’s walkthrough through the interesting snippets here.

The following code example sets the value of the variable uploadData to the image to be uploaded:

guard let uploadData = image.jpegData(compressionQuality: 0.5)

The following code example sets the name of the image that to be uploaded:

let filename = NSUUID().uuidString

The following code example picks the access level selected in the application when uploading the image:

let accessLevel = StorageAccessLevel.init(rawValue: segmentedControl.titleForSegment(at: segmentedControl.selectedSegmentIndex)!)

Use StorageUploadDataOperation class to define the options to be used when calling put. In this case, we are setting the access level that the user selects.

let options = StorageUploadDataOperation.Request.Options(accessLevel: accessLevel!)

Finally, call the uploadData function using the Amplify.Storage class. Pass it the key as the file name, the actual image, and the options.

storageUploadDataOperation = Amplify.Storage.uploadData(key: filename, data: uploadData, options: options) …

Listen for events once the operation is called and take appropriate action for different statuses of the upload process: completed, failed, or in-process.

When the upload is complete, download the data and display the image in our app.

The code for downloading images can be found under AmplifyStorageSampleApp/Utils/AmlifyImageView.swift and looks like the following code example:


import UIKit
import Amplify

var imageCache = [String: UIImage]()

class AmplifyImageView: UIImageView {
    
    var lastURLUsedToLoadImage: String?
    
    func loadImage(key: String, accessLevel: StorageAccessLevel, identityId: String?) {
        lastURLUsedToLoadImage = key
        
        self.image = nil
        
        if let cachedImage = imageCache[key] {
            self.image = cachedImage
            return
        }

        let options = StorageDownloadDataRequest.Options(accessLevel: accessLevel, targetIdentityId: identityId)
        var storageDownloadDataOperation: StorageDownloadDataOperation?
        storageDownloadDataOperation = Amplify.Storage.downloadData(key: key, options: options) { (event) in
            switch event {
            case .failed(let error):
                print("Failed \(error)")
                storageDownloadDataOperation?.removeListener()
            case .completed(let data):
                print("completed \(data)")
                let photoImage = UIImage(data: data)
                imageCache[key] = photoImage
                DispatchQueue.main.async {
                    self.image = photoImage
                }

                storageDownloadDataOperation?.removeListener()
            default:
                break
            }
        }
    }
}

The identity id passed to this function can be retrieved for private and protected access levels by calling the following function:

AWSMobileClient.default().identityId

The code can for this in our app can be found under Models/ItemCell.swift

Conclusion

In this post, you looked at the flow for building an iOS app with storage using Amplify iOS. You used the Amplify CLI to set up the backend and the Amplify iOS libraries for interacting with your backend resources. You can share this backend with different frontends. The sample app used in this post also contains code for using storage with private and protected access level. You can easily add additional capabilities such as Analytics, AI/ML, API, and DataStore to build real-time, scalable, and secure applications that seamlessly support online and offline scenarios.

Get started with Amplify iOS and Amplify Android today.

Feedback

We hope you like these new features! Let us know how we are doing, and submit any feedback in the Amplify iOS and Amplify Android GitHub repositories. You can read more about this feature on the Amplify Framework website. Also, checkout our community site to find the events, posts, and contributors to the Amplify community.