Build an iOS Application
Create a simple iOS application using AWS Amplify
Module 4: Add a GraphQL API and Database
In this module, you will use the Amplify CLI and libraries to configure and add a GraphQL API to your app
Overview
The app we will be building is a note-taking app that allows users to create, delete, and list notes. This example gives you a good idea of how to build many popular types of CRUD+L (create, read, update, delete, and list) applications.
What you will accomplish
- Create and deploy a GraphQL API
- Write frontend code to interact with the API
Key concepts
API – Provides a programming interface that allows communication and interactions between multiple software intermediaries.
GraphQL – A query language and server-side API implementation based on a typed representation of your application. This API representation is declared using a schema based on the GraphQL type system. (To learn more about GraphQL, visit this page.)
Time to complete
20 minutes
Services used
Implementation
Create a GraphQL API service and a database
To create the GraphQL API and its backing database, open a terminal and run this command from your project directory:
amplify add api
? Please select from one of the below mentioned services: select GraphQL and press enter
? Provide API name: select the default, press enter
? Choose the default authorization type for the API: use the arrow key to select Amazon Cognito User Pool and press enter
? Do you want to configure advanced settings for the GraphQL API: select the default No, I am done and press enter
? Do you have an annotated GraphQL schema? keep the default N and press enter
? What best describes your project: choose any model, we are going to replace it with our own anyway. Press enter
? Do you want to edit the schema now? type Y and press enter
The default text editor that you chose when you initialized the project (amplify init) opens with a prebuild data schema.
Delete the schema and replace it with our app GraphQL schema.
type NoteData
@model
@auth (rules: [ { allow: owner } ]) {
id: ID!
name: String!
description: String
image: String
}
After a few seconds, you should see a success message:
GraphQL schema compiled successfully.
Generate client-side code
Based on the GraphQL data model definition we just created, Amplify generates client-side code (specifically, Swift code) to represent the data in our app.
To generate the code, run the following command in your terminal.
amplify codegen models
This creates Swift files in the amplify/generated/models directory, as you can see with:
➜ iOS Getting Started git:(master) ✗ ls -al amplify/generated/models
total 24
drwxr-xr-x 5 stormacq admin 160 Jul 9 14:20 .
drwxr-xr-x 3 stormacq admin 96 Jul 9 14:20 ..
-rw-r--r-- 1 stormacq admin 380 Jul 9 14:20 AmplifyModels.swift
-rw-r--r-- 1 stormacq admin 822 Jul 9 14:20 NoteData+Schema.swift
-rw-r--r-- 1 stormacq admin 445 Jul 9 14:20 NoteData.swift
Deploy the API service and database
To deploy the backend API and database we have just created, go to your terminal and run this command:
amplify push
# press Y when asked to continue
? Are you sure you want to continue? accept the default Y and press enter
? Do you want to generate code for your newly created GraphQL API type N and press enter
After a few minutes, you should see a success message:
✔ All resources are updated in the cloud
GraphQL endpoint: https://yourid.appsync-api.eu-central-1.amazonaws.com/graphql
Add API client library to the Xcode project
Before going to the code, you add the Amplify API library to the dependencies of your project. Navigate back to the General tab of your target and select AWSAPIPlugin and then choose Add.
Initialize Amplify libraries at runtime
Return to Xcode and open Backend.swift. Add a line in the Amplify initialization sequence in the private init() method. The code block should look like this:
// at top of file
import AWSAPIPlugin
// initialize amplify
do {
try Amplify.add(plugin: AWSCognitoAuthPlugin())
try Amplify.add(plugin: AWSAPIPlugin(modelRegistration: AmplifyModels()))
try Amplify.configure()
print("Initialized Amplify")
} catch {
print("Could not initialize Amplify: \(error)")
}
Add bridging between GraphQL data model and app model
Our project already has a data model to represent a Note. For this tutorial, we made a design decision to continue to use that model and provide an easy way to convert a NoteData to a Note.
Open ContentView.swift and add this initializer in the Note class.
convenience init(from data: NoteData) {
self.init(id: data.id, name: data.name, description: data.description, image: data.image)
// store API object for easy retrieval later
self._data = data
}
fileprivate var _data : NoteData?
// access the privately stored NoteData or build one if we don't have one.
var data : NoteData {
if (_data == nil) {
_data = NoteData(id: self.id,
name: self.name,
description: self.description,
image: self.imageName)
}
return _data!
}
Add API CRUD methods to the backend class
We will now add three methods to call our API: a method to query the Note, a method to create a new Note, and a method to delete a Note. Notice that these methods work on the app data model (Note) to make it easy to interract from the user interface. These methods transparently convert Note to GraphQL's NoteData objects.
Open the Backend.swift file and add the following snippet at the end of Backend class:
// MARK: API Access
func queryNotes() {
Task {
do {
let result = try await Amplify.API.query(request: .list(Transaction.self))
switch result {
case .success(let notesData):
print("Successfully retrieved list of Notes")
// convert an array of NoteData to an array of Note class instances
for n in notesData {
let note = Note.init(from: n)
MainActor.run {
UserData.shared.notes.append(note)
}
}
case .failure(let error):
print("Can not retrieve result : error \(error.errorDescription)")
}
} catch {
print("Can not retrieve Notes : error \(error)")
}
}
}
func createNote(note: Note) {
Task {
do {
// use note.data to access the NoteData instance
let result = try await Amplify.API.mutate(request: .create(note.data))
switch result {
case .success(let data):
print("Successfully created note: \(data)")
case .failure(let error):
print("Got failed result with \(error.errorDescription)")
}
}
} catch {
print("Got failed result with error \(error)")
}
}
}
func deleteNote(note: Note) {
// use note.data to access the NoteData instance
Task {
do {
let result = try await Amplify.API.mutate(request: .delete(note.data))
switch result {
case .success(let data):
print("Successfully deleted note: \(data)")
case .failure(let error):
print("Got failed result with \(error.errorDescription)")
}
} catch {
print("Got failed result with error \(error)")
}
}
}
Finally, we must call the API to query the list of Note for the currently signed-in user when the application starts. Add this piece of code in the backend's private init() method:
Task {
do {
let session = try await Amplify.Auth.fetchAuthSession()
// let's update UserData and the UI
await self.updateUserData(withSignInStatus: session.isSignedIn)
} catch {
print("Fetch auth session failed with error - \(error)")
}
}
In the same Backend.swift file, update the updateUserData(withSignInStatus:) method to look like this:
// change our internal state, this triggers an UI update on the main thread
func updateUserData(withSignInStatus status : Bool) async {
await MainActor.run {
let userData : UserData = .shared
userData.isSignedIn = status
// when user is signed in, query the database, otherwise empty our model
if status {
self.queryNotes()
} else {
userData.notes = []
}
}
}
Now, all that is left is to create a UI component for creating a new note and deleting a note from the list.
Add an edit button to add note
Now that the backend and data model pieces are in place, the last step in this section is to allow users to create a new note and to delete notes.
In Xcode, open ContentView.swift.
a. In ContentView struct, add state variables bound to the user interface.
// add at the begining of ContentView class
@State var showCreateNote = false
@State var name : String = "New Note"
@State var description : String = "This is a new note"
@State var image : String = "image"
b. Anywhere in the file, add a View struct to let the user create a new note.
struct AddNoteView: View {
@Binding var isPresented: Bool
var userData: UserData
@State var name : String = "New Note"
@State var description : String = "This is a new note"
@State var image : String = "image"
var body: some View {
Form {
Section(header: Text("TEXT")) {
TextField("Name", text: $name)
TextField("Name", text: $description)
}
Section(header: Text("PICTURE")) {
TextField("Name", text: $image)
}
Section {
Button(action: {
self.isPresented = false
let noteData = NoteData(id : UUID().uuidString,
name: self.$name.wrappedValue,
description: self.$description.wrappedValue)
let note = Note(from: noteData)
// asynchronously store the note (and assume it will succeed)
Backend.shared.createNote(note: note)
// add the new note in our userdata, this will refresh UI
self.userData.notes.append(note)
}) {
Text("Create this note")
}
}
}
}
}
c. Add a + button on the navigation bar to present a sheet for creating a note.
In ContentView struct, replace navigationBarItems(leading SignOutButton()) with
.navigationBarItems(leading: SignOutButton(),
trailing: Button(action: {
self.showCreateNote.toggle()
}) {
Image(systemName: "plus")
})
}.sheet(isPresented: $showCreateNote) {
AddNoteView(isPresented: self.$showCreateNote, userData: self.userData)
Add a swipe-to-delete behavior
Finally, in ContentView, add the swipe to delete behavior: add the .onDelete { } method to the ForEach struct:
ForEach(userData.notes) { note in
ListRow(note: note)
}.onDelete { indices in
indices.forEach {
// removing from user data will refresh UI
let note = self.userData.notes.remove(at: $0)
// asynchronously remove from database
Backend.shared.deleteNote(note: note)
}
}
Build and test
To verify everything works as expected, build and run the project. Select the Product menu and then select Run or press ⌘R. There should be no error.
Assuming you are still signed in, the app starts on the empty List. It now has a + button to add a note. Choose the + sign, choose Create this Note, and the note should appear in the list.
You can close the AddNoteView by pulling it down. Note that, on the iOS simulator, it is not possible to tap + a second time; you need to pull-to-refresh the List first.
You can delete the note by swiping a row left.
Here is the complete flow.
Conclusion
You have now created an iOS app. Using AWS Amplify, you added a GraphQL API and configured create, read, and delete functionality in your app.