AWS AppSync
AWS AppSync Developer Guide

iOS App Tutorial

AWS AppSync integrates with the Apollo GraphQL client when building client applications. AWS provides plugins for offline support, authorization, and subscription handshaking to make this process easier. You can use the Apollo client directly, or with some client helpers provided in the AWS AppSync SDK when you get started.

This tutorial describes how to build an iOS application, including code generation for Swift types, by using AWS AppSync. For the latest AWS AppSync SDK documentation, see aws-mobile-appsync-sdk-ios on GitHub.

Download a Client Application

To show you how to use AWS AppSync, first we review an iOS application with a local array of data. Then we add AWS AppSync capabilities to it. Download a sample application that we'll use to add, update, and delete posts.

Understanding the iOS Sample App

The iOS sample app has three major files:

  • PostListViewController: The PostListViewController shows the list of posts available in the app. It uses a simple TableView to list all the posts. You can Add, Update, or Delete posts from this ViewController.

  • AddPostViewController: The AddPostViewController adds a new post into the list of existing posts. It gives a call to the delegate in PostListViewController to update the list of posts.

  • UpdatePostViewController: The UpdatePostViewController updates an existing post from the list of posts. It gives a call to the delegate in PostListViewController to update the values of existing posts.

Running the iOS Sample App

  1. Open the PostsApp.xcodeproj file from the download bundle, which you downloaded previously.

  2. Build the project (COMMAND+B) and ensure that it completes without error.

  3. Run the project (COMMAND+R) and try the Add, Update, and Delete (swipe left) operations on the post list.

Create an API

Before you get started, you'll need an API. For details, see Designing a GraphQL API. For this tutorial use the AWS Amplify CLI:

The AWS Amplify CLI is easy to install via NPM:

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

After you install the CLI, navigate to the iOS project root and initialize AWS Amplify in the new directory by running amplify init. After a few configuration questions, you can use amplify help at any time to see the overall command structure. You can use amplify help <category> to see actions for a specific category.

When you’re ready to add a feature, run amplify add <category> and answer the questions. For instance, in an Android application, try running amplify api add to have your application integrated with a serverless backend API built and hosted on AWS APIGateway or AWS AppSync.

Run the amplify api add command and select GraphQL to create a GraphQL API using AWS AppSync. The following example uses a single object with fields. When the CLI prompts you to edit the schema.graphql file, use the following schema:

type Post @model { id: ID! author: String! title: String content: String url: String ups: Int downs: Int version: Int! } type Query { singlePost(id: ID!): Post } schema { query: Query }

The schema has a single object called Post which has 8 fields in it. The object Post is annotated with @model annotation.

~/PostsApp $ amplify api add

Choose GraphQL for the service and API Key for the authorization type.

Note: Alternatively, you can use your own schema. To do this, go to the root of your iOS application. For example, if ~/PostsApp/ is the root of your iOS application, create a directory named graphql under ~/PostsApp/app/src/main/graphql/. Now create a directory structure that mimics the package name (for example, com.amazonaws.demo.posts) for the strongly typed generated code for your schema ~/PostsApp/app/src/main/graphql/com/amazonaws/demo/posts/. Create a file named posts.graphql and add the schema definition to it.

Check the Status of the Project

~/PostsApp $ amplify status | Category | Resource name | Operation | Provider plugin | | -------- | ---------------- | --------- | ----------------- | | Api | appsyncsampleapp | Create | awscloudformation |

Create Backend Resources

Now, run amplify push to build the GraphQL API and provision the resources in the cloud.

~/PostsApp $ amplify push

If you want to do more customization of GraphQL resolvers, see the Resolver Mapping Template Reference.

Configuration for the API

The amplify push operation retrieves the GraphQL endpoint URL (ApiUrl), AWS Region (Region) and the Authentication Mode selected (AuthMode), and adds it to the AppSync section of the awsconfiguration.json file at the root of your project. By default, this will be in root directory of your app unless you have overwritten it.

The contents of the file will be of the following format:

~/PostsApp $ cat awsconfiguration.json { "UserAgent": "aws-amplify/cli", "Version": "1.0", "IdentityManager": { "Default": {} }, "AppSync": { "Default": { "ApiUrl": "https://xxxxxYYYYYzzzzzAAAAAbbbbb.appsync-api.us-east-1.amazonaws.com/graphql", "Region": "us-east-1", "AuthMode": "API_KEY", "ApiKey": "da2-xxxxxYYYYYzzzzzAAAAAbbbbb" } } }

Code Generation for the GraphQL Operations

Run the amplify codegen to download the schema of the GraphQL API and generate the code.

Create Schema for Queries, Mutations, and Subscriptions

To interact with AWS AppSync, your client needs to define GraphQL queries and mutations.

Create a .graphql file inside the graphql folder of your app. For example, .graphql/posts.graphql

query GetPost($id:ID!) { getPost(id:$id) { id title author content url version } } query AllPosts { listPosts { items { id title author content url version ups downs } } } mutation AddPost($input: CreatePostInput!) { createPost(input: $input) { title author url content } } mutation UpdatePost($input: UpdatePostInput!) { updatePost(input: $input) { id author title content url version } } mutation DeletePost($input: DeletePostInput!) { deletePost(input: $input){ id title author url content } }

Download the Schema and Generate the Code

~/PostsApp $ amplify codegen add ? Enter the file name pattern of graphql queries, mutation and subscriptions graphql/**/*.graphql ✔ Downloaded the schema
~/PostsApp $ amplify codegen generate

Run the previous command to automatically generate code for the schema downloaded from AWS AppSync.

~/PostsApp $ ls API.swift PostsApp PostsApp.xcodeproj PostsApp.xcworkspace Podfile Podfile.lock Pods amplify awsconfiguration.json graphql

The API.swift file has the generated code for the schema that is created. Add the generated API.swift file into your Xcode project.

Add the SDK to Your App

Pod Setup

$ pod init

This creates a Podfile in the root directory of the project. We'll use this Podfile to declare dependency on the AWS AppSync SDK and other required components.

Open the Podfile and add the following lines in the application target:

target 'PostsApp' do use_frameworks! pod 'AWSAppSync', '~> 2.6.18' end

From the terminal, run the following command:

pod install --repo-update

This should create a file named PostsApp.xcworkspace. DO NOT open *.xcodeproj going forward. If it's open, you can close PostsApp.xcodeproj.

Open the PostsApp.xcworkspace with Xcode. Build the project (CMD + B) and ensure that it completes without error.

Create the AWSAppSync Client

You can use the awsconfiguration.json file to supply the configuration information required to create a AWSAppSyncClient object.

The AWSConfiguration represents the configuration information present in awsconfiguration.json file. By default, the information under Default section is used.

For authorization using the API key, use the following code to create an AWSAppSyncClient.

import UIKit import AWSAppSync @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? var appSyncClient: AWSAppSyncClient? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { // You can choose your database location, accessible by the SDK let databaseURL = URL(fileURLWithPath:NSTemporaryDirectory()).appendingPathComponent("my_DB") do { // Initialize the AWS AppSync configuration let appSyncConfig = try AWSAppSyncClientConfiguration(appSyncClientInfo: AWSAppSyncClientInfo(), databaseURL: databaseURL) // Initialize the AWS AppSync client appSyncClient = try AWSAppSyncClient(appSyncConfig: appSyncConfig) } catch { print("Error initializing appsync client. \(error)") } // ... other intercept methods } }

Perform GraphQL Operations

Query the Posts

Update the PostListViewController.swift file with the following code. The loadAllPosts function uses the fetch method in AWSAppSyncClient to make a call to fetch all the posts with the cache policy returnCacheDataAndFetch.

import UIKit import AWSAppSync class PostListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { var appSyncClient: AWSAppSyncClient? @IBOutlet weak var tableView: UITableView! var postList: [AllPostsQuery.Data.ListPost.Item?]? = [] { didSet { tableView.reloadData() } } func loadAllPosts() { appSyncClient?.fetch(query: AllPostsQuery(), cachePolicy: .returnCacheDataAndFetch) { (result, error) in if let error = error as? AWSAppSyncClientError { print("Error occurred: \(error.localizedDescription )") } self.postList = result?.data?.listPosts?.items } } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib let appDelegate = UIApplication.shared.delegate as! AppDelegate appSyncClient = appDelegate.appSyncClient // fetch all the posts from the backend and load it to postList loadAllPosts() } }

Mutate the Posts

Mutate the Posts (Add a Post)

Update the AddPostViewController.swift file with the following code. To add a new Post to the list of existing posts, create a CreatePostInput object with the required attributes. Now create the mutation object AddPostMutation that wraps the CreatePostInput. Use the perform method in AWSAppSyncClient to send the mutation to the AWS AppSync backend.

import Foundation import UIKit import AWSAppSync class AddPostViewController: UIViewController { var appSyncClient: AWSAppSyncClient? override func viewDidLoad() { super.viewDidLoad() // Grab a reference to the AppSync Client created in AppDelegate let appDelegate = UIApplication.shared.delegate as! AppDelegate appSyncClient = appDelegate.appSyncClient! } @IBAction func addNewPost(_ sender: Any) { // Create a GraphQL mutation based on a mutation input let mutationInput = CreatePostInput(id: 1001, author: "Tom", title: "Hamburger on sale!", content: "Grab a Hamburger for $3.99 only", version: 1) let mutation = AddPostMutation(input: mutationInput) appSyncClient?.perform(mutation: mutation) { (result, error) in if let error = error as? AWSAppSyncClientError { print("Error occurred: \(error.localizedDescription )") } if let resultError = result?.errors { print("Error saving the item on server: \(resultError)") return } } } }

Mutate the Posts (Update a Post)

import Foundation import UIKit import AWSAppSync class UpdatePostViewController: UIViewController { var appSyncClient: AWSAppSyncClient? override func viewDidLoad() { super.viewDidLoad() let appDelegate = UIApplication.shared.delegate as! AppDelegate appSyncClient = appDelegate.appSyncClient! } @IBAction func updatePost(_ sender: Any) { let mutationInput = UpdatePostInput(id: 1001, author: "Tom", title: "Salmon on sale!", content: "Grab a Salmon for $5.99 per pound!!!", version: 2) let updatePostMutation = UpdatePostMutation(input: updatePostInput!) appSyncClient?.perform(mutation: updatePostMutation!) { (result, error) in if let error = error as? AWSAppSyncClientError { print("Error occurred while making request: \(error.localizedDescription )") return } if let resultError = result?.errors { print("Error saving the item on server: \(resultError)") return } } } }

Authentication Modes

When making calls to AWS AppSync, there are several ways to authenticate those calls. The API key authorization (API_KEY) is the simplest way to onboard, but we recommend you use either Amazon IAM (AWS_IAM) or Amazon Cognito UserPools (AMAZON_COGNITO_USER_POOLS) or any OpenID Connect Provider (OPENID_CONNECT) after you onboard with an API key.

API Key

For authorization using the API key, update the awsconfiguration.json file and code snippet as follows:

Configuration

Add the following snippet to your awsconfiguration.json file.

{ "AppSync": { "Default": { "ApiUrl": "YOUR-GRAPHQL-ENDPOINT", "Region": "us-east-1", "ApiKey": "YOUR-API-KEY", "AuthMode": "API_KEY" } } }

Code

Add the following code to use the information in the Default section from awsconfiguration.json file.

// You can choose your database location, accessible by the SDK let databaseURL = URL(fileURLWithPath:NSTemporaryDirectory()).appendingPathComponent("my_DB") do { // Initialize the AWS AppSync configuration let appSyncConfig = try AWSAppSyncClientConfiguration(appSyncClientInfo: AWSAppSyncClientInfo(), databaseURL: databaseURL) // Initialize the AWS AppSync client appSyncClient = try AWSAppSyncClient(appSyncConfig: appSyncConfig) } catch { print("Error initializing appsync client. \(error)") }

AWS IAM

For authorization using the Amazon IAM credentials using Amazon IAM or Amazon STS or Amazon Cognito, update the awsconfiguration.json file and code snippet as follows:

Configuration

Add the following snippet to your awsconfiguration.json file.

{ "CredentialsProvider": { "CognitoIdentity": { "Default": { "PoolId": "YOUR-COGNITO-IDENTITY-POOLID", "Region": "us-east-1" } } }, "AppSync": { "Default": { "ApiUrl": "YOUR-GRAPHQL-ENDPOINT", "Region": "us-east-1", "AuthMode": "AWS_IAM" } } }

Code

Add the following code to use the information in the Default section from awsconfiguration.json file.

// Set up the Amazon Cognito CredentialsProvider let credentialsProvider = AWSCognitoCredentialsProvider(regionType: CognitoIdentityRegion, identityPoolId: CognitoIdentityPoolId) // You can choose your database location, accessible by the SDK let databaseURL = URL(fileURLWithPath:NSTemporaryDirectory()).appendingPathComponent("my_DB") do { // Initialize the AWS AppSync configuration let appSyncConfig = try AWSAppSyncClientConfiguration(appSyncClientInfo: AWSAppSyncClientInfo(), credentialsProvider: credentialsProvider, databaseURL: databaseURL) // Initialize the AWS AppSync client appSyncClient = try AWSAppSyncClient(appSyncConfig: appSyncConfig) } catch { print("Error initializing appsync client. \(error)") }

Amazon Cognito User Pools

Follow the instructions in Setup Email and Password based SignIn to configure Amazon Cognito user pools as an identity provider to your app.

For authorization using the Amazon Cognito user pools, update the awsconfiguration.json file and code snippet as follows:

Configuration

Add the following snippet to your awsconfiguration.json file.

{ "CognitoUserPool": { "Default": { "PoolId": "POOL-ID", "AppClientId": "APP-CLIENT-ID", "AppClientSecret": "APP-CLIENT-SECRET", "Region": "us-east-1" } }, "AppSync": { "Default": { "ApiUrl": "YOUR-GRAPHQL-ENDPOINT", "Region": "us-east-1", "AuthMode": "AMAZON_COGNITO_USER_POOLS" } } }

Code

import AWSUserPoolsSignIn import AWSAppSync class MyCognitoUserPoolsAuthProvider: AWSCognitoUserPoolsAuthProvider { func getLatestAuthToken() -> String { var token: String? = nil AWSCognitoUserPoolsSignInProvider.sharedInstance().getUserPool().currentUser()?.getSession().continueOnSuccessWith(block: { (task) -> Any? in token = task.result!.idToken!.tokenString return nil }).waitUntilFinished() return token! } }
// You can choose your database location, accessible by the SDK let databaseURL = URL(fileURLWithPath:NSTemporaryDirectory()).appendingPathComponent("my_DB") do { // Initialize the AWS AppSync configuration let appSyncConfig = try AWSAppSyncClientConfiguration(appSyncClientInfo: AWSAppSyncClientInfo(), userPoolsAuthProvider: MyCognitoUserPoolsAuthProvider(), databaseURL:databaseURL) // Initialize the AWS AppSync client appSyncClient = try AWSAppSyncClient(appSyncConfig: appSyncConfig) } catch { print("Error initializing appsync client. \(error)") }

OIDC (OpenID Connect)

For authorization using any OIDC (OpenID Connect) Identity Provider, update the awsconfiguration.json file and code snippet as follows:

Configuration

Add the following snippet to your awsconfiguration.json file.

{ "AppSync": { "Default": { "ApiUrl": "YOUR-GRAPHQL-ENDPOINT", "Region": "us-east-1", "AuthMode": "OPENID_CONNECT" } } }

Code

Add the following code to use the information in the Default section from awsconfiguration.json file.

class MyOidcProvider: AWSOIDCAuthProvider { func getLatestAuthToken() -> String { return "token" } } // You can choose your database location, accessible by the SDK let databaseURL = URL(fileURLWithPath:NSTemporaryDirectory()).appendingPathComponent("my_DB") do { // Initialize the AWS AppSync configuration let appSyncConfig = try AWSAppSyncClientConfiguration(appSyncClientInfo: AWSAppSyncClientInfo(), oidcAuthProvider: MyOidcProvider(), databaseURL:databaseURL) // Initialize the AWS AppSync client appSyncClient = try AWSAppSyncClient(appSyncConfig: appSyncConfig) } catch { print("Error initializing appsync client. \(error)") }

Optimistic Updates

For optimistic updates, create the data expected to be returned after the mutation. The optimistic updates are written to the persistent SQL store. Use the optimisticUpdate parameter of the perform method in the AWSAppSyncClient to perform an optimistic update.

import Foundation import UIKit import AWSAppSync class AddPostViewController: UIViewController { var appSyncClient: AWSAppSyncClient? override func viewDidLoad() { super.viewDidLoad() let appDelegate = UIApplication.shared.delegate as! AppDelegate appSyncClient = appDelegate.appSyncClient! } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated } @IBAction func addNewPost(_ sender: Any) { // Create a GraphQL mutation let mutationInput = CreatePostInput(id: 1002, author: "Eric Jones", title: "Vegan Icecream", content: "Vegan Icecream on sale for $1/oz", version: 1) let mutation = AddPostMutation(input: mutationInput) appSyncClient?.perform(mutation: mutation, optimisticUpdate: { (transaction) in do { // Update our normalized local store immediately for a responsive UI try transaction?.update(query: AllPostsQuery()) { (data: inout AllPostsQuery.Data) in data.listPosts?.items?.append(AllPostsQuery.Data.ListPost.Item.init(id: uniqueId, title: mutationInput.title!, author: mutationInput.author, content: mutationInput.content!, version: 0)) } } catch { print("Error updating the cache with optimistic response.") } }) { (result, error) in if let error = error as? AWSAppSyncClientError { print("Error occurred: \(error.localizedDescription )") return } // Remove local object from cache. let _ = self.appSyncClient?.store?.withinReadWriteTransaction { transaction in try? transaction.update(query: AllPostsQuery(), { (data: inout AllPostsQuery.Data) in guard let items = data.listPosts?.items else { return } var pos = -1 var counter = 0 for post in items { if post?.id == uniqueId { pos = counter continue } counter += 1 } if pos != -1 { data.listPosts?.items?.remove(at: pos) } }) } } } }

Subscriptions

AWS AppSync and GraphQL use the concept of subscriptions to deliver real-time updates of data to the application. We have defined subscriptions on the events of NewPost, UpdatePost, and DeletePost. This means we'll get a real-time notification if app data is changed from another device, and we can update our application UI based on the updates.

Subscriptions allow information to be pushed to the client when certain events happen, such as a new post being added.

Append to the file named graphql/posts.graphql as follows:

subscription OnCreatePost { onCreatePost { id author title content url } }

Now run amplify codegen generate --download to generate the code for the subscriptions in the graphql schema.

Add a real-time subscription to receive events on a new post that is added by anyone. In the PostListViewController.swift file, add the following function:

func startNewPostSubscription() { let subscription = OnCreatePostSubscription() do { _ = try appSyncClient?.subscribe(subscription: subscription, resultHandler: { (result, transaction, error) in if let result = result { // Store a reference to the new object let newPost = result.data!.onCreatePost! // Create a new object for the desired query, where the new object content should reside let postToAdd = AllPostsQuery.Data.ListPost.Item(id: newPost.id, title: newPost.title, author: newPost.author, content: newPost.content, url: newPost.url, version: 1) do { // Update the local store with the newly received data try transaction?.update(query: AllPostsQuery()) { (data: inout AllPostsQuery.Data) in data.listPosts?.items?.append(postToAdd) } self.loadAllPostsFromCache() } catch { print("Error updating store") } } else if let error = error { print(error.localizedDescription) } }) } catch { print("Error starting subscription.") } }

Next, call the method we created from the viewDidLoad method of PostListViewController. This updates the list of posts every time a new post is added from any client.

Complex Objects

Many times you might want to create logical objects that have more complex data, such as images or videos, as part of their structure. For example, you might create a Person type with a profile picture or a Post type that has an associated image. You can use AWS AppSync to model these as GraphQL types. If any of your mutations have a variable with bucket, key, region, mimeType, and localUri fields, the SDK uploads the file to Amazon S3 for you.

Update your schema as follows to add the S3Object and S3ObjectInput types for the file, and a new mutation named CreatePostWithFileInputMutation:

input CreatePostInput { author: String! title: String content: String url: String ups: Int downs: Int version: Int! } input CreatePostWithFileInput { author: String! title: String content: String url: String ups: Int downs: Int file: S3ObjectInput! version: Int! } input DeletePostInput { id: ID! } type Mutation { createPost(input: CreatePostInput!): Post createPostWithFile(input: CreatePostWithFileInput!): Post updatePost(input: UpdatePostInput!): Post deletePost(input: DeletePostInput!): Post } type Post { id: ID! author: String! title: String content: String url: String ups: Int downs: Int file: S3Object version: Int! } type PostConnection { items: [Post] nextToken: String } type Query { singlePost(id: ID!): Post getPost(id: ID!): Post listPosts(filter: TablePostFilterInput, limit: Int, nextToken: String): PostConnection } type S3Object { bucket: String! key: String! region: String! } input S3ObjectInput { bucket: String! key: String! region: String! localUri: String! mimeType: String! } type Subscription { onCreatePost( id: ID, author: String, title: String, content: String, url: String ): Post @aws_subscribe(mutations: ["createPost"]) onUpdatePost( id: ID, author: String, title: String, content: String, url: String ): Post @aws_subscribe(mutations: ["updatePost"]) onDeletePost( id: ID, author: String, title: String, content: String, url: String ): Post @aws_subscribe(mutations: ["deletePost"]) } input TableBooleanFilterInput { ne: Boolean eq: Boolean } input TableFloatFilterInput { ne: Float eq: Float le: Float lt: Float ge: Float gt: Float contains: Float notContains: Float between: [Float] } input TableIDFilterInput { ne: ID eq: ID le: ID lt: ID ge: ID gt: ID contains: ID notContains: ID between: [ID] beginsWith: ID } input TableIntFilterInput { ne: Int eq: Int le: Int lt: Int ge: Int gt: Int contains: Int notContains: Int between: [Int] } input TablePostFilterInput { id: TableIDFilterInput author: TableStringFilterInput title: TableStringFilterInput content: TableStringFilterInput url: TableStringFilterInput ups: TableIntFilterInput downs: TableIntFilterInput version: TableIntFilterInput } input TableStringFilterInput { ne: String eq: String le: String lt: String ge: String gt: String contains: String notContains: String between: [String] beginsWith: String } input UpdatePostInput { id: ID! author: String title: String content: String url: String ups: Int downs: Int version: Int } schema { query: Query mutation: Mutation subscription: Subscription }

Note: If you're using the sample schema specified at the start of this documentation, you can replace your schema with the previous schema.

Next, you need to add a resolver for createPostWithFile mutation. You can do that from the AWS AppSync console by selecting PostsTable as the data source and the following mapping templates.

Request Mapping Template

{ "version": "2017-02-28", "operation": "PutItem", "key": { "id": $util.dynamodb.toDynamoDBJson($util.autoId()), }, #set( $attribs = $util.dynamodb.toMapValues($ctx.args.input) ) #if($util.isNull($ctx.args.input.file.version)) #set( $attribs.file = $util.dynamodb.toS3Object($ctx.args.input.file.key, $ctx.args.input.file.bucket, $ctx.args.input.file.region)) #else #set( $attribs.file = $util.dynamodb.toS3Object($ctx.args.input.file.key, $ctx.args.input.file.bucket, $ctx.args.input.file.region, $ctx.args.input.file.version)) #end "attributeValues": $util.toJson($attribs), "condition": { "expression": "attribute_not_exists(#id)", "expressionNames": { "#id": "id", }, }, }

Response Mapping Template

$util.toJson($context.result)

After you have a resolver for the mutation, to ensure that our S3 Complex Object details are fetched correctly during any query operation, add a resolver for the file field of Post. You can do that from the AWS AppSync console by using the following mapping templates.

Request Mapping Template

{ "version" : "2017-02-28", "operation" : "Query", "query" : { ## Provide a query expression. ** "expression": "id = :id", "expressionValues" : { ":id" : { "S" : "${ctx.args.id}" } } } }

Response Mapping Template

$util.toJson($util.dynamodb.fromS3ObjectJson($context.source.file))

The AWS AppSync SDK doesn't take a direct dependency on the AWS SDK for iOS for Amazon S3, but takes in AWSS3TransferUtility and AWSS3PresignedURLClient clients as part of AWSAppSyncClientConfiguration. The code generator used above for generating the API generates the Amazon S3 wrappers required to use the previous clients in the client code. To generate the wrappers, pass the --add-s3-wrapper flag while running the code generator tool. You also need to take a dependency on the AWSS3 SDK. You can do that by updating your Podfile to the following:

target 'PostsApp' do use_frameworks! pod 'AWSAppSync' ~> '2.6.18' pod 'AWSS3' ~> '2.6.27' end

Then run pod install to fetch the new dependency.

Download the updated schema.json from the and put it in the GraphQLOperations folder in the root of the app.

Next, you have to add the new mutation, which is used to perform S3 uploads as part of mutation. Add the following mutation operation in your posts.graphql file:

mutation AddPostWithFile($input: CreatePostWithFileInput!) { createPostWithFile(input: $input) { id title author url content ups downs version file { ...S3Object } } } fragment S3Object on S3Object { bucket key region }

After adding the new mutation in our operations file, we run the code generator again with the new schema to generate mutations that support file uploads. This time, we also pass the -add-s3-wrapper flag, as follows:

aws-appsync-codegen generate GraphQLOperations/posts.graphql --schema GraphQLOperations/schema.json --output API.swift --add-s3-wrapper

Update the AWSAppSyncClientConfiguration object to provide the AWSS3TransferUtility client for managing the uploads and downloads:

let appSyncConfig = try AWSAppSyncClientConfiguration(url: AppSyncEndpointURL, serviceRegion: AppSyncRegion, credentialsProvider: credentialsProvider, databaseURL:databaseURL, s3ObjectManager: AWSS3TransferUtility.default())

The mutation operation doesn't require any specific changes in method signature. It requires only an S3ObjectInput with bucket, key, region, localUri, and mimeType. Now when you do a mutation, it automatically uploads the specified file to Amazon S3 using the AWSS3TransferUtility client internally.

Conflict Resolution

When clients make a mutation, either online or offline, they can send a version number with the payload (named expectedVersion) for AWS AppSync to check before writing to Amazon DynamoDB. You can configure a DynamoDB resolver mapping template to perform conflict resolution in the cloud. For more information, see Resolver Mapping Template Reference for DynamoDB. If the service determines it needs to reject the mutation, data is sent to the client. You can optionally run an additional callback to perform client-side conflict resolution.

For example, suppose you had a mutation with DynamoDB set for checking the version, and the client sent expectedVersion:0, as in the following example:

@IBAction func updatePost(_ sender: Any) { let updatePostMutation = UpdatePostMutation(id: "1", author: "Mr. Abc", content: "UpdatedContent", expectedVersion: 0) appSyncClient?.perform(mutation: updatePostMutation) { (result, error) in if let error = error as? AWSAppSyncClientError { print("Error occurred while making request: \(error.localizedDescription )") return } if let resultError = result?.errors { print("Error saving the item on server: \(resultError)") return } self.dismiss(animated: true, completion: nil) } }

This would fail the version check because 0 would be lower than any of the current values. You can then define a custom callback conflict resolver. A custom conflict resolver will receive the following variables:

appSyncClient?.perform(mutation: updatePostMutation, conflictResolutionBlock: { (serverState, taskCompletionSource, result) in // Conflict resolution block gets a callback here let snapshot = UpdatePostMutation.Data.UpdatePost(snapshot: serverState!) print("Server version is: \(snapshot.version)") let updateMutation = UpdatePostMutation(id: "1", author: "Mr. Abc", content: "UpdatedContent", expectedVersion: snapshot.version) // This would retry the specified `updateMutation` before processing any other queued mutations taskCompletionSource?.set(result: updateMutation) }, resultHandler: { (result, error) in if let error = error as? AWSAppSyncClientError { print("Error occurred while making request: \(error.localizedDescription )") return } if let resultError = result?.errors { print("Error saving the item on server: \(resultError)") return } self.dismiss(animated: true, completion: nil) })