Swift SDKを使ってカメラメモアプリを作る（その1：画面の仕様とSwift SDKの導入）

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

今回から複数回に分けて、カメラメモアプリを作ってみます。Swift SDKの使い方はもちろん、NCMBの基本的な利用法を学ぶのに役立ててください。

なお、今回のコードは[NCMBMania/camera_memo_swift: Swift SDKを使ったカメラメモアプリのコードです](https://github.com/NCMBMania/camera_memo_swift)にアップしてあります。

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

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

## 利用する機能について

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

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

## 画面について

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

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

## ContentView

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

```swift
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】カメラ機能の実装方法【撮影画像とライブラリー画像の利用】](https://tomato-develop.com/swiftui-how-to-use-camera-and-select-photos-from-library/)から拝借しています。ありがとうございます。

## UploaderView

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

```swift
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

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

```swift
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

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

```swift
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

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

```swift
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  

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

```swift
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

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

```swift
struct GridImageView: View {
    @State private var fileName: String = ""
    @State private var imageData: Data? = .init(capacity:0)
    @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の導入法

Swift SDKはCocoaPods向けに提供しています。Xcodeで新しいiOSプロジェクトを作ったら、Podfileを用意します。

```
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'todoapp' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for todoapp
  pod 'NCMB', :git => 'https://github.com/NIFCLOUD-mbaas/ncmb_swift.git'
end
```

そして `pod install` でSwift SDKをインストールします。

## 初期化について

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

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

```swift
import SwiftUI
import NCMB   // 追加
```

そして `var body` の上に `@Environment` を追加します。

```swift
@Environment(\.scenePhase) private var scenePhase // 追加
var body: some Scene {
```

最後に `WindowGroup` に `onChange` を追加します。このonChangeの中で初期化処理を行います。

```swift
WindowGroup {
    ContentView()
}
.onChange(of: scenePhase) { scene in
    switch scene {
    case .active:
        NCMB.initialize(applicationKey: "YOUR_APPLICATION_KEY", clientKey: "YOUR_CLIENT_KEY")
    case .background:
        print("background")
    case .inactive:
        print("inactive")
    @unknown default:
        print("default")
    }
}
```

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

## まとめ

今回は必要な画面の内容と、NCMBのSwift SDK導入までを紹介しました。次回は[認証処理と写真アップロード](https://qiita.com/goofmint/items/e087f35c7bbb0e2cd14e)を解説します。

[イントロダクション (Swift) : クイックスタート | ニフクラ mobile backend](https://mbaas.nifcloud.com/doc/current/introduction/quickstart_swift.html)