//
// ModalView.swift
// OCR_TestApp
//
import SwiftUI
import PhotosUI
import Photos
import Vision
import VisionKit
import AVFoundation
struct ModalView: View {
// MARK: - 変数
@EnvironmentObject var signInHandler: SignInHandler
@Environment(\.dismiss) var dismiss
@State private var showPhotoPicker = false
@State private var selectedImages: [UIImage] = []
@State private var imageProcessingQueue: [UIImage] = []
@State private var selectedRecognizedText: [String] = []
@State private var image: UIImage? // カメラで撮影した画像を保持する
@State private var isProcessingImage = false
@State private var isCameraPickerPresented = false
// アラート用の状態変数
@State private var imageToSave: UIImage?
@State private var capturedImage: UIImage? // キャプチャされた画像
@State private var isCameraPresented = false
@State private var isSaveAlertPresented = false
//トースト用
@State private var isToastVisible = false
@State private var toastMessage = ""
// MARK: - ボディ
var body: some View {
VStack {
// Header with Close Button
HStack {
Button(action: { dismiss() }) {
Image(systemName: "x.circle.fill")
.resizable()
.frame(width: 40, height: 40)
.foregroundColor(.red)
}
.buttonStyle(PlainButtonStyle())
.padding()
}
// Title
Text("モーダルウィンドウ")
.font(.title)
.padding()
// Photo Picker and Camera Buttons
HStack {
// Photo Picker Button
Button(action: { showPhotoPicker = true }) {
VStack {
Image(systemName: "photo.on.rectangle")
.resizable()
.frame(width: 40, height: 40)
.foregroundColor(.blue)
Text("写真選択").font(.caption)
}
}
.buttonStyle(PlainButtonStyle())
.padding()
.sheet(isPresented: $showPhotoPicker) {
PhotoPickerView(selectedImages: $selectedImages)
}
// Camera Button
Button(action: {
requestCameraPermission { granted in
if granted {
isCameraPresented = true
} else {
showAlert(title: "カメラアクセス拒否", message: "設定アプリからカメラへのアクセスを許可してください。")
}
}
}) {
VStack {
Image(systemName: "camera")
.resizable()
.frame(width: 40, height: 40)
.foregroundColor(.blue)
Text("撮影").font(.caption)
}
}
.padding()
.sheet(isPresented: $isCameraPresented) {
CameraView(capturedImage: $capturedImage)
.onChange(of: capturedImage) { image in
if let image = image {
imageToSave = image
isSaveAlertPresented = true
}
}
}
// Send All Button
Button(action: {
print("Selected Recognized Text: \(selectedRecognizedText)")
sendAllImages()
}) {
Text("すべて送信")
.foregroundColor(.white)
.padding()
.background(selectedImages.isEmpty ? Color.gray : Color.green)
.cornerRadius(10)
}
.disabled(selectedImages.isEmpty)
}
// トースト表示
if isToastVisible {
VStack {
Spacer()
ToastView(message: toastMessage)
.transition(.opacity)
.animation(.easeInOut(duration: 0.3))
}
.zIndex(1) // トーストが他の要素の上に表示されるように設定
}
// Manual Text Extraction Button
Button(action: {
extractTextManually()
}) {
Text("テキストを抽出")
.foregroundColor(.white)
.padding()
.background(selectedImages.isEmpty ? Color.gray : Color.orange)
.cornerRadius(10)
}
.disabled(selectedImages.isEmpty)
// Display Selected Images
if !selectedImages.isEmpty {
ScrollView(.horizontal) {
HStack {
ForEach(selectedImages, id: \.self) { image in
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
.padding()
}
}
// Display Recognized Text
if !selectedRecognizedText.isEmpty {
ScrollView {
VStack(alignment: .leading) {
ForEach(selectedRecognizedText, id: \.self) { text in
Text(text)
.padding()
.background(Color.gray.opacity(0.2))
.cornerRadius(8)
}
}
.padding()
}
}
}
.padding()
.alert(isPresented: $isSaveAlertPresented) {
Alert(
title: Text("写真を保存しますか?"),
message: Text("フォトライブラリに写真を保存しますか?"),
primaryButton: .default(Text("はい")) {
if let imageToSave = imageToSave {
saveImageToPhotoLibrary(imageToSave)
}
},
secondaryButton: .cancel(Text("いいえ"))
)
}
.onAppear {
checkPermissions()
}
}
// MARK: - showToast
// トースト表示ロジック
private func showToast(message: String) {
toastMessage = message
isToastVisible = true
// トーストを3秒後に非表示にする
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
withAnimation {
isToastVisible = false
}
}
}
// MARK: - 画像権限
private func checkPermissions() {
// カメラアクセス権限
AVCaptureDevice.requestAccess(for: .video) { granted in
DispatchQueue.main.async {
if !granted {
print("カメラへのアクセスが拒否されました。")
// ユーザーに通知
}
}
}
// 写真ライブラリアクセス権限
PHPhotoLibrary.requestAuthorization { status in
DispatchQueue.main.async {
switch status {
case .authorized:
print("写真ライブラリへのアクセスが許可されました。")
case .denied, .restricted:
print("写真ライブラリへのアクセスが拒否されました。")
// ユーザーに通知
case .notDetermined:
print("写真ライブラリへのアクセスがまだリクエストされていません。")
@unknown default:
print("未知の権限ステータス。")
}
}
}
}
// MARK: - Send All Images
private func sendAllImages() {
guard !selectedImages.isEmpty else {
print("No images selected")
return
}
if selectedRecognizedText.isEmpty {
print("Recognized text is empty, extracting manually...")
extractTextManually()
}
var successCount = 0
var failureCount = 0
let totalImages = selectedImages.count
for (index, image) in selectedImages.enumerated() {
if let imageData = image.jpegData(compressionQuality: 0.8) {
let text = index < selectedRecognizedText.count ? selectedRecognizedText[index] : "テキストなし"
let dict: [String: String] = [
"adress": signInHandler.emailAddress ?? "unknown",
"txtval": text
]
uploadImage(imageData, additionalFields: dict) { success in
if success {
successCount += 1
} else {
failureCount += 1
}
// 全ての送信処理が完了した時
if successCount + failureCount == totalImages {
DispatchQueue.main.async {
if failureCount == 0 {
selectedImages.removeAll()
selectedRecognizedText.removeAll()
showToast(message: "すべての画像が正常に送信されました!")
} else {
showToast(message: "\(successCount) 件成功、\(failureCount) 件失敗しました。")
}
}
}
}
}
}
}
// MARK: - Upload Image
private func uploadImage(_ imageData: Data, additionalFields: [String: String], completion: @escaping (Bool) -> Void) {
guard let url = URL(string: "https://ocrapp.we-labo.com/Swift_Insert.php") else {
print("Invalid URL")
completion(false)
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
let boundary = UUID().uuidString
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
var body = Data()
for (key, value) in additionalFields {
body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
body.append("\(value)\r\n".data(using: .utf8)!)
}
body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"file\"; filename=\"image.jpg\"\r\n".data(using: .utf8)!)
body.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)
body.append(imageData)
body.append("\r\n".data(using: .utf8)!)
body.append("--\(boundary)--\r\n".data(using: .utf8)!)
request.httpBody = body
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("Error: \(error.localizedDescription)")
completion(false)
return
}
guard let data = data, let responseString = String(data: data, encoding: .utf8) else {
print("No data or invalid response")
completion(false)
return
}
print("Server response: \(responseString)")
if responseString.contains("success") {
completion(true)
} else {
completion(false)
}
}.resume()
}
// MARK: - Process Images Queue
private func processImagesInQueue() {
guard !isProcessingImage else { return }
guard !imageProcessingQueue.isEmpty else {
print("すべての画像が処理されました。")
return
}
isProcessingImage = true
processNextImage()
}
private func processNextImage() {
guard let image = imageProcessingQueue.first else {
isProcessingImage = false
return
}
imageProcessingQueue.removeFirst()
recognizeText(in: image) { recognizedText in
DispatchQueue.main.async {
self.selectedRecognizedText.append(recognizedText)
self.isProcessingImage = false
}
}
}
// MARK: - extractTextManually
private func extractTextManually() {
selectedRecognizedText.removeAll() // 前回の結果をクリア
for image in selectedImages {
recognizeText(in: image) { recognizedText in
DispatchQueue.main.async {
self.selectedRecognizedText.append(recognizedText)
}
}
}
}
// MARK: - Recognize Text
private func recognizeText(in image: UIImage, completion: @escaping (String) -> Void) {
guard let cgImage = image.cgImage else { return }
let request = VNRecognizeTextRequest { (request, error) in
guard let observations = request.results as? [VNRecognizedTextObservation] else { return }
let recognizedText = observations
.compactMap { $0.topCandidates(1).first?.string }
.joined(separator: "\n")
completion(recognizedText)
}
request.recognitionLanguages = ["ja-JP"]
request.recognitionLevel = .accurate
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
DispatchQueue.global(qos: .userInitiated).async {
do {
try handler.perform([request])
} catch {
print("Error recognizing text: \(error)")
}
}
}
// MARK: - Save Image to Photo Library
private func saveImageToPhotoLibrary(_ image: UIImage) {
let status = PHPhotoLibrary.authorizationStatus(for: .addOnly)
switch status {
case .authorized, .limited:
ImageSaveHelper.shared.saveImageToPhotoLibrary(image) { success, errorMessage in
if success {
showAlert(title: "保存成功", message: "写真がライブラリに保存されました。")
} else {
showAlert(title: "保存失敗", message: errorMessage ?? "エラーが発生しました。")
}
}
case .notDetermined:
PHPhotoLibrary.requestAuthorization(for: .addOnly) { newStatus in
if newStatus == .authorized || newStatus == .limited {
ImageSaveHelper.shared.saveImageToPhotoLibrary(image) { success, errorMessage in
if success {
showAlert(title: "保存成功", message: "写真がライブラリに保存されました。")
} else {
showAlert(title: "保存失敗", message: errorMessage ?? "エラーが発生しました。")
}
}
} else {
DispatchQueue.main.async {
showAlert(title: "写真ライブラリへのアクセスが拒否されました", message: "設定から変更してください。")
}
}
}
default:
DispatchQueue.main.async {
showAlert(title: "写真ライブラリへのアクセスが拒否されました", message: "設定から変更してください。")
}
}
}
// MARK: - カメラ権限の確認
func requestCameraPermission(completion: @escaping (Bool) -> Void) {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .authorized:
completion(true)
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { granted in
completion(granted)
}
default:
completion(false)
}
}
// MARK: - Show Alert
@State private var alertTitle: String = ""
@State private var alertMessage: String = ""
@State private var isAlertPresented: Bool = false
private func showAlert(title: String, message: String) {
DispatchQueue.main.async {
alertTitle = title
alertMessage = message
isAlertPresented = true
}
}
}
import UIKit
// MARK: - ImageSaveHelper
class ImageSaveHelper: NSObject {
static let shared = ImageSaveHelper()
func saveImageToPhotoLibrary(_ image: UIImage, completion: @escaping (Bool, String?) -> Void) {
UIImageWriteToSavedPhotosAlbum(image, self, #selector(saveCompleted(_:didFinishSavingWithError:contextInfo:)), nil)
self.completion = completion
}
private var completion: ((Bool, String?) -> Void)?
@objc private func saveCompleted(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
if let error = error {
completion?(false, error.localizedDescription)
} else {
completion?(true, nil)
}
}
}
// MARK: - CameraView
import SwiftUI
import UIKit
import AVFoundation
struct CameraView: UIViewControllerRepresentable {
@Binding var capturedImage: UIImage?
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
if UIImagePickerController.isSourceTypeAvailable(.camera) {
picker.sourceType = .camera
// 利用可能なカメラデバイスをチェックして設定
if UIImagePickerController.isCameraDeviceAvailable(.rear) {
picker.cameraDevice = .rear // バックカメラを設定
} else if UIImagePickerController.isCameraDeviceAvailable(.front) {
picker.cameraDevice = .front // フロントカメラをフォールバック
} else {
print("利用可能なカメラデバイスがありません。")
}
picker.cameraCaptureMode = .photo // 写真モードを指定
} else {
print("このデバイスではカメラがサポートされていません。")
}
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let parent: CameraView
init(_ parent: CameraView) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
if let image = info[.originalImage] as? UIImage {
parent.capturedImage = image
}
picker.dismiss(animated: true)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true)
}
}
}
// MARK: - ImageTextPair
struct ImageTextPair: Identifiable {
let id = UUID()
let image: UIImage
let text: String
}
// MARK: - ToastView
struct ToastView: View {
let message: String
var body: some View {
Text(message)
.font(.body)
.foregroundColor(.white)
.padding()
.background(Color.black.opacity(0.8))
.cornerRadius(10)
.shadow(radius: 10)
.padding()
}
}