AWS の開始方法

iOS アプリケーションを構築する

AWS Amplify を使用してシンプルな iOS アプリケーションを作成する

モジュール 5: 画像保存機能を追加する

このモジュールでは、ストレージと、画像をアプリのメモに関連付ける機能を追加します。

はじめに

メモアプリが機能するようになったので、各メモに画像を関連付ける機能を追加しましょう。このモジュールでは、Amplify CLI およびライブラリを使用して、Amazon S3 を利用したストレージサービスを作成します。最後に、iOS アプリを更新して、画像のアップロード、取得、レンダリングができるようにします。

学習内容

  • ストレージサービスの作成
  • iOS アプリの更新 - 画像をアップロードおよびダウンロードするロジック
  • iOS アプリの更新 - ユーザーインターフェイス

主要な概念

ストレージサービス - 画像や動画などのファイルの保存とクエリは、ほとんどのアプリケーションの一般的な要件です。これを行う 1 つのオプションは、ファイルを Base64 エンコードし、文字列として送信してデータベースに保存することです。これには、エンコードされたファイルが元のバイナリよりも大きいこと、操作に計算コストがかかること、適切にエンコードおよびデコードすることの複雑さが増すなどの欠点があります。別のオプションは、ファイルストレージ用に特別に構築および最適化されたストレージサービスを用意することです。Amazon S3 のようなストレージサービスは、これを可能な限り簡単、高性能、低料金になるようにしています。

 所要時間

10 分

 使用するサービス

実装

  • ストレージサービスを作成する

    画像ストレージ機能を追加するには、Amplify ストレージカテゴリを使用します。

    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

    しばらくすると、以下の出力が表示されます。

    Successfully added resource image locally
  • ストレージサービスをデプロイする

    作成したストレージサービスをデプロイするには、ターミナルで次のコマンドを実行します。

    amplify push

    Y を押して確認すると、少し時間を置いて以下の出力が表示されます。

    ✔ Successfully pulled backend environment amplify from the cloud.
  • Amplify ストレージライブラリを Xcode プロジェクトに追加する

    コードに進む前に、Amplify ストレージライブラリをプロジェクトの依存関係に追加します。Podfile ファイルを開き、AmplifyPlugins/AWSS3StoragePlugin という文字列を含む行を追加するか、または次のファイル全体をコピーして貼り付けます。

    # you need at least version 13.0 for this tutorial, more recent versions are valid too
    platform :ios, '13.0'
    
    target 'getting started' do
      # Comment the next line if you don't want to use dynamic frameworks
      use_frameworks!
    
      # Pods for getting started
      pod 'Amplify', '~> 1.0'                             # required amplify dependency
      pod 'Amplify/Tools', '~> 1.0'                       # allows to call amplify CLI from within Xcode
    
      pod 'AmplifyPlugins/AWSCognitoAuthPlugin', '~> 1.0' # support for Cognito user authentication
      pod 'AmplifyPlugins/AWSAPIPlugin', '~> 1.0'         # support for GraphQL API
      pod 'AmplifyPlugins/AWSS3StoragePlugin', '~> 1.0'   # support for Amazon S3 storage
    
    end

    ターミナルでこのコマンドを実行します。

    pod install

    このコマンドは完了するまでにしばらく時間がかかります。次のように表示されます (実際のバージョン番号は異なる場合があります)。

    Analyzing dependencies
    Downloading dependencies
    Installing AWSS3 (2.14.2)
    Installing AmplifyPlugins 1.0.4
    Generating Pods project
    Integrating client project
    Pod installation complete! There are 5 dependencies from the Podfile and 12 total pods installed.
  • Amplify ストレージプラグインを実行時に初期化する

    Xcode に戻って Backend.swift を開き、private init() メソッドの Amplify 初期化シーケンスに 1 行を追加します。コードブロックが完成すると、以下のようになります。

    // 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)")
    }
  • 画像の CRUD メソッドを Backend クラスに追加する

    Backend.swift を開きます。Backend クラスの任意の場所に以下のメソッドを追加します。

    // MARK: - Image Storage
    
    func storeImage(name: String, image: Data) {
    
    //        let options = StorageUploadDataRequest.Options(accessLevel: .private)
        let _ = Amplify.Storage.uploadData(key: name, data: image,// options: options,
            progressListener: { progress in
                // optionlly update a progress bar here
            }, resultListener: { event in
                switch event {
                case .success(let data):
                    print("Image upload completed: \(data)")
                case .failure(let storageError):
                    print("Image upload failed: \(storageError.errorDescription). \(storageError.recoverySuggestion)")
            }
        })
    }
    
    func retrieveImage(name: String, completed: @escaping (Data) -> Void) {
        let _ = Amplify.Storage.downloadData(key: name,
            progressListener: { progress in
                // in case you want to monitor progress
            }, resultListener: { (event) in
                switch event {
                case let .success(data):
                    print("Image \(name) loaded")
                    completed(data)
                case let .failure(storageError):
                    print("Can not download image: \(storageError.errorDescription). \(storageError.recoverySuggestion)")
                }
            }
        )
    }
    
    func deleteImage(name: String) {
        let _ = Amplify.Storage.remove(key: name,
            resultListener: { (event) in
                switch event {
                case let .success(data):
                    print("Image \(data) deleted")
                case let .failure(storageError):
                    print("Can not delete image: \(storageError.errorDescription). \(storageError.recoverySuggestion)")
                }
            }
        )
    }
  • API からデータを取得するときに画像を読み込む

    バックエンド関数が利用できるようになったので、API コールが結果を返すときに画像を読み込みましょう。この動作は、API が返す NoteData から Note UI が構築されるタイミングで実行されるように追加します。

    ContentView.swift を開き、当該メモの初期化子を更新します (8 行目から 17 行目を追加)。

    // 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
                DispatchQueue.main.async() {
                    let uim = UIImage(data: data)
                    self.image = Image(uiImage: uim!)
                }
            }
        }
        // store API object for easy retrieval later
        self._data = data
    }

    メモのインスタンスに画像名が存在する場合、コードは retrieveImage を呼び出します。これは非同期関数です。画像のダウンロード時に関数の呼び出しが必要です。この関数は Image UI オブジェクトを作成し、それをメモのインスタンスに割り当てます。この割り当てはユーザーインターフェイスの更新をトリガーし、DispatchQueue.main.async によりアプリケーションのメインスレッドで発生することに注意してください。

  • UI コードを追加して画像をキャプチャする

    最初に、画像のキャプチャをサポートする一般的なコードを追加します。このコードは多くのアプリケーションで再利用することができ、ユーザーが画像ライブラリから画像を選択できるようにする画像セレクターを表示します。

    Xcode で新しい swift ファイルを作成します (⌘N を押してから Swift を選択)。ファイルに CaptureImageView.swift という名前を付け、このコードを追加します。

    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>) {
    
        }
    }
  • メモが作成されたら画像を保存する

    メモが作成された際に、バックエンドからストレージメソッドを呼び出しましょう。ContentView.swift を開き、AddNoteView を変更して ImagePicker コンポーネントを追加します。

    // 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()
                }
            }
        }
    }

    Create 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")
        }
    }
  • 構築およびテストする

    すべてが想定どおりに動作することを確認するには、プロジェクトを構築して実行します。[Product (製品)] メニューをクリックして [Run (実行)] を選択するか、または ⌘R を押します。何のエラーも発生しないはずです。

    引き続きサインイン済みの状態であれば、アプリが起動して 1 つのメモを含んだリストが表示されます。もう一度、+ 記号を押してメモを作成します。ここでは、ローカルの画像ストアから選択した写真を追加します。

    完全なフローは以下のとおりです。

    iOSTutorial-Module5-step1
    iOSTutorial-Module5-step2
    iOSTutorial-Module5-step3
    iOSTutorial-Module5-step4
    iOSTutorial-Module5-step5

まとめ

AWS Amplify を使用して iOS アプリケーションを構築しました。 アプリに認証を追加して、ユーザーがサインアップ、サインイン、アカウント管理を行えるようにしました。アプリには、Amazon DynamoDB データベースで設定されたスケーラブルな GraphQL API もあり、ユーザーはメモを作成および削除できます。また、Amazon S3 を使用してファイルストレージを追加し、ユーザーが画像をアップロードしてアプリで表示できるようにしました。

最後のセクションでは、作成したバックエンドを再利用または削除する手順を解説します。

  • 複数のプロジェクト間でバックエンドを共有する

    Amplify を使用すれば、複数のフロントエンドアプリケーション間で単一のバックエンドを簡単に共有できます。

    ターミナルで他のプロジェクトディレクトリに移動し、次のコマンドを実行します。

    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.

    数秒後、次のメッセージが表示されます。

    Added backend environment config object to your project.
    Run 'amplify pull' to sync upstream changes.

    プルされた 2 つの設定ファイルが確認できます。「このバックエンドを変更する予定はありますか?」という質問に「はい」と答えると、Amplify ディレクトリも表示されます。

    ➜  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
  • バックエンドを削除する

    このチュートリアルと同様に、テストやプロトタイプのバックエンドを作成した場合、または学習目的でバックエンドを作成した場合には、作成したクラウドリソースは削除します。

    本チュートリアルのコンテキストにおけるこのリソースの使用は無料利用枠の対象ですが、ベストプラクティスは、クラウド内の使用していないリソースをクリーンアップすることです。

    Amplify プロジェクトをクリーンアップするには、ターミナルで次のコマンドを実行します。

    amplify delete

    しばらくすると、すべてのクラウドリソースが削除されたことを確認する、次のメッセージが表示されます。

    ✔ Project deleted in the cloud
    Project deleted locally.

このチュートリアルを最後まで続けていただき、ありがとうございます。ぜひ、以下のツールまたは Github リポジトリのプルリクエストを使用して、フィードバックをお寄せください。

このモジュールは役に立ちましたか?

ありがとうございます
このチュートリアルで良かった点をお聞かせください。
閉じる
ご期待に添えず申し訳ありません
古い説明、わかりにくい説明、間違った説明はございませんでしたか? このチュートリアルの改善のために、ぜひフィードバックをお寄せください。
閉じる

おめでとうございます

AWS でウェブアプリケーションを構築できました! 次のステップとして、特定の AWS テクノロジーをさらに詳しく見て、アプリケーションを次のレベルに引き上げます。