ニフクラ 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の中にある + アイコンをクリックします。
パッケージ名が色々と出てくるので、左上のURL入力欄にて下記のURLを入力します。
https://github.com/NIFCLOUD-mbaas/ncmb_swift
そうすると ncmb_swift が出てくるので、右下にある Add Package ボタンを押します。
確認ダイアログはそのままで、Add Package ボタンを押します。
これでインストール完了です。
初期化について
現在、新規でiOSアプリを作成すると、InterfaceがSwiftUI、Life CycleがSwiftUI Appとなっています。この状態で作ると AppDelegate.swift
はなく、 (アプリ名)App.swift
というファイルが作られます。
この場合、まずSwift SDKを読み込みます。
import SwiftUI
import NCMB // 追加
次にWindowGroupをダミーのVStackで囲み、そこにonAppearを追加します。その中で初期化処理を行います。 YOUR_APPLICATION_KEY
と YOUR_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.swift
の getAllPhotos
にて行います。まず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をぜひご利用ください。