Skip to content
Open
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
15 changes: 14 additions & 1 deletion Sources/CodeScanner/CodeScanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -90,13 +91,15 @@ public struct CodeScannerView: UIViewControllerRepresentable {
public var isGalleryPresented: Binding<Bool>
public var videoCaptureDevice: AVCaptureDevice?
public var completion: (Result<ScanResult, ScanError>) -> Void
private(set) var currentViewfinderStyle: AnyScannerViewfinderStyle = .init(style: .default)

public init(
codeTypes: [AVMetadataObject.ObjectType],
scanMode: ScanMode = .once,
manualSelect: Bool = false,
scanInterval: Double = 2.0,
showViewfinder: Bool = false,
useViewfinderAsRectOfInterest: Bool = false,
requiresPhotoOutput: Bool = true,
simulatedData: String = "",
shouldVibrateOnSuccess: Bool = true,
Expand All @@ -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
Expand All @@ -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) {
Expand All @@ -135,6 +143,11 @@ public struct CodeScannerView: UIViewControllerRepresentable {
)
}

public func viewfinderStyle<S>(_ style: S) -> Self where S: ScannerViewfinderStyle {
var copy = self
copy.currentViewfinderStyle = .init(style: style)
return copy
}
}

@available(macCatalyst 14.0, *)
Expand Down
68 changes: 48 additions & 20 deletions Sources/CodeScanner/ScannerViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

#if os(iOS)
import AVFoundation
import UIKit
import SwiftUI

@available(macCatalyst 14.0, *)
extension CodeScannerView {
Expand All @@ -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)

Expand All @@ -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)
}

Expand Down Expand Up @@ -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<AnyView> = {
let vc = UIHostingController(rootView: parentView.currentViewfinderStyle.makeBody())
vc.view.translatesAutoresizingMaskIntoConstraints = false
vc.view.backgroundColor = .clear
return vc
}()

private lazy var manualCaptureButton: UIButton = {
Expand Down Expand Up @@ -144,6 +148,7 @@ extension CodeScannerView {

override public func viewWillLayoutSubviews() {
previewLayer?.frame = view.layer.bounds
updateRectOfInterest()
}

@objc func updateOrientation() {
Expand Down Expand Up @@ -187,7 +192,8 @@ extension CodeScannerView {
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
addViewFinder()

updateRectOfInterest()

reset()

if !captureSession.isRunning {
Expand Down Expand Up @@ -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),
])
}

Expand Down
44 changes: 44 additions & 0 deletions Sources/CodeScanner/ScannerViewfinderStyle.swift
Original file line number Diff line number Diff line change
@@ -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<S>(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