Getting Started / Hands-on / ...
Build an iOS Application
Create a simple iOS application using AWS Amplify
Module 5: Add the Ability to Store Images
In this module, you will add storage and the ability to associate an image with the notes in your app
Overview
Now that we have the notes app working, let's add the ability to associate an image with each note. In this module, you will use the Amplify CLI and libraries to create a storage service using Amazon S3. Finally, you will update the iOS app to enable image uploading, fetching, and rendering.
What you will accomplish
- Create a storage service
- Update your iOS app with logic to upload and download images
- Update the UI of your iOS app
Key concepts
Storage service – Storing and querying of files, such as images and videos, is a common requirement for applications. One option to do this is to Base64 encode the file and send it as a string to save in the database. This comes with disadvantages, such as the encoded file being larger than the original binary, the operation being computationally expensive, and the added complexity around encoding and decoding properly. Another option is to have a storage service specifically built and optimized for file storage. Storage services like Amazon S3 exist to make this as easy, performant, and inexpensive as possible.
Time to complete
10 minutes
Services used
Implementation
Create the storage service
To add image storage functionality, we will use the Amplify storage category.
amplify add storage
? Please select from one of the below mentioned services:,accept the default Content (Images, audio, video, etc.) and press enter
? Please provide a friendly name for your resource that will be used to label this category in the project: type image and press enter
? Please provide bucket name:, accept the default and press enter
? Who should have access:, accept the default Auth users only and press enter
? What kind of access do you want for Authenticated users? select all three options create/update, read, delete using the space and arrows keys, then press enter
? Do you want to add a Lambda Trigger for your S3 Bucket?, accept the default No and press enter
After a while, you should see:
Successfully added resource image locally
Deploy the storage service
To deploy the storage service we have just created, go to your terminal and run the following command:
amplify push
Press Y to confirm and, after a while, you should see:
✔ Successfully pulled backend environment amplify from the cloud.
Add Amplify storage libraries to the Xcode project
Before going to the code, add the Amplify storage library to the dependencies of your project. Navigate back to the General tab of your target and select AWSS3StoragePlugin and then choose Add.
Initialize Amplify storage plugin at runtime
Return to Xcode, open the Backend.swift file, and add a line in the Amplify initialization sequence in private init() method. The code block should look like this:
// at the top of the file
import AWSS3StoragePlugin
// initialize amplify
do {
try Amplify.add(plugin: AWSCognitoAuthPlugin())
try Amplify.add(plugin: AWSAPIPlugin(modelRegistration: AmplifyModels()))
try Amplify.add(plugin: AWSS3StoragePlugin())
try Amplify.configure()
print("Initialized Amplify");
} catch {
print("Could not initialize Amplify: \(error)")
}
Add image CRUD methods to the backend class
Open Backend.swift. Anywhere in the backend class, add the following methods:
// MARK: - Image Storage
func storeImage(name: String, image: Data) {
let options = StorageUploadDataRequest.Options(accessLevel: .private)
let uploadTask = Amplify.Storage.uploadData(
key: name,
data: image,
options: options
)
Task {
for await progress in await uploadTask.progress {
// optionally update a progress bar here
}
do {
let data = try await uploadTask.value
print("Image upload completed: \(data)")
} catch {
print("Image upload failed: \(error.errorDescription). \(error.recoverySuggestion)")
}
}
}
func retrieveImage(name: String, completed: @escaping (Data) -> Void) {
let options = StorageDownloadDataRequest.Options(accessLevel: .private)
let downloadTask = Amplify.Storage.downloadData(
key: name,
options: options
)
Task {
for await progress in await downloadTask.progress {
// optionally update a progress bar here
}
do {
let data = try await downloadTask.value
print("Image \(name) loaded")
completed(data)
} catch {
print("Can not download image: \(error.errorDescription). \(error.recoverySuggestion)")
}
}
}
func deleteImage(name: String) {
let options = StorageRemoveRequest.Options(accessLevel: .private)
Task {
do {
let key = try await Amplify.Storage.remove(key: name, options: options)
print("Image \(key) deleted")
} catch {
print("Can not delete image: \(error.errorDescription). \(error.recoverySuggestion)")
}
}
}
These three methods simply call their Amplify counterpart. Amplify storage has three file protection levels:
- Public – Accessible by all users
- Protected – Readable by all users, but only writable by the creating user
- Private – Readable and writable only by the creating user
Load image when data are retrieved from the API
Open ContentView.swift and update the Note's initializer.
// add a publishable's object property
@Published var image : Image?
// update init's code
convenience init(from data: NoteData) {
self.init(id: data.id, name: data.name, description: data.description, image: data.image)
if let name = self.imageName {
// asynchronously download the image
Backend.shared.retrieveImage(name: name) { (data) in
// update the UI on the main thread
MainActor.run {
let uim = UIImage(data: data)
self.image = Image(uiImage: uim!)
}
}
}
// store API object for easy retrieval later
self._data = data
}
Add UI code to capture an image
First, we add generic code to support image capture. This code can be reused in many applications; it shows an image selector that allows the user to choose an image from their image library.
In Xcode, create the new Swift file (⌘N, then select Swift). Name the file CaptureImageView.swift and add this code:
import Foundation
import UIKit
import SwiftUI
struct CaptureImageView {
/// MARK: - Properties
@Binding var isShown: Bool
@Binding var image: UIImage?
func makeCoordinator() -> Coordinator {
return Coordinator(isShown: $isShown, image: $image)
}
}
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
@Binding var isCoordinatorShown: Bool
@Binding var imageInCoordinator: UIImage?
init(isShown: Binding<Bool>, image: Binding<UIImage?>) {
_isCoordinatorShown = isShown
_imageInCoordinator = image
}
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let unwrapImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else { return }
imageInCoordinator = unwrapImage
isCoordinatorShown = false
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
isCoordinatorShown = false
}
}
extension CaptureImageView: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<CaptureImageView>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
// picker.sourceType = .camera // on real devices, you can capture image from the camera
// see https://medium.com/better-programming/how-to-pick-an-image-from-camera-or-photo-library-in-swiftui-a596a0a2ece
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController,
context: UIViewControllerRepresentableContext<CaptureImageView>) {
}
}
Store image when notes are created
Let's invoke the storage methods from the backend when a note is created. Open ContentView.swift and modify the AddNoteView to add an ImagePicker component.
// at the start of the Content View struct
@State var image : UIImage? // replace the previous declaration of image
@State var showCaptureImageView = false
// in the view, replace the existing PICTURE section
Section(header: Text("PICTURE")) {
VStack {
Button(action: {
self.showCaptureImageView.toggle()
}) {
Text("Choose photo")
}.sheet(isPresented: $showCaptureImageView) {
CaptureImageView(isShown: self.$showCaptureImageView, image: self.$image)
}
if (image != nil ) {
HStack {
Spacer()
Image(uiImage: image!)
.resizable()
.frame(width: 250, height: 200)
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 10)
Spacer()
}
}
}
}
Modify the Create Note section to store the image as well as the note.
Section {
Button(action: {
self.isPresented = false
let note = Note(id : UUID().uuidString,
name: self.$name.wrappedValue,
description: self.$description.wrappedValue)
if let i = self.image {
note.imageName = UUID().uuidString
note.image = Image(uiImage: i)
// asynchronously store the image (and assume it will work)
Backend.shared.storeImage(name: note.imageName!, image: (i.pngData())!)
}
// 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
withAnimation { self.userData.notes.append(note) }
}) {
Text("Create this note")
}
}
Build and test
To verify everything works as expected, build and run the project. Select the Product menu and select Run or press ⌘R. There should be no error.
Assuming you are still signed in, the app starts on the list with one note. Use the + sign again to create a note. This time, add a picture selected from the local image store.
Here is the complete flow.
Conclusion
In the last section, you will find instructions to reuse or to delete the backend we just created.
-
Share your Backend Between Multiple Projects
Amplify makes it easy to share a single backend between multiple front end applications.
In a terminal, navigate to your other project directory and execute the following command:
mkdir other-project cd other-project amplify pull ? Do you want to use an AWS profile? accept the default Yes and press enter ? Please choose the profile you want to use select the profile you want to use and press enter ? Which app are you working on? select the backend you want to share and press enter ? Choose your default editor: select you prefered text editor and press enter ? Choose the type of app that you're building select the operating system for your new project and press enter ? Do you plan on modifying this backend? most of the time, select No and press enter. All backend modifications can be done from the original iOS project.
After a few seconds, you will see the following message:
Added backend environment config object to your project. Run 'amplify pull' to sync upstream changes.
You can see the two configurations files that have been pulled out. When you answer 'Yes' to the question 'Do you plan on modifying this backend?', you also see a amplify directory
➜ other-project git:(master) ✗ ls -al total 24 drwxr-xr-x 5 stormacq admin 160 Jul 10 10:28 . drwxr-xr-x 19 stormacq admin 608 Jul 10 10:27 .. -rw-r--r-- 1 stormacq admin 315 Jul 10 10:28 .gitignore -rw-r--r-- 1 stormacq admin 3421 Jul 10 10:28 amplifyconfiguration.json -rw-r--r-- 1 stormacq admin 1897 Jul 10 10:28 awsconfiguration.json
-
Delete Your Backend
When creating a backend for a test or a prototype, or just for learning purposes, just like when you follow this tutorial, you want to delete the cloud resources that have been created.
Although the usage of this resources in the context of this tutorial fall under the free tier, it is a best practice to clean up unused resources in the cloud.
To clean your amplify project, in a terminal, execute the following command:
amplify delete
After a while, you will see the below message confirming all cloud resources have been deleted.
✔ Project deleted in the cloud Project deleted locally.
Congratulations!
You successfully built a web application on AWS! As a great next step, dive deeper into specific AWS technologies and take your application to the next level.