Front-End Web & Mobile
Building a Cash Flow App with Amplify DataStore and SwiftUI
This article was written by Kyle Lee, Sr. Developer Advocate, AWS.
In May 2020, AWS launched the open-source Amplify Libraries for both iOS and Android, adding to AWS Amplify’s set of tools and services for mobile and front-end web developers. Amplify Libraries make it easy for developers to implement common features like authentication, data synchronization, file storage, and more.
In this post, you will learn how to build Native iOS apps with SwiftUI and Amplify DataStore, a library that focuses on providing a seamless experience for shared and distributed data that offers a simple solution to local persistence.
In this tutorial, you will be using SwiftUI and DataStore’s local persistence to create an app called Cash Flow. This will be a simple app that allows the user to keep track of their transactions by adding and subtracting inputted amounts to and from their account balance.
Topics we’ll be covering
- Configuring AWS Amplify
- Simple GraphQL data modeling
- Using Amplify DataStore as a persistence layer
- Create
- Read
- Update
Prerequisites
- Install Node.js version 10 or higher
- Install Xcode version 10.2 or later
- Install CocoaPods
- Install Amplify CLI version 4.21.0 or later
Building the App
Configuring the project
First, open Xcode and create a new Single View App.

Name the project whatever you would like; I will be using the name Cash-Flow.
Note: It is strongly recommended that your project name does not contain spaces as this could cause complications with Amplify as well as any other dependencies you decide to add to the project.

In this tutorial, we will be using SwiftUI.
Laying out the UI
If we examine the UI that we are trying to build, we can see that it is made up of several VStack views (shown in shades of blue) and an HStack view (shown in orange). We also have some Spacer views (shown in black) that help align our content towards the top of the screen.

Let’s start building this out in code, starting with each of the stacks views we will be using. In the body property of ContentView.swift, add the following:
var body: some View {
// 1
VStack {
// 2
Spacer()
.frame(height: 50)
// 3
VStack {
Text("Current Balance")
.fontWeight(.medium)
Text("$0.00")
.font(.system(size: 60))
}
// 4
Spacer()
}
}
- The
VStackthat will hold all the content on our screen. - A
Spacerthat adds 50px of space from the top of the screen. - The
VStackthat groups together the “Current Balance”Textview and anotherTextview that will be responsible for displaying the dynamic account balance. We are using “$0.00” as a placeholder for now. - Another
Spacerresponsible for pushing the content towards the top of the screen.

Next, let’s add the VStack that is responsible for holding the interactable views (TextField and two Button views). This will be below the VStack holding the Current Balance Text view value and above the bottom Spacer.
... // Current balance VStack
// 1
Spacer()
.frame(height: 150)
// 2
VStack(spacing: 20) {
// 3
VStack {
Text("Transaction Amount")
.fontWeight(.medium)
// 4
TextField("Amount", text: amountFormatterBinding)
.font(.largeTitle)
.multilineTextAlignment(.center)
.keyboardType(.decimalPad)
}
// 5
HStack(spacing: 50) {
Button(action: {}, label: {
Image(systemName: "plus")
.padding()
.background(Color.green)
.clipShape(Circle())
.font(.largeTitle)
.foregroundColor(.white)
})
Button(action: {}, label: {
Image(systemName: "minus")
.padding()
.background(Color.red)
.clipShape(Circle())
.font(.largeTitle)
.foregroundColor(.white)
})
}
}
... // Bottom Spacer
- Add some padding between the Current Balance
VStack. - We have setup a new
VStackthat is adding 20px of spacing between each of its children. In this case, theVStackonly has two children, anotherVStackand aHStack. - This
VStackwill group ourTextview andTextFieldtogether. - We are creating a
TextFieldthat will be binding its text to a property calledamountFormatterBindingwhich we have not defined yet. - The
HStackwill group our two circular buttons together. The green plus button will be used to add the amount to the balance and the red minus button will subtract the amount from the balance. We will provide the actions soon.
In step 3 above, we used a property called amountFormatterBinding which hasn’t been defined yet. amountFormatterBinding will be responsible for setting the entered text to a Double as well as getting the Double and converting that to a currency formatted String. This means we will need to have a stored @State property for our Double value as well as a NumberFormatter to handle the conversion between String and Double
Add the following at the above the body property in ContentView.swift:
... // struct ContentView
// 1
@State var amount: Double = 0.0
// 2
private let currencyFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
return formatter
}()
// 3
var amountFormatterBinding: Binding<String> {
Binding<String>(
get: {
self.currencyFormatter
.string(from: NSNumber(value: self.amount))
?? ""
},
set: { newAmount in
self.amount = self.currencyFormatter.number(from: newAmount)?
.doubleValue
?? 0
}
)
}
... // var body
- Our stored
@Stateproperty which will hold theDoublevalue of the amount to be applied towards the balance. (Note: This specific value will always be positive)\ - A
NumberFormatterconfigured to format numbers as currency. - We are creating a
Binding<String>by reading fromself.amount(defined in step 1) as a currency formattedStringand setting thenewAmountas a number from the currency formattedString.
Note: We are forced to manually create
amountFormatterBindingbecause we needamountto be updated as the user types, but using something likeTextField(_ title: StringProtocol, value: Binding<T>, formatter: Formatter)will only updateamountwhenever the user taps the Return key on the keyboard, which a.decimalPaddoes not have.
The UI layout is complete at this point:

Let’s finish up the basics by creating the add() and subtract() methods which can be set as the actions for our buttons. Below the body property and before the ContentView closing brace, add the following:
... // body closing }
// 1
func resetAmount() {
amount = 0
}
// 2
func add() {
resetAmount()
}
func subtract() {
resetAmount()
}
... // ContentView closing }
- This will be used to zero out the amount after it has been applied to the account balance.
- We have two very similar functions, one to add to the account balance and the other to subtract from it. After either action, we will need to reset the amount to avoid duplicated transactions.
Configure Dependencies
Now that we have our UI laid out, we can start adding DataStore to the app.
Let’s start by adding CocoaPods to our project. Open your terminal and run the following:
pod init
Now that our project contains a Podfile, go ahead and open it in your favorite editor and add the following pods:
pod 'Amplify'
pod 'Amplify/Tools'
pod 'AmplifyPlugins/AWSAPIPlugin'
pod 'AmplifyPlugins/AWSDataStorePlugin'
The entire file should look something like this:
platform :ios, '13.0'
target 'Cash-Flow' do
use_frameworks!
# Pods for Cash-Flow
pod 'Amplify'
pod 'Amplify/Tools'
pod 'AmplifyPlugins/AWSAPIPlugin'
pod 'AmplifyPlugins/AWSDataStorePlugin'
end
Keep in mind that the platform should be set to iOS 11 or higher or else Amplify won’t be able to properly install.
Next will will start the installation of the pods by running the following in the Terminal:
pod install --repo-update
Note: It is recommended that you close Xcode while the pods are installed.
Once the pods are installed, open up the .xcworkspace file. You can do this by running the following command in the Terminal:
xed .
Try building and running the project just to make sure everything is still working. Cmd + R
Next we will be adding a new run script to the project so that the Amplify Tools can run some magic in the background and configure our project for us.
Open up Build Phases

Create a new Run Script Phase

Drag the new Run Script Phase above the Compile Sources phase.

Rename the new Run Script Phase to “Run Amplify Tools“.
Add the following line to the “Run Amplify Tools” script area:
"${PODS_ROOT}/AmplifyTools/amplify-tools.sh"
Your Build Phases area should now look like this:

Now build the project. Cmd + B
Note: If you run into any errors during this build, try cleaning the project (
Cmd + Shift + K) and building again (Cmd + B).
Generating Account model
Now that our project is properly configured with Amplify, we can finally create our Account model.
When we built our project, Amplify Tools generated some files and folders for us, one of which is schema.graphql in the AmplifyConfig folder. (Cash-Flow > AmplifyConfig > schema.graphql)
Open the file and replace all contents of the file with the following:
type Account @model {
id: ID!
balance: Float!
}
This is the schema for our Account model. Let’s break this down a bit:
type– this defines ourAccountas an object, allowing a Swift Struct to be generated@model– signifies thatAccountshould be stored in a database tableID– the UUID for our object to distinguish between different objectsFloat– a floating point number which will be generated as a Swift Double!– indicates that this is a non-optional value (not necessarily force unwrapped though)
We are all set to generate our Account object. Navigate to amplifytools.xcconfig (Cash-Flow > AmplifyConfig > amplifytools.xcconfig) and update the following value:
modelgen=true
This will tell Amplify Tools that we want our model to be generated.
Build the project. Cmd + B
Note: Again, if errors occur during the build, clean the project (Cmd + Shift + K) then build again (Cmd + B).
Once we see the AmplifyModels folder generated in our Navigation Panel, we can let Amplify Tools know that we no longer need to generate models:
modelgen=false
Using DataStore
Before we can start using DataStore, we need to make sure we properly configure Amplify.
Navigate to AppDelegate.swift and add the following import statements at the top of the file:
import Amplify
import AmplifyPlugins
Now in the didFinishLaunchingWithOptions method, add the Amplify configuration code:
... // didFinishLaunchingWithOptions {
do {
// 1
try Amplify.add(plugin: AWSDataStorePlugin(modelRegistration: AmplifyModels()))
// 2
try Amplify.configure()
print("Amplify initialized successfully")
} catch {
print("Could not initialize Amplify \(error)")
}
... // return tru
- We need to initialize
AWSDataStorePlugin, passing in the generated models (Account). - We configure Amplify now that we have added the
AWSDataStorePluginplugin.
Run the app to make sure Amplify is configured successfully. Cmd + R
Once we have Amplify initialized in our project, we can start using DataStore. Let’s head back over to ContentView.swift.
Start by adding the import statement at the top of the file:
import Amplify
... // import SwiftUI
Earlier we set a placeholder value ($0.00) for the user’s account balance. We are going to replace that placeholder with a real balance. However, we need to keep in mind that Account.balance is of type Double and we need to have the displayed balance as a currency formatted String. We will create a computed property called balance that will handle this conversion for us.
Add the following towards the top of the ContentView:
... // @State var amount: Double = 0.0
// 1
@State var account: Account?
// 2
var balance: String {
let accountBalance = NSNumber(value: self.account?.balance ?? 0)
let currencyBalance = self.currencyFormatter.string(from: accountBalance)
return currencyBalance ?? "$0.00"
}
... // private let currencyFormatter: NumberFormatter = {
- We will be retrieving the
Accountobject from DataStore through an asynchronous operation so we need account to be optional as it will have no value when the view initially loads. - The
balancewill read fromaccount.balanceand convert the value to a currency formattedStringand will return our placeholder $0.00 if something goes wrong.
Now we can update the placeholder value ($0.00) that is currently being used by the “Current Balance” Text view:
... // Text("Current Balance")
Text(balance)
... // .font(.system(size: 60))
If we run the app now, we should see the same thing as before since the account balance default value is $0.00.
In order to populate the account balance with real data, we will need to create an Account object in DataStore. Let’s write a function that will handle the creation process for us:
... // body closing }
func createAccount() {
// 1
let account = Account(id: Self.currentUserId, balance: 0)
// 2
Amplify.DataStore.save(account) { result in
switch result {
case .success(let createdAccount):
print("Account created - \(createdAccount)")
self.account = createdAccount
case .failure(let error):
print("Could not create account - \(error)")
}
}
}
... // func resetAmount() {
- We start by creating an Account by using a constant
currentUserId(we haven’t defined this yet) for theidand a balance of 0. - Now we will attempt to save the newly created Account to DataStore. As long as the save is successful, we can set
self.accountto thecreatedAccount.
Let also define our constant that we will be using as the id of our Account:
... // ContentView closing }
extension ContentView {
fileprivate static let currentUserId = "currentUserId"
}
... // struct ContentView_Previews: PreviewProvider {
The createAccount() method is ready to go, but we only want to create an account if there isn’t one already. We should run a query once the ContentView appears that will check DataStore for our Account object and only create an Account if one doesn’t exist. Add the following method:
... // createAccount() closing }
func getAccount() {
// 1
Amplify.DataStore.query(Account.self, byId: Self.currentUserId) { (result) in
// 2
switch result {
case .success(let queriedAccount):
// 3
if let queriedAccount = queriedAccount {
print("Found account - \(queriedAccount)")
self.account = queriedAccount
} else {
print("No account found")
self.createAccount()
}
case .failure(let error):
print("Could not perform query for account - \(error)")
}
}
}
... // func resetAmount() {
- We query DataStore for an
Accountobject that has our constantcurrentUserId. - We then receive a result that determines whether the query itself was successful or not. This is not whether the object exists or not.
- If the query was successful, we can verify if there was an
Accountreturned or not. If there was an account, we setself.account; but if not, we create the account.
Now we add getAccount() to body.onAppear:
... // VStack closing }
.onAppear(perform: getAccount)
... // body closing }
If we run the app now, we should see “Account created” in the logs. If we run the app a second time, we should see “Found account” in the logs.
At this point, we are ready to start updating Account.balance and saving the changes to DataStore:
... // subtract() closing }
func updateBalance(to newBalance: Double) {
// 1
guard var account = self.account else { return }
// 2
account.balance = newBalance
// 3
Amplify.DataStore.save(account) { result in
// 4
switch result {
case .success(let updatedAccount):
print("Updated balance to - \(updatedAccount.balance)")
self.account = updatedAccount
case .failure(let error):
print("Could not update account - \(error)")
}
}
}
... // ContentView closing }
- Verify that
self.accounthas successfully been set by thegetAccount()method. - Update the local
account.balanceto thenewBalance. - Attempt to save the updated
Accountto DataStore - Check the result to see if the save was successful or not. If the
Accountwas saved, then we can set theupdatedAccounttoself.accountwhich is used to populate our UI.
Now we need to add the updateBalance(to:) method to our add() and subtract() methods, and we are good to go.
func add() {
let currentBalance = account?.balance ?? 0
// 1
let newBalance = currentBalance + amount
//2
updateBalance(to: newBalance)
resetAmount()
}
func subtract() {
let currentBalance = account?.balance ?? 0
// 1
let newBalance = currentBalance - amount
//2
updateBalance(to: newBalance)
resetAmount()
}
- Depending on which method is called, we will either add or subtract the amount from the unwrapped
currentBalance. - We then pass the
newBalancetoupdateBalance(to:)so the updated balance is saved.
Finally, update the Buttons to call the add and subtract methods:
// ...HStack(spacing: 50) {
Button(action: add, label: {
// ...})
Button(action: subtract, label: {
// ...Image(systemName: "minus")
Now we’re all done with the code ?
Run the code and give the app a try ?

Deploying to the cloud
So far the app is only working locally with the DataStore engine, so let’s deploy the API service to AWS.
To do so, first make sure you have the Amplify CLI installed and configured:
npm install @aws-amplify/cli
amplify configure
For a video walkthrough of how to install and configure the Amplify CLI, click here.
Next, initialize an Amplify project from the root of your Xcode project:
amplify init
? Enter a name for the environment: dev
? Choose your default editor: (your default text editor)
? Do you want to use an AWS profile? Yes
? Please choose the profile you want to use: (your AWS profile)
Once the project has finished initializing, deploy the back end by running the push command:
amplify push
? Are you sure you want to continue? Y
? Do you want to generate code for your newly created GraphQL API: N
Your back end has been deployed.
Next, update AppDelegate.swift to add the AWSAPIPlugin plugin:
// ... do {
try Amplify.add(plugin: AWSAPIPlugin(modelRegistration: AmplifyModels()))
// ... try Amplify.add(plugin: AWSDataStorePlugin(modelRegistration: AmplifyModels()))
Now, rebuild your app to sync to the cloud. CMD + B
When you make an update, the changes should now be synced to your back end.
You can view your app and database at any time in the Amplify Console by running the following command in the terminal:
amplify console
To learn more about Amplify DataStore, check out the documentation here. And to learn more about dedicated tools and services for mobile and front-end web developers visit AWS Amplify.