Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 1 addition & 18 deletions Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
968FE1402DFB016B0053CD7F /* LDKNode in Frameworks */ = {isa = PBXBuildFile; productRef = 968FE13F2DFB016B0053CD7F /* LDKNode */; };
96DEA03A2DE8BBA1009932BF /* BitkitCore in Frameworks */ = {isa = PBXBuildFile; productRef = 96DEA0392DE8BBA1009932BF /* BitkitCore */; };
96DEA03C2DE8BBAB009932BF /* BitkitCore in Frameworks */ = {isa = PBXBuildFile; productRef = 96DEA03B2DE8BBAB009932BF /* BitkitCore */; };
96E20CD42CB6D91A00C24149 /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 96E20CD32CB6D91A00C24149 /* CodeScanner */; };
96E493A42C942FD1000E8BC2 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 96E493A32C942FD1000E8BC2 /* secp256k1 */; };
96E493A62C94317D000E8BC2 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 96E493A52C94317D000E8BC2 /* secp256k1 */; };
96E493A82C943184000E8BC2 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 96E493A72C943184000E8BC2 /* secp256k1 */; };
Expand Down Expand Up @@ -272,7 +271,6 @@
18D65E002EB964B500252335 /* VssRustClientFfi in Frameworks */,
96E493A42C942FD1000E8BC2 /* secp256k1 in Frameworks */,
96DEA03A2DE8BBA1009932BF /* BitkitCore in Frameworks */,
96E20CD42CB6D91A00C24149 /* CodeScanner in Frameworks */,
4AAB08CA2E1FE77600BA63DF /* Lottie in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -403,7 +401,6 @@
packageProductDependencies = (
9613018B2C5022D700878183 /* LDKNode */,
96E493A32C942FD1000E8BC2 /* secp256k1 */,
96E20CD32CB6D91A00C24149 /* CodeScanner */,
96DEA0392DE8BBA1009932BF /* BitkitCore */,
4AAB08C92E1FE77600BA63DF /* Lottie */,
18D65DFF2EB964B500252335 /* VssRustClientFfi */,
Expand Down Expand Up @@ -512,7 +509,6 @@
mainGroup = 96FE1F582C2DE6AA006D0C8B;
packageReferences = (
96E493A22C942FD1000E8BC2 /* XCRemoteSwiftPackageReference "swift-secp256k1" */,
96E20CD22CB6D91A00C24149 /* XCRemoteSwiftPackageReference "CodeScanner" */,
96DEA0382DE8BBA1009932BF /* XCRemoteSwiftPackageReference "bitkit-core" */,
968FE13E2DFB016B0053CD7F /* XCRemoteSwiftPackageReference "ldk-node" */,
4AAB08C82E1FE77600BA63DF /* XCRemoteSwiftPackageReference "lottie-ios" */,
Expand Down Expand Up @@ -1214,15 +1210,7 @@
repositoryURL = "https://github.com/synonymdev/bitkit-core";
requirement = {
kind = exactVersion;
version = 0.5.14;
};
};
96E20CD22CB6D91A00C24149 /* XCRemoteSwiftPackageReference "CodeScanner" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/twostraws/CodeScanner";
requirement = {
kind = exactVersion;
version = 2.5.2;
version = 0.5.18;
};
};
96E493A22C942FD1000E8BC2 /* XCRemoteSwiftPackageReference "swift-secp256k1" */ = {
Expand Down Expand Up @@ -1276,11 +1264,6 @@
package = 96DEA0382DE8BBA1009932BF /* XCRemoteSwiftPackageReference "bitkit-core" */;
productName = BitkitCore;
};
96E20CD32CB6D91A00C24149 /* CodeScanner */ = {
isa = XCSwiftPackageProductDependency;
package = 96E20CD22CB6D91A00C24149 /* XCRemoteSwiftPackageReference "CodeScanner" */;
productName = CodeScanner;
};
96E493A32C942FD1000E8BC2 /* secp256k1 */ = {
isa = XCSwiftPackageProductDependency;
package = 96E493A22C942FD1000E8BC2 /* XCRemoteSwiftPackageReference "swift-secp256k1" */;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

201 changes: 184 additions & 17 deletions Bitkit/Components/Scanner.swift
Original file line number Diff line number Diff line change
@@ -1,22 +1,185 @@
import CodeScanner
import AVFoundation
import PhotosUI
import SwiftUI
import Vision

// MARK: - Scanner Camera Component

private struct ScannerCamera: View {
private struct ScannerCamera: UIViewControllerRepresentable {
let isTorchOn: Bool
let onScan: (String) async -> Void
let onScan: (QRCodePayload) async -> Void

var body: some View {
CodeScannerView(codeTypes: [.qr], shouldVibrateOnSuccess: false, isTorchOn: isTorchOn) { response in
if case let .success(result) = response {
Task {
await onScan(result.string)
func makeUIViewController(context _: Context) -> QRCodeScannerViewController {
QRCodeScannerViewController { payload in
Task {
await onScan(payload)
}
}
}

func updateUIViewController(_ controller: QRCodeScannerViewController, context _: Context) {
controller.setTorch(isOn: isTorchOn)
}
}

private final class QRCodeScannerViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate {
private let captureSession = AVCaptureSession()
private let sessionQueue = DispatchQueue(label: "to.bitkit.qr-scanner.session")
private let videoQueue = DispatchQueue(label: "to.bitkit.qr-scanner.video")
private let onScan: (QRCodePayload) -> Void
private var captureDevice: AVCaptureDevice?
private var isTorchRequested = false
private var framesWithoutQRCode = 0
private var isProcessingFrame = false
private var lastPayloadData: Data?
private var lastPayloadString: String?
private lazy var previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)

init(onScan: @escaping (QRCodePayload) -> Void) {
self.onScan = onScan
super.init(nibName: nil, bundle: nil)
}

@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
configureCaptureSession()
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
previewLayer.frame = view.bounds
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
sessionQueue.async { [weak self] in
guard let self, !captureSession.isRunning else { return }
captureSession.startRunning()
}
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
sessionQueue.async { [weak self] in
guard let self, captureSession.isRunning else { return }
captureSession.stopRunning()
}
}

func setTorch(isOn: Bool) {
sessionQueue.async { [weak self] in
guard let self else { return }
isTorchRequested = isOn
applyTorchState()
}
}

private func applyTorchState() {
guard let captureDevice, captureDevice.hasTorch else { return }
Comment thread
pwltr marked this conversation as resolved.

do {
try captureDevice.lockForConfiguration()
captureDevice.torchMode = isTorchRequested ? .on : .off
captureDevice.unlockForConfiguration()
} catch {
Logger.error(error, context: "QR scanner torch")
}
}

private func configureCaptureSession() {
sessionQueue.async { [weak self] in
guard let self else { return }

captureSession.beginConfiguration()
defer { captureSession.commitConfiguration() }
captureSession.sessionPreset = .high

guard let device = AVCaptureDevice.default(
.builtInWideAngleCamera,
for: .video,
position: .back
) else {
Logger.error("Failed to find QR scanner camera")
return
}

do {
let input = try AVCaptureDeviceInput(device: device)
guard captureSession.canAddInput(input) else {
Logger.error("Failed to add QR scanner camera input")
return
}
} else if case let .failure(error) = response {
Logger.error(error, context: "CodeScanner")
captureSession.addInput(input)
captureDevice = device
applyTorchState()
} catch {
Logger.error(error, context: "QR scanner camera input")
return
}

let output = AVCaptureVideoDataOutput()
output.alwaysDiscardsLateVideoFrames = true
output.setSampleBufferDelegate(self, queue: videoQueue)
guard captureSession.canAddOutput(output) else {
Logger.error("Failed to add QR scanner video output")
return
}
captureSession.addOutput(output)
}
}

func captureOutput(
_: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from _: AVCaptureConnection
) {
guard !isProcessingFrame else { return }
isProcessingFrame = true
defer { isProcessingFrame = false }

let request = VNDetectBarcodesRequest()
request.symbologies = [.qr]

do {
let handler = VNImageRequestHandler(
cmSampleBuffer: sampleBuffer,
orientation: .right,
options: [:]
)
try handler.perform([request])
} catch {
Logger.error(error, context: "QR scanner frame detection")
return
}

guard let observation = request.results?.first else {
framesWithoutQRCode += 1
if framesWithoutQRCode >= 10 {
lastPayloadData = nil
lastPayloadString = nil
}
return
}

framesWithoutQRCode = 0
let payload = QRCodePayload(
string: observation.payloadStringValue,
data: observation.payloadData
)
guard payload.data != lastPayloadData || payload.string != lastPayloadString else { return }
Comment thread
pwltr marked this conversation as resolved.

lastPayloadData = payload.data
lastPayloadString = payload.string
DispatchQueue.main.async { [onScan] in
onScan(payload)
}
}
}
Expand Down Expand Up @@ -65,20 +228,24 @@ private struct ScannerCornerButtons: View {
struct Scanner: View {
@Environment(CameraManager.self) private var cameraManager

let onScan: (String) async -> Void
let onScan: (QRCodePayload) async -> Void
let onImageSelection: (PhotosPickerItem?) async -> Void

@State private var isTorchOn = false

var body: some View {
ZStack {
if cameraManager.hasPermission {
ScannerCamera(
isTorchOn: isTorchOn,
onScan: { uri in
await onScan(uri)
}
)
#if targetEnvironment(simulator)
Color.black
#else
ScannerCamera(
isTorchOn: isTorchOn,
onScan: { payload in
await onScan(payload)
}
)
#endif

ScannerCornerButtons(
isTorchOn: $isTorchOn,
Expand Down
Loading
Loading