Front-End Web & Mobile
Rapid iOS App Prototyping with Amplify Admin UI and SwiftUI
June 27, 2024: This blog post covers Amplify Gen 1. For new Amplify apps, we recommend using Amplify Gen 2. You can learn more about Gen 2 in our launch blog post.
AWS Amplify now offers a new way to model your app’s data schemas that is easier than ever. The new Admin UI feature provides an easy-to-use interface where you can layout the models of your app, create relationships between them, and pull the Swift representation of those models into your codebase.
In this post, you will be using the Admin UI to prototype a room booking app that allows a user to sign in, view seeded available rooms, book one of those rooms, and see the bookings associated with that user. Since this is a prototype, we will be focusing on building the concept of the app and feign other features like authenticating the user and storing photos.
This tutorial will show you how to configure the models required for the prototype and start using them in a native iOS app built in SwiftUI.
Topics we’ll be covering
- Data modeling with Amplify Admin UI
- Configuring Amplify in a native iOS app
- Implement prototype functionality with the generated models
Prerequisites
- Install Xcode version 12.2 or later
- Install CocoaPods
- Sign up for an AWS account
- Install the Amplify CLI
Data modeling
Start off by heading over to https://sandbox.amplifyapp.com and clicking the “Get started” button under “Create an app backend”.
Choose “Data” as the feature to setup, select “Blank schema”, and click “Create new schema”.
Now that we have a blank data modeling canvas, we can start adding our models.
The prototype will allow different users to “sign in” by providing a username. Let’s add a User
model that will simply have an id
and username
.
With the username
field selected, select the “Is required” box on the right side of the screen.
Once the user is “signed in”, the first screen they will see is a list of rooms that are available to be booked. Add a Room
model.
Each of the properties for our Room
object will be required, so you will need to select the checkbox for each of the fields.
We also get to see that these schemas can support a lot of different types when we switch the type of price
to Int
.
Each of these properties will be represented by either a native Swift type or a type provided by the Amplify libraries, making our feature implementation feel native to the codebase.
Lastly, we need to create an object that represents the booking of a room by the user. Our app will show the user a list of their Booking
‘s which will include the data of the Room
itself. To keep each Booking
up to date with the associated Room
, we will establish a one-to-one relationship in our schema.
Each of the properties on the Booking
object will be required, including the relationship to the Room
.
Now that the data schema of our app is complete, navigate to the “Deploy” tab and log in to or sign up for an AWS Account.
Once you’re signed in, you will be prompted to create your app’s backend.
Enter an app name, select an AWS region, and click deploy. Amplify will begin creating our backend environment and prep all the resources to start using our models.
After a few minutes, you will see that the Amplify app has been created. Open the Admin UI to see an overview of the staging environment created for us.
Navigate to the “Data” section, and you will see that our model schemas have been added to this Amplify app.
At this point, I’d recommend that you review the schemas for typos, correct property type, and/or properties being marked as “Is required”.
Next, click “Local setup instructions”. You will be presented with a popup containing the terminal command to pull down your Amplify project, as well as code snippets on how to initialize our model object in Swift code.
Configuring Amplify Locally
Now it’s time to open up Xcode and create a new project for our app.
Once the Xcode project is created, open the terminal, navigate to the root of the Xcode project and run the command provided in the “Local setup instructions” of the Admin UI. It should look something like this:
If you run $ ls
at the root of your Xcode project, you should now see an amplify
folder as well as two configuration files: awsconfiguration.json
and amplifyconfiguration.json
. We will need to add the configuration files and the generated models inside the amplify folder to our Xcode project.
In the navigation pane of Xcode, right click the source code folder and select “Add files to project”.
Add both configuration files and the generated models folder at path/to/project/amplify/generated/models
. The navigation pane will now look like this:
All our files are ready and in place. Now we need to add the Amplify Libraries to our project by installing them with CocoaPods.
At the root of your Xcode project, run the following:
Replace the contents of the Podfile with this:
We are specifying that the platform is iOS 14 and that we will be adding three pods: Amplify, the API plugin, and the DataStore plugin.
Save the file and install the dependencies at the root of the Xcode project:
After the installation is complete, open the *.xcworkspace
file.
The
*.xcodeproj
needs to be closed so the workspace will automatically be opened or else it will appear as if the Xcode project is missing files.
In Xcode, navigate to the *App.swift
file where the @main
struct is implemented. This is where we will configure the Amplify Libraries.
At the top add the following import statements:
Now in the App struct itself, add a configureAmplify()
function:
Call configureAmplify()
in the init
method of the *App
object.
Run the app and you should see “Initialized Amplify” printed to your logs.
Implement Prototype Functionality
You’re now completely done setting up a backend that will support scalable, live data; giving you more time to focus on the app’s functionality.
The first screen of the room booking prototype will be the “login” screen. Here, a user will simply enter a username and tap a button to login.
Create a new file called LoginView.swift
and add the following:
- We will be using
Amplify
in ourViewModel
to handle the “login” logic. - The
ViewModel
is responsible for the state of our view. We are using an instance of aViewModel
class that is scoped to theLoginView
through anextension
- This view is responsible for sending the “logged in”
User
back up the chain. This is done with a closure that will be implemented back in the*App.swift
file. - When the “Login” button is tapped, it will run
ViewModel.login
. TheonLogin
closure is passed as an argument to send theUser
up the chain. - The
login
method is where we will implement the Amplify related logic.
The LoginView should now appear like this in the preview area:
The prototype will only require a username to login. To make it so a user can have bookings associated with their account, we will create a User
object for each username entered into the field. If the username has already been used, we can simply return the existing User
so they can log back in.
In order to simplify this process, we will be making the id
and username
the same value when creating a User
. This makes it easier to query a User
by id
.
Add the following to ViewModel.login
:
- Run a query to see if the
User
already exists and pass the queriedUser
to thecompletion
closure. - If the
User
doesn’t exist, create thatUser
and send it throughcompletion
.
Back in *App.swift
, make the LoginView
the main view in the body
:
This will initialize the LoginView
with a closure where the provided User
is passed to a SessionManager
object.
In a new file SessionManager.swift
, add this code:
This object will be responsible managing the logged in User
.
Back in *App.swift
, create an observed instance of SessionManager
.
Once the user has “logged in”, they will be presented with multiple room listings. They would also be able to tab over to their bookings where a similar listing would be displayed, but would include their booking dates.
Let’s create a view that can be used to show the details of available rooms as well as booked rooms in a file called RoomItemView.swift
:
- By allowing
bookingDates
to be optional,RoomItemView
can be used for both listings and bookings. - The
bookingDescription
will format thebookingDates
tuple into a human readable description of the booked dates. - If a
bookingDescription
can be generated, then it will be displayed. If not, we assume the view is presented in the context of a listing and should display the price per night.
Next, create RoomsView.swift
as the view responsible for showing the room listings.
- We’ll loop over each of the
Room
objects provided by theViewModel
and pass them into the destination and label. There will be an error presented sinceRoom
doesn’t conform toIdentifiable
at the moment. - The destination will be a
RoomDetailsView
that we will be implementing next. - Whenever the
RoomView
appears, we will query the database for the latestRoom
objects. getRooms()
will be responsible for querying DataStore for all theRoom
‘s.
Let’s implement the functionality of getRooms()
:
This is as simple as querying the Room's
and updating the ViewModel
with whatever comes back.
There are two errors we need to resolve at this point, Room
needs to conform to Identifiable
and RoomDetailsView
needs to be implemented.
In a new file named Room+Extensions.swift
add the following:
Any extensions on generated objects should take place in a separate file so the changes aren’t overwritten when updating any model schemas in the future.
Add the following to a new file named RoomDetailsView.swift
:
- RoomDetailsView will be passed the instance of the SessionManager created in the *App object as an EnvironmentObject.
- The SessionManager provides the currentUser which can be used to book a Room.
- The book(_:for:) method will be responsible for booking a Room for a specified User.
To book a room, we will create a new Booking
instance and save it to DataStore. Update the book(_:for:)
method.
The only view we’re missing is MyBookingsView
which will be responsible for showing the user the different Room
s they booked and for which dates.
- We will display a
RoomItemView
for eachBooking
provided by theViewModel
. Similar toRoom
,Booking
will need to conform toIdentifiable
to be used byForEach
. - Booking also doesn’t have a property called
bookingDates
which is a tuple of thecheckInDate
andcheckOutDate
. - Every time
MyBookingsView
appears, we will query DataStore for the latestBooking
‘s for the current user.
MyBookingsView
is simply showing RoomItemView
‘s the same way RoomsView
is; however, we need to provide the Room
from a booking that is associated with the current user and not queried directly on Room
itself. To accomplish getting the list of Room
‘s, we must perform a query on Booking
where the guestId
matches the current user ID.
Add the following to getBookings(for:)
:
This query provides a predicate that specifies that booking.guestId
matches user.id
and all results that come back should be sorted in ascending order based on the check in date. Then we update the bookings
of the ViewModel
so the list can be shown in the view.
To fix the compiler errors, create another file named Booking+Extensions.swift
with the following code:
Making Booking
conform to Identifiable
will allow each object to be managed by SwiftUI when iterating over them in a ForEach
. The bookingDates
property makes for a cleaner call site when passing the tuple of check in and check out dates; especially since those properties are Amplify wrappers around Foundation.Date
and not Foundation.Date
‘s themselves.
With that last view out of the way, we can update the *App
object to handle a user logging in and selecting a tab. Add this to the body
in *App.swift
:
When a User
is provided by LoginView
, the user will be displayed a tabbed view of available rooms and their bookings. Everything is all setup and ready to work, the problem right now is that there aren’t any Room
‘s to display or book. Since this is a prototype, we can simply seed some Room
‘s into our backend.
In a file called DataSeeder.swift
create a method that will seed some Room
‘s into DataStore.
Back in *App.swift
, create a function that will check if there are any Room
‘s already saved; if not, seed those Room
‘s.
Now call that method in the *App
initializer:
Now if you run the app, you should be able to “login”, view rooms as well as their details, book a room, and see the rooms booked by that account.
Conclusion
AWS Amplify is a powerful toolset that makes life easier for developers; whether that means building out a quick prototype, like we did here, or creating a large app with thousands of users. The new Admin UI makes it easier to visualize your model schema and create connections between different objects, saving time by making connections between objects more apparent and easy to understand. All of this shortens the time to get the backend configured and spend more time on the client facing app.
This was just a glimpse of what is possible with Amplify Admin UI; there’s still Authentication, Storage, Predictions, and much more that can be managed with the new interface. It looks like prototyping apps is only going to get easier from here ?