QR: add iOS camera scanner using AVFoundation; integrate into Scan view; add NSCameraUsageDescription to Info.plist

This commit is contained in:
jack
2025-08-24 18:23:37 +02:00
parent 0c7054ce30
commit 81fa77b761
2 changed files with 96 additions and 1 deletions
+2
View File
@@ -37,6 +37,8 @@
<string>bitchat uses Bluetooth to discover and connect with other bitchat users nearby.</string> <string>bitchat uses Bluetooth to discover and connect with other bitchat users nearby.</string>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string>bitchat uses your approximate location to compute local geohash channels for optional public chats. Exact GPS is never shared.</string> <string>bitchat uses your approximate location to compute local geohash channels for optional public chats. Exact GPS is never shared.</string>
<key>NSCameraUsageDescription</key>
<string>bitchat uses the camera to scan QR codes to verify peers.</string>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>bluetooth-central</string> <string>bluetooth-central</string>
+94 -1
View File
@@ -104,9 +104,25 @@ struct ImageWrapper: View {
struct QRScanView: View { struct QRScanView: View {
@State private var input = "" @State private var input = ""
@State private var result: String = "" @State private var result: String = ""
@State private var lastValid: String = ""
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
Text("Paste QR content to simulate scan:") #if os(iOS)
Text("Scan a friend's QR")
.font(.system(size: 14, weight: .medium, design: .monospaced))
CameraScannerView { code in
if let qr = VerificationService.shared.verifyScannedQR(code) {
result = "Valid QR: \(qr.nickname)"
lastValid = code
} else {
result = "Invalid or expired QR"
}
}
.frame(height: 260)
.clipShape(RoundedRectangle(cornerRadius: 8))
.overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.gray.opacity(0.3), lineWidth: 1))
#else
Text("Paste QR content to validate:")
.font(.system(size: 14, weight: .medium, design: .monospaced)) .font(.system(size: 14, weight: .medium, design: .monospaced))
TextEditor(text: $input) TextEditor(text: $input)
.frame(height: 100) .frame(height: 100)
@@ -119,11 +135,88 @@ struct QRScanView: View {
} }
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
#endif
Text(result) Text(result)
.font(.system(size: 12, design: .monospaced)) .font(.system(size: 12, design: .monospaced))
.foregroundColor(result.contains("Valid") ? .green : .orange) .foregroundColor(result.contains("Valid") ? .green : .orange)
if !lastValid.isEmpty {
Text(lastValid)
.font(.system(size: 10, design: .monospaced))
.textSelection(.enabled)
.foregroundColor(.secondary)
}
Spacer() Spacer()
} }
.padding() .padding()
} }
} }
#if os(iOS)
import AVFoundation
struct CameraScannerView: UIViewRepresentable {
typealias UIViewType = PreviewView
var onCode: (String) -> Void
func makeUIView(context: Context) -> PreviewView {
let view = PreviewView()
context.coordinator.setup(sessionOwner: view, onCode: onCode)
return view
}
func updateUIView(_ uiView: PreviewView, context: Context) {}
func makeCoordinator() -> Coordinator { Coordinator() }
final class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
private var onCode: ((String) -> Void)?
private weak var owner: PreviewView?
private let session = AVCaptureSession()
func setup(sessionOwner: PreviewView, onCode: @escaping (String) -> Void) {
self.owner = sessionOwner
self.onCode = onCode
session.beginConfiguration()
session.sessionPreset = .high
guard let device = AVCaptureDevice.default(for: .video),
let input = try? AVCaptureDeviceInput(device: device),
session.canAddInput(input) else { return }
session.addInput(input)
let output = AVCaptureMetadataOutput()
guard session.canAddOutput(output) else { return }
session.addOutput(output)
output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
if output.availableMetadataObjectTypes.contains(.qr) {
output.metadataObjectTypes = [.qr]
}
session.commitConfiguration()
sessionOwner.videoPreviewLayer.session = session
// Request permission and start
AVCaptureDevice.requestAccess(for: .video) { granted in
DispatchQueue.main.async {
if granted { self.session.startRunning() }
}
}
}
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
for obj in metadataObjects {
guard let m = obj as? AVMetadataMachineReadableCodeObject,
m.type == .qr,
let str = m.stringValue else { continue }
onCode?(str)
}
}
}
final class PreviewView: UIView {
override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }
var videoPreviewLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer }
override init(frame: CGRect) {
super.init(frame: frame)
videoPreviewLayer.videoGravity = .resizeAspectFill
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
}
#endif