Files
ios/MusicBridge/QRScanner.swift
T

136 lines
6.4 KiB
Swift
Raw Normal View History

import SwiftUI
import AVFoundation
struct QRScannerSheet: View {
let receive: (String) -> Void
@Environment(\.dismiss) private var dismiss
@Environment(\.scenePhase) private var phase
@State private var error: String?
var body: some View {
NavigationStack {
VStack(spacing: 18) {
Text("Наведите камеру на QR в окне MusicBridge на ПК.")
.multilineTextAlignment(.center)
if let error {
ContentUnavailableView("Камера недоступна", systemImage: "camera", description: Text(error))
Button("Открыть настройки") {
if let url = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(url) }
}
Text("Можно закрыть сканер и ввести данные вручную.").font(.footnote)
} else {
QRScanner(receive: { text in receive(text); dismiss() }, failed: { error = $0 })
.clipShape(RoundedRectangle(cornerRadius: 20))
.accessibilityLabel("Камера для сканирования QR")
}
}.padding()
.navigationTitle("Сканировать QR")
.navigationBarTitleDisplayMode(.inline)
.toolbar { ToolbarItem(placement: .cancellationAction) { Button("Закрыть") { dismiss() } } }
.onChange(of: phase) { _, value in if value == .background { dismiss() } }
}
}
}
private struct QRScanner: UIViewControllerRepresentable {
let receive: (String) -> Void
let failed: (String) -> Void
func makeUIViewController(context: Context) -> QRScannerController {
QRScannerController(receive: receive, failed: failed)
}
func updateUIViewController(_ controller: QRScannerController, context: Context) {}
static func dismantleUIViewController(_ controller: QRScannerController, coordinator: ()) { controller.stop() }
}
private final class QRScannerController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
private let capture = AVCaptureSession()
private let queue = DispatchQueue(label: "MusicBridge.camera")
private var preview: AVCaptureVideoPreviewLayer?
private var visible = false
private var delivered = false
private var wantsRunning = false // camera queue only
private var configured = false // camera queue only
private let receive: (String) -> Void
private let failed: (String) -> Void
init(receive: @escaping (String) -> Void, failed: @escaping (String) -> Void) {
self.receive = receive; self.failed = failed
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
let preview = AVCaptureVideoPreviewLayer(session: capture)
preview.videoGravity = .resizeAspectFill
view.layer.addSublayer(preview)
self.preview = preview
}
override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews(); preview?.frame = view.bounds }
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
visible = true
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized: start()
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
DispatchQueue.main.async {
guard let self, self.visible else { return }
if granted { self.start() } else { self.failed("Разрешите доступ к камере в настройках MusicBridge.") }
}
}
default: failed("Разрешите доступ к камере в настройках MusicBridge.")
}
}
override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated); stop() }
private func start() {
queue.async { [weak self] in
guard let self else { return }
self.wantsRunning = true
do {
if !self.configured {
self.capture.beginConfiguration()
defer { self.capture.commitConfiguration() }
guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
throw BridgeError.message("Задняя камера не найдена. На симуляторе сканирование недоступно.")
}
let input = try AVCaptureDeviceInput(device: camera)
let output = AVCaptureMetadataOutput()
guard self.capture.canAddInput(input), self.capture.canAddOutput(output) else {
throw BridgeError.message("Не удалось запустить камеру. Закройте сканер и повторите попытку.")
}
self.capture.addInput(input); self.capture.addOutput(output)
guard output.availableMetadataObjectTypes.contains(.qr) else { throw BridgeError.message("Сканирование QR недоступно.") }
output.setMetadataObjectsDelegate(self, queue: .main)
output.metadataObjectTypes = [.qr]
self.configured = true
}
if self.wantsRunning { self.capture.startRunning() }
} catch {
DispatchQueue.main.async { [weak self] in
guard let self, self.visible else { return }
self.failed(error.localizedDescription)
}
}
}
}
func stop() {
visible = false
queue.async { [weak self] in
guard let self else { return }
self.wantsRunning = false
if self.capture.isRunning { self.capture.stopRunning() }
}
}
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput objects: [AVMetadataObject], from connection: AVCaptureConnection) {
guard visible, !delivered, let text = objects.compactMap({ ($0 as? AVMetadataMachineReadableCodeObject)?.stringValue }).first else { return }
delivered = true
stop()
receive(text)
}
}