From 46b789c9ad3cc272c19c74b8b65f5e9ba845e8be Mon Sep 17 00:00:00 2001 From: "bartlomiej.bukowieck" Date: Thu, 23 Jan 2025 14:20:58 +0100 Subject: [PATCH 1/3] Add ViewfinderOptions for control over Viewfinder image, it's size. Add support for AVCaptureMetadataOutput .rectOfInterest based on viewfinder's view. --- Sources/CodeScanner/CodeScanner.swift | 9 ++++- .../CodeScanner/ScannerViewController.swift | 37 +++++++++++++++---- Sources/CodeScanner/ViewfinderOptions.swift | 28 ++++++++++++++ 3 files changed, 66 insertions(+), 8 deletions(-) create mode 100644 Sources/CodeScanner/ViewfinderOptions.swift diff --git a/Sources/CodeScanner/CodeScanner.swift b/Sources/CodeScanner/CodeScanner.swift index 85b7994..6ad7f66 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 viewfinderOptions: ViewfinderOptions public let requiresPhotoOutput: Bool public var simulatedData = "" public var shouldVibrateOnSuccess: Bool @@ -97,6 +98,7 @@ public struct CodeScannerView: UIViewControllerRepresentable { manualSelect: Bool = false, scanInterval: Double = 2.0, showViewfinder: Bool = false, + viewfinderOptions: ViewfinderOptions = .default, requiresPhotoOutput: Bool = true, simulatedData: String = "", shouldVibrateOnSuccess: Bool = true, @@ -110,6 +112,7 @@ public struct CodeScannerView: UIViewControllerRepresentable { self.scanMode = scanMode self.manualSelect = manualSelect self.showViewfinder = showViewfinder + self.viewfinderOptions = viewfinderOptions self.requiresPhotoOutput = requiresPhotoOutput self.scanInterval = scanInterval self.simulatedData = simulatedData @@ -122,7 +125,11 @@ public struct CodeScannerView: UIViewControllerRepresentable { } public func makeUIViewController(context: Context) -> ScannerViewController { - return ScannerViewController(showViewfinder: showViewfinder, parentView: self) + return ScannerViewController( + showViewfinder: showViewfinder, + viewfinderOptions: viewfinderOptions, + parentView: self + ) } public func updateUIViewController(_ uiViewController: ScannerViewController, context: Context) { diff --git a/Sources/CodeScanner/ScannerViewController.swift b/Sources/CodeScanner/ScannerViewController.swift index 292194a..03b6915 100644 --- a/Sources/CodeScanner/ScannerViewController.swift +++ b/Sources/CodeScanner/ScannerViewController.swift @@ -22,6 +22,7 @@ extension CodeScannerView { var didFinishScanning = false var lastTime = Date(timeIntervalSince1970: 0) private let showViewfinder: Bool + private let viewfinderOptions: ViewfinderOptions let fallbackVideoCaptureDevice = AVCaptureDevice.default(for: .video) @@ -34,14 +35,20 @@ extension CodeScannerView { } } - public init(showViewfinder: Bool = false, parentView: CodeScannerView) { + public init( + showViewfinder: Bool = false, + viewfinderOptions: ViewfinderOptions = .default, + parentView: CodeScannerView + ) { self.parentView = parentView self.showViewfinder = showViewfinder + self.viewfinderOptions = viewfinderOptions super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { self.showViewfinder = false + self.viewfinderOptions = .default super.init(coder: coder) } @@ -106,9 +113,8 @@ extension CodeScannerView { var previewLayer: AVCaptureVideoPreviewLayer! private lazy var viewFinder: UIImageView? = { - guard let image = UIImage(named: "viewfinder", in: .module, with: nil) else { - return nil - } + guard let image = viewfinderOptions.customImage ?? UIImage(named: "viewfinder", in: .module, with: nil) + else { return nil } let imageView = UIImageView(image: image) imageView.translatesAutoresizingMaskIntoConstraints = false @@ -144,6 +150,7 @@ extension CodeScannerView { override public func viewWillLayoutSubviews() { previewLayer?.frame = view.layer.bounds + updateRectOfInterest() } @objc func updateOrientation() { @@ -187,7 +194,8 @@ extension CodeScannerView { previewLayer.videoGravity = .resizeAspectFill view.layer.addSublayer(previewLayer) addViewFinder() - + updateRectOfInterest() + reset() if !captureSession.isRunning { @@ -276,6 +284,21 @@ extension CodeScannerView { return } } + + private func updateRectOfInterest() { + guard viewfinderOptions.useAsRectOfInterest && previewLayer != nil, let captureSession else { + return + } + + let rectSize = viewfinderOptions.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 } @@ -285,8 +308,8 @@ extension CodeScannerView { NSLayoutConstraint.activate([ imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor), imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - imageView.widthAnchor.constraint(equalToConstant: 200), - imageView.heightAnchor.constraint(equalToConstant: 200), + imageView.widthAnchor.constraint(equalToConstant: viewfinderOptions.size.width), + imageView.heightAnchor.constraint(equalToConstant: viewfinderOptions.size.height), ]) } diff --git a/Sources/CodeScanner/ViewfinderOptions.swift b/Sources/CodeScanner/ViewfinderOptions.swift new file mode 100644 index 0000000..17c5201 --- /dev/null +++ b/Sources/CodeScanner/ViewfinderOptions.swift @@ -0,0 +1,28 @@ +// +// ViewfinderOptions.swift +// CodeScanner +// +// Created by Bartłomiej Bukowiecki on 23/01/2025. +// + +#if os(iOS) +import UIKit + +public struct ViewfinderOptions { + public let customImage: UIImage? + public let size: CGSize + public let useAsRectOfInterest: Bool + + public static let `default`: ViewfinderOptions = .init(size: CGSize(width: 200, height: 200)) + + public init( + customImage: UIImage? = nil, + size: CGSize, + useAsRectOfInterest: Bool = false + ) { + self.customImage = customImage + self.size = size + self.useAsRectOfInterest = useAsRectOfInterest + } +} +#endif From f23084854513e3a22d82242e3d43adee5b7bdf85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Bukowiecki?= Date: Tue, 28 Jan 2025 16:31:56 +0100 Subject: [PATCH 2/3] More SwiftUI-like approach. --- Sources/CodeScanner/CodeScanner.swift | 14 +++-- .../CodeScanner/ScannerViewController.swift | 51 ++++++++++--------- .../CodeScanner/ScannerViewfinderStyle.swift | 44 ++++++++++++++++ Sources/CodeScanner/ViewfinderOptions.swift | 28 ---------- 4 files changed, 82 insertions(+), 55 deletions(-) create mode 100644 Sources/CodeScanner/ScannerViewfinderStyle.swift delete mode 100644 Sources/CodeScanner/ViewfinderOptions.swift diff --git a/Sources/CodeScanner/CodeScanner.swift b/Sources/CodeScanner/CodeScanner.swift index 6ad7f66..7b6c852 100644 --- a/Sources/CodeScanner/CodeScanner.swift +++ b/Sources/CodeScanner/CodeScanner.swift @@ -82,7 +82,7 @@ public struct CodeScannerView: UIViewControllerRepresentable { public let manualSelect: Bool public let scanInterval: Double public let showViewfinder: Bool - public let viewfinderOptions: ViewfinderOptions + public let useViewfinderAsRectOfInterest: Bool public let requiresPhotoOutput: Bool public var simulatedData = "" public var shouldVibrateOnSuccess: Bool @@ -91,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], @@ -98,7 +99,7 @@ public struct CodeScannerView: UIViewControllerRepresentable { manualSelect: Bool = false, scanInterval: Double = 2.0, showViewfinder: Bool = false, - viewfinderOptions: ViewfinderOptions = .default, + useViewfinderAsRectOfInterest: Bool = false, requiresPhotoOutput: Bool = true, simulatedData: String = "", shouldVibrateOnSuccess: Bool = true, @@ -112,7 +113,7 @@ public struct CodeScannerView: UIViewControllerRepresentable { self.scanMode = scanMode self.manualSelect = manualSelect self.showViewfinder = showViewfinder - self.viewfinderOptions = viewfinderOptions + self.useViewfinderAsRectOfInterest = useViewfinderAsRectOfInterest self.requiresPhotoOutput = requiresPhotoOutput self.scanInterval = scanInterval self.simulatedData = simulatedData @@ -127,7 +128,7 @@ public struct CodeScannerView: UIViewControllerRepresentable { public func makeUIViewController(context: Context) -> ScannerViewController { return ScannerViewController( showViewfinder: showViewfinder, - viewfinderOptions: viewfinderOptions, + useViewfinderAsRectOfInterest: useViewfinderAsRectOfInterest, parentView: self ) } @@ -142,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 03b6915..9c4f8a0 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,7 +22,7 @@ extension CodeScannerView { var didFinishScanning = false var lastTime = Date(timeIntervalSince1970: 0) private let showViewfinder: Bool - private let viewfinderOptions: ViewfinderOptions + private let useViewfinderAsRectOfInterest: Bool let fallbackVideoCaptureDevice = AVCaptureDevice.default(for: .video) @@ -37,18 +37,18 @@ extension CodeScannerView { public init( showViewfinder: Bool = false, - viewfinderOptions: ViewfinderOptions = .default, + useViewfinderAsRectOfInterest: Bool = false, parentView: CodeScannerView ) { self.parentView = parentView self.showViewfinder = showViewfinder - self.viewfinderOptions = viewfinderOptions + self.useViewfinderAsRectOfInterest = useViewfinderAsRectOfInterest super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { self.showViewfinder = false - self.viewfinderOptions = .default + self.useViewfinderAsRectOfInterest = false super.init(coder: coder) } @@ -111,14 +111,12 @@ extension CodeScannerView { var captureSession: AVCaptureSession? var previewLayer: AVCaptureVideoPreviewLayer! - - private lazy var viewFinder: UIImageView? = { - guard let image = viewfinderOptions.customImage ?? 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 = { @@ -286,11 +284,11 @@ extension CodeScannerView { } private func updateRectOfInterest() { - guard viewfinderOptions.useAsRectOfInterest && previewLayer != nil, let captureSession else { + guard let captureSession, showViewfinder && useViewfinderAsRectOfInterest && previewLayer != nil else { return } - let rectSize = viewfinderOptions.size + let rectSize = viewFinder.sizeThatFits(in: view.bounds.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)) @@ -301,15 +299,22 @@ extension CodeScannerView { } 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: viewfinderOptions.size.width), - imageView.heightAnchor.constraint(equalToConstant: viewfinderOptions.size.height), + 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 diff --git a/Sources/CodeScanner/ViewfinderOptions.swift b/Sources/CodeScanner/ViewfinderOptions.swift deleted file mode 100644 index 17c5201..0000000 --- a/Sources/CodeScanner/ViewfinderOptions.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// ViewfinderOptions.swift -// CodeScanner -// -// Created by Bartłomiej Bukowiecki on 23/01/2025. -// - -#if os(iOS) -import UIKit - -public struct ViewfinderOptions { - public let customImage: UIImage? - public let size: CGSize - public let useAsRectOfInterest: Bool - - public static let `default`: ViewfinderOptions = .init(size: CGSize(width: 200, height: 200)) - - public init( - customImage: UIImage? = nil, - size: CGSize, - useAsRectOfInterest: Bool = false - ) { - self.customImage = customImage - self.size = size - self.useAsRectOfInterest = useAsRectOfInterest - } -} -#endif From 35b9b51dc5b2a41e02e539fb793f2ff142426cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Bukowiecki?= Date: Wed, 29 Jan 2025 09:35:54 +0100 Subject: [PATCH 3/3] typo --- Sources/CodeScanner/ScannerViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CodeScanner/ScannerViewController.swift b/Sources/CodeScanner/ScannerViewController.swift index 9c4f8a0..d6e8fc7 100644 --- a/Sources/CodeScanner/ScannerViewController.swift +++ b/Sources/CodeScanner/ScannerViewController.swift @@ -288,7 +288,7 @@ extension CodeScannerView { return } - let rectSize = viewFinder.sizeThatFits(in: view.bounds.size) + 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))