mycode/myApp/Expiranner/IOS/ScannerView.swift
2026-07-09 19:23:40 +09:00

243 lines
8.2 KiB
Swift

//
// ScannerView.swift
// Expiranner
//
// : OCR
import SwiftUI
import SwiftData
import VisionKit
import Vision
import UIKit
struct ScannerView: View {
/// / ( )
let year: Int
let month: Int
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var modelContext
@State private var scannerHolder = ScannerHolder()
@State private var isScanning = true
@State private var capturedImage: UIImage?
@State private var recognizedText = ""
@State private var showPopup = false
@State private var showDatePicker = false
@State private var selectedDate = Date()
/// :
private var monthRange: ClosedRange<Date> {
let calendar = Calendar.current
let start = calendar.date(from: DateComponents(year: year, month: month, day: 1)) ?? Date()
let last = calendar.date(byAdding: DateComponents(month: 1, day: -1), to: start) ?? start
return start...last
}
var body: some View {
ZStack {
if DataScannerViewController.isSupported && DataScannerViewController.isAvailable {
DataScannerRepresentable(holder: scannerHolder, isScanning: $isScanning)
.ignoresSafeArea()
} else {
Color.black.ignoresSafeArea()
Text("이 기기에서는 카메라 스캐너를 사용할 수 없습니다")
.foregroundStyle(.white)
}
VStack {
HStack {
Spacer()
Button {
print("스캐너 종료")
dismiss()
} label: {
Image(systemName: "xmark")
.font(.title2)
.foregroundStyle(.white)
.padding(12)
.background(Circle().fill(.black.opacity(0.5)))
}
.padding()
}
Spacer()
Button {
capturePhoto()
} label: {
Circle()
.stroke(.white, lineWidth: 4)
.frame(width: 72, height: 72)
.overlay(Circle().fill(.white).frame(width: 58, height: 58))
}
.padding(.bottom, 32)
}
if showPopup {
ocrPopup
}
}
.sheet(isPresented: $showDatePicker) {
datePickerSheet
}
}
/// OCR : + + []/[]
private var ocrPopup: some View {
VStack(spacing: 16) {
if let capturedImage {
Image(uiImage: capturedImage)
.resizable()
.scaledToFit()
.frame(maxHeight: 180)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
TextField("인식된 텍스트", text: $recognizedText, axis: .vertical)
.lineLimit(1...5)
.textFieldStyle(.roundedBorder)
HStack(spacing: 12) {
Button("재촬영") {
print("재촬영 선택")
resumeScanning()
}
.frame(maxWidth: .infinity)
.buttonStyle(.bordered)
Button("적용") {
print("적용 선택: \(recognizedText)")
selectedDate = defaultDate()
showDatePicker = true
}
.frame(maxWidth: .infinity)
.buttonStyle(.borderedProminent)
}
}
.padding()
.background(RoundedRectangle(cornerRadius: 14).fill(Color(.systemBackground)))
.padding(24)
}
/// :
private var datePickerSheet: some View {
NavigationStack {
VStack {
DatePicker("날짜", selection: $selectedDate, in: monthRange, displayedComponents: .date)
.datePickerStyle(.graphical)
.padding()
Spacer()
}
.navigationTitle("\(String(year))\(month)월 날짜 선택")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("취소") { showDatePicker = false }
}
ToolbarItem(placement: .confirmationAction) {
Button("저장") { saveItem() }
}
}
}
}
/// : ,
private func defaultDate() -> Date {
let today = Date()
return monthRange.contains(today) ? today : monthRange.lowerBound
}
private func capturePhoto() {
guard let scanner = scannerHolder.scanner else {
print("스캐너를 사용할 수 없습니다")
return
}
Task {
do {
let photo = try await scanner.capturePhoto()
print("사진 촬영 완료")
isScanning = false
capturedImage = photo
recognizedText = recognizeText(in: photo)
showPopup = true
} catch {
print("사진 촬영 실패: \(error.localizedDescription)")
}
}
}
/// Vision
private func recognizeText(in image: UIImage) -> String {
guard let cgImage = image.cgImage else { return "" }
let request = VNRecognizeTextRequest()
request.recognitionLanguages = ["ko-KR", "en-US"]
request.recognitionLevel = .accurate
request.usesLanguageCorrection = true
let handler = VNImageRequestHandler(cgImage: cgImage)
do {
try handler.perform([request])
} catch {
print("텍스트 인식 실패: \(error.localizedDescription)")
return ""
}
let text = request.results?
.compactMap { $0.topCandidates(1).first?.string }
.joined(separator: "\n") ?? ""
print("인식된 텍스트: \(text)")
return text
}
private func saveItem() {
let imageData = capturedImage?.jpegData(compressionQuality: 0.5)
let item = ProductItem(name: recognizedText, date: selectedDate, imageData: imageData)
modelContext.insert(item)
print("저장 완료: \(item.name)\(selectedDate)")
//
UINotificationFeedbackGenerator().notificationOccurred(.success)
showDatePicker = false
resumeScanning()
}
///
private func resumeScanning() {
showPopup = false
capturedImage = nil
recognizedText = ""
isScanning = true
}
}
/// DataScannerViewController
@Observable
final class ScannerHolder {
var scanner: DataScannerViewController?
}
struct DataScannerRepresentable: UIViewControllerRepresentable {
let holder: ScannerHolder
@Binding var isScanning: Bool
func makeUIViewController(context: Context) -> DataScannerViewController {
let scanner = DataScannerViewController(
recognizedDataTypes: [.text(languages: ["ko-KR"])],
qualityLevel: .balanced,
isHighlightingEnabled: false
)
holder.scanner = scanner
return scanner
}
func updateUIViewController(_ scanner: DataScannerViewController, context: Context) {
if isScanning, !scanner.isScanning {
do {
try scanner.startScanning()
print("스캔 시작")
} catch {
print("스캔 시작 실패: \(error.localizedDescription)")
}
} else if !isScanning, scanner.isScanning {
scanner.stopScanning()
print("스캔 중지")
}
}
}