GoogleのML Kitを使って、オフラインで日本語テキスト認識するカメラメモアプリを作る（その1：画面の説明とSDKの導入）

iOS 15ではOCR機能がOSに標準搭載されます（Live Text）。これはオフラインの機械学習モデルを使っているので、ネットワークに画像データが乗ったり、プライバシーの心配なく使えるのが魅力です。しかし、リリース時点では日本語に対応していません。

そこで使ってみたのがGoogleのML Kitになります。[まだベータ版なのですが、バージョン2から日本語にも対応しています](https://developers.google.com/ml-kit/vision/text-recognition/v2/ios?hl=ja)。また、ML Kitはオフライン動作も可能で、Google Cloudへの登録なども不要です。これを使えばLive Text的なことはできそうです。

今回はこのML Kitを使って、[以前作成したNCMBのSwift SDKを用いたカメラメモアプリ](https://qiita.com/goofmint/items/c98c160bedf8649b20ae)のメモを自動入力する機能を追加してみます。写真の中にあるテキスト情報をメモに使えば、後で検索したりコピーしたりするのに便利そうです。

## コード

今回のデモアプリのコードは[NCMBMania/Swift_CameraMemo_MLKit: Swift SDKによるカメラメモアプリにML Kitを追加した版です](https://github.com/NCMBMania/Swift_CameraMemo_MLKit)にアップロードしてあります。

## 利用したライブラリ・SDK

今回は以下のライブラリ・SDKを利用しています。

- [ML Kit  |  Google Developers](https://developers.google.com/ml-kit?hl=ja)
- [ニフクラ mobile backend](https://mbaas.nifcloud.com/doc/current/#/Swift)

言語はSwiftです。

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

カメラメモアプリは、写真を撮影して、その写真データをファイルストアにアップロードします。そして、写真の中にあるテキスト情報をML Kitを使って抽出し、メモとして写真に紐付けて保存します。

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

カメラメモアプリで利用するNCMBの機能は次の通りです。以前あった認証機能は省いています。

- データストア
  - ファイルストアと紐付けたメモを保存
  - 保存しているメモを取得
- ファイルストア
  - ファイルアップロード
  - ファイルダウンロード

## 画面について

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

- ContentView  
タブバーで2つの画面を読み込んでいます。1つは写真撮影とアップロードする画面（InputView）、もう1つは写真一覧画面（ImageView）です。
- Imagepicker  
写真の撮影またはフォトライブラリから写真を選択します。
- InputView  
タブバーから読み込まれます。写真の撮影とテキスト抽出、アップロードを行います。
- ImageView  
NCMBへアップロードした写真を一覧表示します。
- ModalView  
写真一覧をタップした際にモーダル表示します。
- GridImageView  
写真一覧のグリッド表示用ビューです。

## ContentView

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

```swift
struct ContentView: View {
    var body: some View {
        TabView {
            InputView()
                .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/)から拝借しています。ありがとうございます。

## InputView

![IMG_2431.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/0a1fd2b7-5bb4-1c9f-1e0e-a0404c30e5d2.png)


写真の選択やML Kitによる日本語抽出、NCMBへの保存処理を行います。

```swift
struct InputView: View {
    // イメージピッカーの表示用フラグ
    @State var showingPicker = false
    // 選択した写真が入る
    @State var image: UIImage?
    // 認識したテキストが入る
    @State var recognizeText: String?
    // ML Kitのテキスト認識用オブジェクト
    @State var textRecognizer = TextRecognizer.textRecognizer(options: JapaneseTextRecognizerOptions())
    // アラートのタイトル
    @State var title: String?
    // アラートのメッセージ
    @State var message: String?
    // アラートの表示用フラグ
    @State private var showingAlert = false
        
    var body: some View {
        VStack {
            HStack {
                Button("写真を撮る", action: {
                    showingPicker = true
                })
                Spacer()
                Button("保存する", action: {
                    savePhoto()
                })
                .disabled(image == nil)
            }
            if let image = image {
                Image(uiImage: image)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                Text(recognizeText ?? "")
            }
        }
        .sheet(isPresented: $showingPicker) {
            ImagePickerView(image: $image, sourceType: .library)
        }
        .onChange(of: image, perform: { value in
            getText()
        })
        .alert(isPresented: $showingAlert) {
            Alert(title: Text(title!), message: Text(message!))
        }
    }

    // テキスト抽出を行います
    func getText() -> Void {
      // 後述
    }
    
    // NCMBへ写真とテキストを保存します
    func savePhoto() -> Void {
      // 後述
    }    
}
```

## 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

![IMG_2433.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/f504aba7-361f-9156-82f5-591403f56a3d.png)


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

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

![IMG_2434.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/073ecdab-ad6d-44e9-1785-d7e17a86dcb5.png)


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

```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() {
      // 写真データを読み込みます（後述）
    }
}
```

## SDKの導入法

NCMBのSwift SDKはCocoaPods向けに提供しています。Swift Packagesでもインストールできますが、ML KitがCocoaPods向けだったので、一緒にインストールします。Xcodeで新しいiOSプロジェクトを作ったら、Podfileを用意します。

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

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

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

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

## 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: break;
    case .inactive: break;
    @unknown default: break;
    }
}
```

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

## まとめ

今回は必要な画面の内容と、NCMBのSwift SDK導入までを紹介しました。次回は[ML Kitによるテキスト抽出と写真アップロード](https://qiita.com/goofmint/items/e087f35c7bbb0e2cd14e)を解説します。

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