SwiftUI Tutorialsをとりあえずやったものの、以来触る機会がなかったので世の中に取り残される危機感を払拭するために、復習を兼ねて簡単な文字数カウントアプリを作ってみました。
以前から興味のあったOCRライブラリも一緒に試すことで一石二鳥で学習できたこともあり、せっかくなのでQiitaに残しておこうと思います。
とてもシンプルなアプリなので、先に完成品のgifを貼っておきます。
ボタンには「撮影する」と表示されてますが、今回は省略してイメージライブラリの中から選択しています。
対象読者、環境
- SwiftUI Tutorials 経験者
- Xcode 11 / Swift 5
さっそく実装
SwiftUI Tutorialsを経験していることを前提に話を進めるため、プロジェクトやファイルの作成等の基本的操作は省略します。まだの方は先に進めるか交互に見ながら読み進めることをおすすめします。
https://developer.apple.com/tutorials/swiftui
最初のViewにButtonを追加
起動後の最初のViewに「撮影する」ボタンと「解析する」ボタンを追加します。
ファイル名はHomeView
としてますが、もちろんContentView
でも構いません。
import SwiftUI
struct HomeView: View {
var body: some View {
VStack(alignment: .center, spacing: 10) {
Button(action: {
//
}) {
Text("撮影する")
}.padding()
.background(Color.green)
.foregroundColor(.white)
.cornerRadius(10)
Button(action: {
// OCR実行
}) {
Text("解析する")
}.padding()
.background(Color.gray)
.foregroundColor(.white)
.cornerRadius(10)
}
}
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
画像を取得するためのImagePickerViewを追加
画像を取得するためにUIImagePickerViewController
を使います。Tutorialsの「Interfacing with UIKit」を参考にUIViewControllerRepresentable
を準拠させたViewクラスとUIImagePickerControllerDelegate
メソッドを利用するためのCoodinatorクラスを用意します。
まずはCoodinatorに必要なNSObject
とUIImagePickerViewControllerんい必要なUINavigationControllerDelegate
, UIImagePickerControllerDelegate
を準拠させたImagePickerCoordinator
クラスを作ります。
class ImagePickerCoordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
@Binding var isShown: Bool
@Binding var uiImage: UIImage?
init(isShown: Binding<Bool>, image: Binding<UIImage?>) {
_isShown = isShown
_uiImage = image
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
uiImage = info[.originalImage] as? UIImage
isShown = false
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
isShown = false
}
}
isShown
は画像選択/キャンセル時に画面を閉じるため、uiImage
はイメージデータを参照するために@Binding
を付けてプロパティを定義しています。この設計であればクラスの責務が極端に肥大化することなく、View側はFatにならずに済みますね。
struct ImagePickerView: UIViewControllerRepresentable {
@Binding var isShown: Bool
@Binding var uiImage: UIImage?
func makeCoordinator() -> ImagePickerCoordinator {
return ImagePickerCoordinator(isShown: $isShown, image: $uiImage)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
こちらは先ほどのImagePickerCoordinator
を利用して間接的にデリゲートメソッドを呼べるようにしています。
HomeViewからImagePickerを呼び出し
sheet
メソッドを追加して「撮影する」ボタン押下時にImagePickerView
へ遷移させます。
画像選択後はボタンの上に画像を表示させます。
また、画像が無いときは「解析ボタン」を無効にしたいので、真偽値を渡して制御するようにしました。
import SwiftUI
struct HomeView: View {
@State private var showImagePicker: Bool = false
@State private var uiImage: UIImage? = nil
private var hasImage: Bool {
return uiImage != nil
}
var body: some View {
VStack(alignment: .center, spacing: 10) {
if hasImage {
Image(uiImage: uiImage!)
.resizable()
.scaledToFit()
}
Button(action: {
self.showImagePicker = true
}) {
Text("撮影する")
}.padding()
.background(Color.green)
.foregroundColor(.white)
.cornerRadius(10)
Button(action: {
// OCRを実行
}) {
Text("解析する")
}.padding()
.background(hasImage ? Color.orange : Color.gray)
.foregroundColor(.white)
.cornerRadius(10)
.disabled(!hasImage)
}.sheet(isPresented: $showImagePicker) {
ImagePickerView(isShown: self.$showImagePicker, uiImage: self.$uiImage)
}
}
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
「SwiftyTesseract」をプロジェクトへ追加
今回はCocoaPodsでインストールしました。もちろんライブラリ管理ツールは、SwiftUIに関わらずこれまで通り使えます。
但し、ライブラリ側がSwift 5に対応している必要があるのでそこは注意が必要です。
「SwiftyTesseract」はリリースバージョン自体はまだSwift 4.2まででしたが、Swift 5に対応したブランチが存在したためそちらを利用しました。
pod 'SwiftyTesseract', git: 'https://github.com/SwiftyTesseract/SwiftyTesseract.git', branch: '3.0.0'
学習データをプロジェクトへ追加
SwiftyTesseractでは、Tesseract用にあらかじめ用意されている学習データをそのまま利用することができます。
今回は日本語を取り扱う文字数カウントをしたいので日本語学習データを利用します。
tesseract-ocr/tessdata_fast から速度重視の学習データをダウンロードします。 👉こちら
プロジェクトの以下のようにtessdataフォルダを作成し、ダウンロードしたファイルを追加します。
これだけで自動で学習データを読み込んでくれます👏
「撮影する」ボタン押下時にOCRの実行
SwiftyTesseractをプロジェクトに追加できたらimportしてコードを書いていきます。
取得した文字列は空行や空文字が入っており、純粋にカウントすると文字数以上の数値になってしまうのでトリミングしました。
import SwiftUI
import SwiftyTesseract
struct HomeView: View {
...
@State private var characterCount: Int? = nil
var body: some View {
VStack(alignment: .center, spacing: 10) {
...
Button(action: {
let swiftyTesseract = SwiftyTesseract(language: .japanese)
let result = swiftyTesseract.performOCR(on: self.uiImage!)
switch result {
case .success(let string):
print(string)
let trimString = string.components(separatedBy: .whitespacesAndNewlines).joined()
print(trimString)
self.characterCount = trimString.count
case .failure: break // Handle the error
}
}) {
Text("解析する")
}.padding()
.background(hasImage ? Color.orange : Color.gray)
.foregroundColor(.white)
.cornerRadius(10)
.disabled(!hasImage)
...
実行結果をTextに反映
最後に「解析する」ボタンの下に以下のコードを追加して文字数を表示するようにして終わりです。
if characterCount != nil {
Text("文字数: \(characterCount!)")
}
あとがき
今回の内容もSwift Tutorialsと同様のレベルではあるものの、事前に用意されていないものを作ることでより実になったように感じます。また思っていたよりもコードが少なく、シンプルに書けたのでSwift UIにより魅了されました。また、Swift UIとWi-Fi 実機デバッグの開発体験は非常に良くて、動作確認すでも待ちやモヤモヤが随分解消されました。
SwiftyTesseractについては、最初の画像以外にもいろいろ試したところ思ったよりも精度が高かったのですが、やはり長文や手書きだと精度が落ちてしまうため、次の学習としてML Kit for Firebaseを試そうかと思っています。
Swift UIを使うとクライアントの準備コストが下がるため、バックエンド側の試作に注力できるのが良いですね。