diff --git a/Sources/CodeScanner/CodeScanner.swift b/Sources/CodeScanner/CodeScanner.swift index 85b7994..7b6c852 100644 --- a/Sources/CodeScanner/CodeScanner.swift +++ b/Sources/CodeScanner/CodeScanner.swift @@ -82,6 +82,7 @@ public struct CodeScannerView: UIViewControllerRepresentable { public let manualSelect: Bool public let scanInterval: Double public let showViewfinder: Bool + public let useViewfinderAsRectOfInterest: Bool public let requiresPhotoOutput: Bool public var simulatedData = "" public var shouldVibrateOnSuccess: Bool @@ -90,6 +91,7 @@ public struct CodeScannerView: UIViewControllerRepresentable { public var isGalleryPresented: Binding public var videoCaptureDevice: AVCaptureDevice? public var completion: (Result) -> Void + private(set) var currentViewfinderStyle: AnyScannerViewfinderStyle = .init(style: .default) public init( codeTypes: [AVMetadataObject.ObjectType], @@ -97,6 +99,7 @@ public struct CodeScannerView: UIViewControllerRepresentable { manualSelect: Bool = false, scanInterval: Double = 2.0, showViewfinder: Bool = false, + useViewfinderAsRectOfInterest: Bool = false, requiresPhotoOutput: Bool = true, simulatedData: String = "", shouldVibrateOnSuccess: Bool = true, @@ -110,6 +113,7 @@ public struct CodeScannerView: UIViewControllerRepresentable { self.scanMode = scanMode self.manualSelect = manualSelect self.showViewfinder = showViewfinder + self.useViewfinderAsRectOfInterest = useViewfinderAsRectOfInterest self.requiresPhotoOutput = requiresPhotoOutput self.scanInterval = scanInterval self.simulatedData = simulatedData @@ -122,7 +126,11 @@ public struct CodeScannerView: UIViewControllerRepresentable { } public func makeUIViewController(context: Context) -> ScannerViewController { - return ScannerViewController(showViewfinder: showViewfinder, parentView: self) + return ScannerViewController( + showViewfinder: showViewfinder, + useViewfinderAsRectOfInterest: useViewfinderAsRectOfInterest, + parentView: self + ) } public func updateUIViewController(_ uiViewController: ScannerViewController, context: Context) { @@ -135,6 +143,11 @@ public struct CodeScannerView: UIViewControllerRepresentable { ) } + public func viewfinderStyle(_ style: S) -> Self where S: ScannerViewfinderStyle { + var copy = self + copy.currentViewfinderStyle = .init(style: style) + return copy + } } @available(macCatalyst 14.0, *) diff --git a/Sources/CodeScanner/ScannerViewController.swift b/Sources/CodeScanner/ScannerViewController.swift index 292194a..d6e8fc7 100644 --- a/Sources/CodeScanner/ScannerViewController.swift +++ b/Sources/CodeScanner/ScannerViewController.swift @@ -8,7 +8,7 @@ #if os(iOS) import AVFoundation -import UIKit +import SwiftUI @available(macCatalyst 14.0, *) extension CodeScannerView { @@ -22,6 +22,7 @@ extension CodeScannerView { var didFinishScanning = false var lastTime = Date(timeIntervalSince1970: 0) private let showViewfinder: Bool + private let useViewfinderAsRectOfInterest: Bool let fallbackVideoCaptureDevice = AVCaptureDevice.default(for: .video) @@ -34,14 +35,20 @@ extension CodeScannerView { } } - public init(showViewfinder: Bool = false, parentView: CodeScannerView) { + public init( + showViewfinder: Bool = false, + useViewfinderAsRectOfInterest: Bool = false, + parentView: CodeScannerView + ) { self.parentView = parentView self.showViewfinder = showViewfinder + self.useViewfinderAsRectOfInterest = useViewfinderAsRectOfInterest super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { self.showViewfinder = false + self.useViewfinderAsRectOfInterest = false super.init(coder: coder) } @@ -104,15 +111,12 @@ extension CodeScannerView { var captureSession: AVCaptureSession? var previewLayer: AVCaptureVideoPreviewLayer! - - private lazy var viewFinder: UIImageView? = { - guard let image = UIImage(named: "viewfinder", in: .module, with: nil) else { - return nil - } - - let imageView = UIImageView(image: image) - imageView.translatesAutoresizingMaskIntoConstraints = false - return imageView + + private lazy var viewFinder: UIHostingController = { + let vc = UIHostingController(rootView: parentView.currentViewfinderStyle.makeBody()) + vc.view.translatesAutoresizingMaskIntoConstraints = false + vc.view.backgroundColor = .clear + return vc }() private lazy var manualCaptureButton: UIButton = { @@ -144,6 +148,7 @@ extension CodeScannerView { override public func viewWillLayoutSubviews() { previewLayer?.frame = view.layer.bounds + updateRectOfInterest() } @objc func updateOrientation() { @@ -187,7 +192,8 @@ extension CodeScannerView { previewLayer.videoGravity = .resizeAspectFill view.layer.addSublayer(previewLayer) addViewFinder() - + updateRectOfInterest() + reset() if !captureSession.isRunning { @@ -276,17 +282,39 @@ extension CodeScannerView { return } } + + private func updateRectOfInterest() { + guard let captureSession, showViewfinder && useViewfinderAsRectOfInterest && previewLayer != nil else { + return + } + + let rectSize = viewFinder.view.frame.size + let rectPointOnLayer = CGPoint(x: previewLayer.frame.midX - (rectSize.width / 2), + y: previewLayer.frame.midY - (rectSize.height / 2)) + let rect = previewLayer.metadataOutputRectConverted(fromLayerRect: CGRect(origin: rectPointOnLayer, size: rectSize)) + + captureSession.outputs.compactMap { $0 as? AVCaptureMetadataOutput }.forEach { + $0.rectOfInterest = rect + } + } private func addViewFinder() { - guard showViewfinder, let imageView = viewFinder else { return } - - view.addSubview(imageView) - + guard showViewfinder else { return } + + let viewfinderVC = viewFinder + + viewfinderVC.willMove(toParent: self) + addChild(viewfinderVC) + view.addSubview(viewfinderVC.view) + viewfinderVC.didMove(toParent: self) + + let desiredSize = viewfinderVC.sizeThatFits(in: view.bounds.size) + NSLayoutConstraint.activate([ - imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor), - imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - imageView.widthAnchor.constraint(equalToConstant: 200), - imageView.heightAnchor.constraint(equalToConstant: 200), + viewfinderVC.view.centerYAnchor.constraint(equalTo: view.centerYAnchor), + viewfinderVC.view.centerXAnchor.constraint(equalTo: view.centerXAnchor), + viewfinderVC.view.widthAnchor.constraint(equalToConstant: desiredSize.width), + viewfinderVC.view.heightAnchor.constraint(equalToConstant: desiredSize.height), ]) } diff --git a/Sources/CodeScanner/ScannerViewfinderStyle.swift b/Sources/CodeScanner/ScannerViewfinderStyle.swift new file mode 100644 index 0000000..714edc6 --- /dev/null +++ b/Sources/CodeScanner/ScannerViewfinderStyle.swift @@ -0,0 +1,44 @@ +// +// ScannerViewfinderStyle.swift +// CodeScanner +// +// Created by Bartłomiej Bukowiecki on 28/01/2025. +// + +#if os(iOS) +import SwiftUI + +public protocol ScannerViewfinderStyle { + associatedtype Content: View + + @ViewBuilder func makeBody() -> Content +} + +struct AnyScannerViewfinderStyle: ScannerViewfinderStyle { + private let wrappedBody: () -> AnyView + + init(style: S) where S: ScannerViewfinderStyle { + self.wrappedBody = { + AnyView(style.makeBody()) + } + } + + func makeBody() -> AnyView { + wrappedBody() + } +} + +public struct DefaultScannerViewfinderStyle: ScannerViewfinderStyle { + public func makeBody() -> some View { + Image("viewfinder", bundle: .module) + .resizable() + .frame(width: 200, height: 200) + } +} + +extension ScannerViewfinderStyle where Self == DefaultScannerViewfinderStyle { + public static var `default`: DefaultScannerViewfinderStyle { + DefaultScannerViewfinderStyle() + } +} +#endif