0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【ハンズオン資料】Swift SDKを使ってカメラメモアプリを作る

Last updated at Posted at 2021-07-19

ニフクラ mobile backend(NCMB)では各種言語向けにSDKを提供しています。その中で最も新しいSwift SDKについて、その使い方を紹介します。

下準備

今回のコードはNCMBMania/camera_memo_swift: Swift SDKを使ったカメラメモアプリのコードですにアップしてあります。ハンズオンにあたって、まずこちらをダウンロードしてください。

カメラメモアプリの機能について

カメラメモアプリは、写真を撮影して、そのデータをファイルストアにアップロードします。そして別途メモを書き残すことで、写真とメモを紐付けて保存します。

利用する機能について

カメラメモアプリで利用するNCMBの機能は次の通りです。

  • 認証機能
    • ID/パスワードによるログイン
    • ログアウト
  • ファイルストア
    • ファイルアップロード
    • ファイルダウンロード
  • データストア
    • ファイルストアと紐付けたメモを保存

画面について

カメラメモアプリでは次の画面(View)を用意しています。

  • ContentView
    タブバーで2つの画面を読み込んでいます。1つはログインとアップロード画面(UploaderView)、もう1つは写真一覧画面(ImageView)です。
  • Imagepicker
    写真の撮影またはフォトライブラリから写真を選択します。
  • UploaderView
    タブバーから読み込まれます。ログイン画面やアップロード画面を表示します。
  • MemoView
    撮影または選択した写真を表示し、メモとともにNCMBへアップロードします。
  • LoginView
    NCMBへのユーザ登録、ログイン処理を行います。
  • ImageView
    NCMBへアップロードした写真を一覧表示します。
  • ModalView
    写真一覧をタップした際にモーダル表示します。
  • GridImageView
    写真一覧のグリッド表示用ビューです。

ContentView

タブバーで2つの画面を読み込んでいます。

struct ContentView: View {
    var body: some View {
        TabView {
            UploaderView()
                .tabItem {
                    VStack {
                        Image(systemName: "photo")
                        Text("Photo")
                    }
            }.tag(1)
            ImageView()
                .tabItem {
                    VStack {
                        Image(systemName: "rectangle.grid.2x2")
                        Text("Photos")
                    }
            }.tag(2)
        }
    }
}

Imagepicker

写真の撮影またはフォトライブラリから写真を選択します。コードは【SwiftUI】カメラ機能の実装方法【撮影画像とライブラリー画像の利用】から拝借しています。

UploaderView

ログイン画面やアップロード画面を表示します。ログインしていない場合は LoginView を表示します。

// 実装済みです
struct UploaderView: View {
    @State var imageData : Data = .init(capacity:0)
    @State var source:UIImagePickerController.SourceType = .photoLibrary
    @State var isImagePicker = false
    @State var isLogin: Bool = NCMBUser.currentUser != nil
    var body: some View {
            NavigationView{
                VStack(spacing:0){
                        ZStack{
                            Color.white
                                .edgesIgnoringSafeArea(.all)
                                .opacity(0.0)
                            NavigationLink(
                                destination: Imagepicker(show: $isImagePicker, image: $imageData, sourceType: source),
                                isActive:$isImagePicker,
                                label: {
                                    Text("")
                                })
                            VStack{
                                if isLogin {
                                    VStack(spacing:30){
                                        MemoView(imageData: $imageData)
                                        HStack(spacing:60){
                                            Button(action: {
                                                self.source = .photoLibrary
                                                self.isImagePicker.toggle()
                                            }, label: {
                                                Text("Photo library")
                                            })
                                            Button(action: {
                                                self.source = .camera
                                                self.isImagePicker.toggle()
                                            }, label: {
                                                Text("Take Photo")
                                            })
                                        }
                                        Button(action: {
                                          // ログアウト処理を実装します
                                        }, label: {
                                            Text("ログアウト")
                                        })
                                    }
                                } else {
                                    LoginView(isLogin: $isLogin)
                                }
                            }
                        }
                        .gesture(
                            TapGesture()
                                .onEnded { _ in
                                    UIApplication.shared.closeKeyboard()
                                }
                        )
                
                }
                .navigationBarTitle("Home", displayMode: .inline)
            }
        .ignoresSafeArea(.all, edges: .top)
        .background(Color.primary.opacity(0.06).ignoresSafeArea(.all, edges: .all))
    }
}

MemoView

ログインしている場合に使われる写真とメモを表示する画面です。

struct MemoView: View {
    @Binding var imageData : Data
    @State private var text: String = ""
    @State private var uploaded = false
    
    var body: some View {
        if imageData.count != 0 {
            Image(uiImage: UIImage(data: self.imageData)!)
                .resizable()
                .aspectRatio(contentMode: .fill)
                .frame(height: 250)
                .cornerRadius(15)
                .padding()
            TextField("メモ", text: $text)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .frame(maxWidth: 280)
            Button(action: {
                // メモアップロード処理(後述)
            }, label: {
                Text("Upload")
            })
            .alert(isPresented: $uploaded, content: {
                Alert(title: Text("アップロード完了"), message: Text("写真をアップロードしました"), dismissButton: .default(Text("閉じる"))
                )
            })
            
        }
    }
}

LoginView

ユーザ登録およびログイン処理を実行します。

struct LoginView: View {
    @State private var userName: String = ""
    @State private var password: String = ""
    @Binding var isLogin: Bool
    
    var body: some View {
        VStack(spacing: 16) {
            TextField("ユーザ名", text: $userName)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .frame(maxWidth: 280)
            SecureField("パスワード", text: $password)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .frame(maxWidth: 280)
            Button(action: {
                signUpOrLogin()
            }, label: {
                Text("新規登録/ログイン")
            })
        }
    }

    func signUpOrLogin() {
      // ユーザ登録とログイン処理(後述)
    }
}

ImageView

ファイルストアから画像を取得します。

struct ImageView: View {
    @State var memos: [NCMBObject] = []
    
    var columns: [GridItem] = Array(repeating: .init(.fixed(200)), count: 2)
    
    var body: some View {
        ScrollView(.vertical) {
            LazyVGrid(columns: columns, alignment: .center, spacing: 200) {
                ForEach (memos, id: \.objectId) { memo in
                    GridImageView(memo: memo)
                }
            }
            .onAppear() {
                getAllPhotos()
            }
        }
    }
    func getAllPhotos() {
      // 写真をファイルストアから取得(後述)
    }
}

ModalView

写真をタップした際にモーダル表示します。

struct ModalView: View {
    @Binding var isActive: Bool
    @State var memo: NCMBObject
    @State var imageData: Data
    
    var body: some View {
        HStack {
            Spacer()
            VStack {
                Spacer()
                GeometryReader { geometry in
                    Image(uiImage: UIImage(data: imageData)!)
                        .resizable()
                        .scaledToFill()
                        .frame(width: geometry.size.width, height: geometry.size.height)
                        .clipped()
                }
                if let text: String = memo["text"] {
                    Text(text).padding()
                }
                Button("閉じる") {
                    isActive = false
                }
                Spacer()
            }
            Spacer()
        }
        .padding()
        .background(Color(.black))
    }
}

GridImageView

写真一覧のグリッド表示用です。

struct GridImageView: View {
    // ファイルストアからダウンロードした写真データが入る
    @State private var imageData: Data? = .init(capacity:0)
    // Memoクラスのインスタンス
    @State var memo: NCMBObject? = nil
    // モーダル表示の制御用
    @State private var isShowing = false
        
    var body: some View {
        GeometryReader { geometry in
            if imageData?.count ?? 0 > 0 {
                Image(uiImage: UIImage(data: imageData!)!)
                    .resizable()
                    .scaledToFill()
                    .frame(width: geometry.size.width, height: geometry.size.width)
                    .clipped()
            } else {
                Rectangle().fill(Color.clear)
            }
        }.onAppear() {
            loadImage()
        }.onTapGesture {
            isShowing = true
        }.fullScreenCover(isPresented: $isShowing) {
            ModalView(isActive: $isShowing, memo: memo!, imageData: imageData!)
        }
        
    }

    func loadImage() {
      // 写真データを読み込みます(後述)
    }
}

Swift SDKの導入法

プロジェクトを開いて、Package Dependenciesの中にある + アイコンをクリックします。

ScreenShot__2021-10-04_18_35_29.png

パッケージ名が色々と出てくるので、左上のURL入力欄にて下記のURLを入力します。

ScreenShot__2021-10-04_18_35_37.png

https://github.com/NIFCLOUD-mbaas/ncmb_swift

そうすると ncmb_swift が出てくるので、右下にある Add Package ボタンを押します。

ScreenShot__2021-10-04_18_35_41.png

確認ダイアログはそのままで、Add Package ボタンを押します。

ScreenShot_ 2021-10-04 18.35.47.png

これでインストール完了です。

ScreenShot_ 2021-10-04 18.35.51.png

初期化について

現在、新規でiOSアプリを作成すると、InterfaceがSwiftUI、Life CycleがSwiftUI Appとなっています。この状態で作ると AppDelegate.swift はなく、 (アプリ名)App.swift というファイルが作られます。

この場合、まずSwift SDKを読み込みます。

import SwiftUI
import NCMB   // 追加

次にWindowGroupをダミーのVStackで囲み、そこにonAppearを追加します。その中で初期化処理を行います。 YOUR_APPLICATION_KEYYOUR_CLIENT_KEY をそれぞれNCMBから取得したものに書き換えてください。

WindowGroup {
    VStack {
        ContentView()
    }
    .onAppear() {
        print("Initialize NCMB")
        // APIキーの設定とSDK初期化
        NCMB.initialize(applicationKey: "YOUR_APPLICATION_KEY", clientKey: "YOUR_CLIENT_KEY")
    }
}

これで利用可能になります。

認証処理について

認証処理を行う画面は LoginView.swift になります。今回はユーザ登録とログインを一つの画面で行うようにします。もちろん分けて実装しても問題ありません。実装は signUpOrLogin 内で行います。まず入力されたユーザ名 userName とパスワード password を使ってユーザ登録処理を実行します。

// NCMBへのユーザ登録とログイン処理を行う関数
func signUpOrLogin() {
    // ログイン用のユーザオブジェクトを作成
    let user = NCMBUser()
    user.userName = userName // 入力されたユーザ名
    user.password = password // 入力されたパスワード
    // ユーザ登録処理
    user.signUpInBackground(callback: { _ in
        // ユーザ登録の成否は問わずログイン処理
        NCMBUser.logInInBackground(userName: userName, password: password, callback: { _ in
            // NCMBUser.currentUser が nilでなければログイン成功
            self.isLogin = NCMBUser.currentUser != nil
        })
    })
}

ログアウト処理

ログアウト処理は UploaderView.swift にて実装します。

Button(action: {
    // ログアウト処理を実装します
}, label: {
    Text("ログアウト")
})

内容は次の3行です。

NCMBUser.logOutInBackground(callback: { _ in
    isLogin = false
})

メモ入力画面について

ログインが完了すると isLogin が true になります。その結果、 UploaderView.swift にて写真選択/入力画面が表示されます。

// 実装済み
if isLogin {
    VStack(spacing:30){
        MemoView(imageData: $imageData)
        HStack(spacing:60){
            Button(action: {
                self.source = .photoLibrary
                self.isImagePicker.toggle()
            }, label: {
                Text("Photo library")
            })
            Button(action: {
                self.source = .camera
                self.isImagePicker.toggle()
            }, label: {
                Text("Take Photo")
            })
        }
// 以下略

写真を撮影または選択すると、そのデータは imageData に入ります。そうすると MemoView にて選択した写真が表示されます。

// 実装済み
var body: some View {
    // 画像データがあれば表示
    if imageData.count != 0 {
        Image(uiImage: UIImage(data: self.imageData)!)
            .resizable()
            .aspectRatio(contentMode: .fill)
            .frame(height: 250)
            .cornerRadius(15)
            .padding()
        TextField("メモ", text: $text)
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .frame(maxWidth: 280)
        Button(action: {
          // アップロード処理(後述)
        }, label: {
            Text("Upload")
        })
        .alert(isPresented: $uploaded, content: {
            Alert(title: Text("アップロード完了"), message: Text("写真をアップロードしました"), dismissButton: .default(Text("閉じる"))
            )
        })
        
    }
}

アップロード処理について

Upload ボタンをタップするとNCMBへのアップロード処理を開始します。MemoView.swiftの下記コメント内に記述します。

Button(action: {
                // ここにアップロード処理を記述します
            }, label: {
                Text("アップロード")
            })

まずファイル名をUUIDを使ってユニークなものを生成し、その名前を使ってNCMBFileオブジェクトを作ります。

// ファイル名を作成(ユニークなものを生成)
let fileName = "\(UUID()).jpg"
// ファイルストア用のインスタンスを作成
let file = NCMBFile(fileName: fileName)

次にそのデータへのアクセス権限として NCMBAcl を設定します。ログインユーザだけが読み書き可能としています。それを NCMBFile オブジェクトの acl に設定します。

// ACL(アクセス権限)を作成
var acl = NCMBACL.empty
let user = NCMBUser.currentUser
// アップロードした本人だけ読み書き可能とします
acl.put(key: user!.objectId!, readable: true, writable: true)
// ACLをセット
file.acl = acl

アップロードは saveInBackground メソッドで実行されます。引数として、写真データを渡します。保存処理は非同期です。

// アップロードを実行します
file.saveInBackground(data: self.imageData, callback: { result in
    switch result {
    case .success: // アップロードが成功
      // 続けて、この中に実装します
    case let .failure(error):
        print("保存に失敗しました: \(error)")
        return;
    }
})

アップロード処理が成功したら、続けてメモを保存します。メモ用のNCMBObjectを作ります。クラス名(DBでいうテーブル名相当)はMemoとします。

// Memoクラス(DBでいうテーブル相当)のインスタンス(DBでいう行相当)を準備
let memo = NCMBObject(className: "Memo");

そしてフィールド(DBでいうテーブルのカラム名)として、メモの入力内容 text とアップロードしたファイル名 fileName を設定します。ACLもファイルと同じものを設定します。

// 入力されたテキスト
memo["text"] = text
// アップロードしたファイル名
memo["fileName"] = fileName
// ACL(アクセス権限)はファイルと同じ
memo.acl = acl

そしてデータストアもファイルストアと同じく saveInBackground で保存処理を行います。保存がうまくいったら入力内容を消したり、キーボードを非表示にしています。

// 保存処理の実行
memo.saveInBackground(callback: {_ in
    // トグル
    self.uploaded.toggle()
    // 入力テキストをクリア
    self.text = ""
    // キーボードを閉じる(メインスレッドにて)
    DispatchQueue.main.async {
        UIApplication.shared.closeKeyboard()
    }
})

メモデータの取得

メモデータの取得は ImageView.swiftgetAllPhotos にて行います。まずNCMBを検索するためのNCMBQueryを用意します。対象となるクラス名(DBでいうテーブル相当)は保存したのと同じMemoです。

// Memoクラス(DBでいうテーブル相当)を準備
let query = NCMBQuery.getQuery(className: "Memo")

そして検索を実行します。検索結果はそのまま memos の中に入れます。

// 検索実行(今回は条件なし)
query.findInBackground(callback: { result in
    switch result {
        case let .success(array): // 検索成功
            // メモデータを適用
            self.memos = array
        case let .failure(error): // 検索失敗
            print("取得に失敗しました: \(error)")
    }
})

memosの中にデータが入ると、ForEachで繰り返し描画されます。この時、ユニークキーは objectId としてください。

ForEach (memos, id: \.objectId) { memo in
    GridImageView(memo: memo)
}

グリッド表示について

メモのグリッド表示は GridImageView.swift にて行います。

画像データを読み込む

ファイルストアから画像データを読み込む処理は loadImage 関数で行っています。ファイルストアではHTTPSアクセスも可能ですが、ACL(アクセス制限)を行っている場合にはデータを取得する方が良いでしょう。こうすることで、他のユーザからは読まれないデータとして利用できます。

まずメモデータからファイル名を取得します。

// ファイル名があれば処理
if let fileName : String = self.memo?["fileName"] {
}

この fileName を使ってNCMBFileオブジェクトを作ります。

// ファイルストア用のオブジェクトを用意
let file : NCMBFile = NCMBFile(fileName: fileName)

ファイルデータの取得は fetchInBackground にて行います。

// ダウンロード実行
file.fetchInBackground(callback: { result in
})

処理がうまくいっていればデータが取得できますので、それをimageDataとして適用します。

switch result {
    case let .success(data):  // ダウンロード成功
        self.imageData = data
    case let .failure(error): // ダウンロード失敗
        print(error)
}

imageDataが入れば、それをUIImageのdataとして適用し、Imageオブジェクトで画像として表示します。

// 記述済み
if imageData?.count ?? 0 > 0 {
    Image(uiImage: UIImage(data: imageData!)!)
        .resizable()
        .scaledToFill()
        .frame(width: geometry.size.width, height: geometry.size.width)
        .clipped()
} else {
    Rectangle().fill(Color.clear)
}

これでファイルストアから取得した画像データを描画できます。

画像をタップした際の処理

画像をタップした場合にはモーダル表示を行っています。

}.onTapGesture {
    // タップしたらフラグを立てる
    isShowing = true
// タップしたらフラグを立てる
}.fullScreenCover(isPresented: $isShowing) {
    // モーダルビューの呼び出し
    ModalView(isActive: $isShowing, memo: memo!, imageData: imageData!)
}

このModalViewは ModalView.swift にて定義しています。ここでは受け取ったNCMBObjectと画像データをそのまま描画しています。

これでカメラメモアプリの完成です。

まとめ

今回のカメラメモアプリを通して、NCMBの次の機能を利用しました。

  • 認証機能
    • ID/パスワードによるログイン
    • ログアウト
  • ファイルストア
    • ファイルアップロード
    • ファイルダウンロード
  • データストア
    • ファイルストアと紐付けたメモを保存

認証機能とデータストアはよく利用される機能ですし、スマートフォンアプリでは写真データもよく利用するでしょう。今回のコードを参考にSwift SDKをぜひご利用ください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?