From 4a68dec5d382dfa102dcfeb9bf71b67e443a2eb6 Mon Sep 17 00:00:00 2001 From: Vlad Suhomlinov Date: Fri, 23 Apr 2021 16:54:00 +0300 Subject: [PATCH] Add CardReader --- .gitignore | 2 + Example/Pods/Pods.xcodeproj/project.pbxproj | 110 ++++++++-- .../BaseReader.swift} | 202 +++++++----------- .../Classes/Core/Card/CardReader.swift | 128 +++++++++++ .../Core/Card/Helpers/CardFactory.swift | 34 +++ .../Classes/Core/QRCode/QRCodeReader.swift | 89 ++++++++ .../Classes/Views/Base/BaseReaderView.swift | 47 ++++ .../Views/CardReaderView/CardReaderView.swift | 48 +++++ .../Views/{ => QRCode}/QRCodeReaderView.swift | 23 +- .../FocusView.swift} | 4 +- .../OverlayView.swift} | 4 +- 11 files changed, 524 insertions(+), 167 deletions(-) rename QRCodeReader/Classes/Core/{QRCodeReader.swift => Base/BaseReader.swift} (60%) create mode 100644 QRCodeReader/Classes/Core/Card/CardReader.swift create mode 100644 QRCodeReader/Classes/Core/Card/Helpers/CardFactory.swift create mode 100644 QRCodeReader/Classes/Core/QRCode/QRCodeReader.swift create mode 100644 QRCodeReader/Classes/Views/Base/BaseReaderView.swift create mode 100644 QRCodeReader/Classes/Views/CardReaderView/CardReaderView.swift rename QRCodeReader/Classes/Views/{ => QRCode}/QRCodeReaderView.swift (80%) rename QRCodeReader/Classes/Views/{QRCodeFocusView.swift => Subviews/FocusView.swift} (96%) rename QRCodeReader/Classes/Views/{QRCodeOverlayView.swift => Subviews/OverlayView.swift} (96%) diff --git a/.gitignore b/.gitignore index 312d1f6..a0c50f3 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ xcuserdata/ *.moved-aside *.xccheckout *.xcscmblueprint +.DS_Store ## Obj-C/Swift specific *.hmap @@ -39,6 +40,7 @@ playground.xcworkspace # Package.pins # Package.resolved .build/ +.swiftpm/ # CocoaPods # diff --git a/Example/Pods/Pods.xcodeproj/project.pbxproj b/Example/Pods/Pods.xcodeproj/project.pbxproj index 5b14db8..6c80456 100644 --- a/Example/Pods/Pods.xcodeproj/project.pbxproj +++ b/Example/Pods/Pods.xcodeproj/project.pbxproj @@ -9,11 +9,16 @@ /* Begin PBXBuildFile section */ 043BA95FA64A9E13CF6DCAD8B77014D6 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB4607EFCA7C5F75397649E792E2AFCB /* Foundation.framework */; }; 0CC2C063233CDCBF009A2245 /* QRCodeReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC2C05E233CDCBF009A2245 /* QRCodeReader.swift */; }; - 0CC2C064233CDCBF009A2245 /* QRCodeOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC2C060233CDCBF009A2245 /* QRCodeOverlayView.swift */; }; - 0CC2C065233CDCBF009A2245 /* QRCodeReaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC2C061233CDCBF009A2245 /* QRCodeReaderView.swift */; }; - 0CC2C066233CDCBF009A2245 /* QRCodeFocusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC2C062233CDCBF009A2245 /* QRCodeFocusView.swift */; }; + 0CC2C064233CDCBF009A2245 /* OverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC2C060233CDCBF009A2245 /* OverlayView.swift */; }; + 0CC2C065233CDCBF009A2245 /* BaseReaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC2C061233CDCBF009A2245 /* BaseReaderView.swift */; }; + 0CC2C066233CDCBF009A2245 /* FocusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC2C062233CDCBF009A2245 /* FocusView.swift */; }; 130551717DB770EA0651FE8D93C23675 /* QRCodeReader-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 95E4F97DB9CA20CC853726128BB706A6 /* QRCodeReader-dummy.m */; }; 3B21E171721DD07BBAB5989E14FD624B /* QRCodeReader-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 5F505016E395D9F953FB3602BC202FD8 /* QRCodeReader-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4C172F3E2632F52700E66397 /* CardReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C172F3D2632F52700E66397 /* CardReader.swift */; }; + 4C172F432632FFF200E66397 /* BaseReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C172F422632FFF200E66397 /* BaseReader.swift */; }; + 4C172F4E2633032400E66397 /* QRCodeReaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C172F4D2633032400E66397 /* QRCodeReaderView.swift */; }; + 4C172F6E263303B000E66397 /* CardReaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C172F6D263303B000E66397 /* CardReaderView.swift */; }; + 4C172F77263306DD00E66397 /* CardFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C172F76263306DD00E66397 /* CardFactory.swift */; }; 4EEDD6031A9ACB58B426DEAF9EF13FB0 /* Pods-QRCodeReader_Tests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = EC559F1A4D837B9FD18823EDF5C5EB98 /* Pods-QRCodeReader_Tests-dummy.m */; }; 66390594843D889C2E6C56C16CBC0963 /* Pods-QRCodeReader_Example-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 1CB2ED5F7E3C6EBC896BFDE238BA51A1 /* Pods-QRCodeReader_Example-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; 929A90508F8A751ABD3901537D9B9FC3 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB4607EFCA7C5F75397649E792E2AFCB /* Foundation.framework */; }; @@ -42,9 +47,9 @@ /* Begin PBXFileReference section */ 0C7471F90A704F08CC0283FC0E4B57DC /* Pods-QRCodeReader_Example-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-QRCodeReader_Example-frameworks.sh"; sourceTree = ""; }; 0CC2C05E233CDCBF009A2245 /* QRCodeReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRCodeReader.swift; sourceTree = ""; }; - 0CC2C060233CDCBF009A2245 /* QRCodeOverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRCodeOverlayView.swift; sourceTree = ""; }; - 0CC2C061233CDCBF009A2245 /* QRCodeReaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRCodeReaderView.swift; sourceTree = ""; }; - 0CC2C062233CDCBF009A2245 /* QRCodeFocusView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRCodeFocusView.swift; sourceTree = ""; }; + 0CC2C060233CDCBF009A2245 /* OverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverlayView.swift; sourceTree = ""; }; + 0CC2C061233CDCBF009A2245 /* BaseReaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseReaderView.swift; sourceTree = ""; }; + 0CC2C062233CDCBF009A2245 /* FocusView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FocusView.swift; sourceTree = ""; }; 1C9D135A54BC4D3FFE5BEA77B04C802D /* Pods-QRCodeReader_Example-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-QRCodeReader_Example-dummy.m"; sourceTree = ""; }; 1CB2ED5F7E3C6EBC896BFDE238BA51A1 /* Pods-QRCodeReader_Example-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-QRCodeReader_Example-umbrella.h"; sourceTree = ""; }; 288313D69136D2B7605F9BB05E7A6C03 /* Pods_QRCodeReader_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_QRCodeReader_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -52,6 +57,11 @@ 436A72EE95B92D301E967BAEB928B9DD /* Pods-QRCodeReader_Example-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-QRCodeReader_Example-acknowledgements.markdown"; sourceTree = ""; }; 445930C222AB7DFC3993CFF4A9BBD021 /* QRCodeReader-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "QRCodeReader-prefix.pch"; sourceTree = ""; }; 46EC7F5010001EBDF8D51322836760E4 /* Pods-QRCodeReader_Example.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-QRCodeReader_Example.modulemap"; sourceTree = ""; }; + 4C172F3D2632F52700E66397 /* CardReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReader.swift; sourceTree = ""; }; + 4C172F422632FFF200E66397 /* BaseReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseReader.swift; sourceTree = ""; }; + 4C172F4D2633032400E66397 /* QRCodeReaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeReaderView.swift; sourceTree = ""; }; + 4C172F6D263303B000E66397 /* CardReaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardReaderView.swift; sourceTree = ""; }; + 4C172F76263306DD00E66397 /* CardFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardFactory.swift; sourceTree = ""; }; 59629FB82E167000859A076FA3816EBE /* Pods_QRCodeReader_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_QRCodeReader_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5C86CA70237BC75ACFBBCB9368BBC2C0 /* Pods-QRCodeReader_Tests-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-QRCodeReader_Tests-acknowledgements.plist"; sourceTree = ""; }; 5F505016E395D9F953FB3602BC202FD8 /* QRCodeReader-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "QRCodeReader-umbrella.h"; sourceTree = ""; }; @@ -108,7 +118,9 @@ 0CC2C05D233CDCBF009A2245 /* Core */ = { isa = PBXGroup; children = ( - 0CC2C05E233CDCBF009A2245 /* QRCodeReader.swift */, + 4C172F5B2633036F00E66397 /* Base */, + 4C172F632633038300E66397 /* QRCode */, + 4C172F5F2633037B00E66397 /* Card */, ); name = Core; path = QRCodeReader/Classes/Core; @@ -117,9 +129,10 @@ 0CC2C05F233CDCBF009A2245 /* Views */ = { isa = PBXGroup; children = ( - 0CC2C060233CDCBF009A2245 /* QRCodeOverlayView.swift */, - 0CC2C061233CDCBF009A2245 /* QRCodeReaderView.swift */, - 0CC2C062233CDCBF009A2245 /* QRCodeFocusView.swift */, + 4C172F522633035400E66397 /* Base */, + 4C172F532633035D00E66397 /* Subviews */, + 4C172F572633036700E66397 /* QRCode */, + 4C172F72263303B300E66397 /* CardReaderView */, ); name = Views; path = QRCodeReader/Classes/Views; @@ -170,6 +183,72 @@ path = ../..; sourceTree = ""; }; + 4C172F522633035400E66397 /* Base */ = { + isa = PBXGroup; + children = ( + 0CC2C061233CDCBF009A2245 /* BaseReaderView.swift */, + ); + path = Base; + sourceTree = ""; + }; + 4C172F532633035D00E66397 /* Subviews */ = { + isa = PBXGroup; + children = ( + 0CC2C062233CDCBF009A2245 /* FocusView.swift */, + 0CC2C060233CDCBF009A2245 /* OverlayView.swift */, + ); + path = Subviews; + sourceTree = ""; + }; + 4C172F572633036700E66397 /* QRCode */ = { + isa = PBXGroup; + children = ( + 4C172F4D2633032400E66397 /* QRCodeReaderView.swift */, + ); + path = QRCode; + sourceTree = ""; + }; + 4C172F5B2633036F00E66397 /* Base */ = { + isa = PBXGroup; + children = ( + 4C172F422632FFF200E66397 /* BaseReader.swift */, + ); + path = Base; + sourceTree = ""; + }; + 4C172F5F2633037B00E66397 /* Card */ = { + isa = PBXGroup; + children = ( + 4C172F7B263306EC00E66397 /* Helpers */, + 4C172F3D2632F52700E66397 /* CardReader.swift */, + ); + path = Card; + sourceTree = ""; + }; + 4C172F632633038300E66397 /* QRCode */ = { + isa = PBXGroup; + children = ( + 0CC2C05E233CDCBF009A2245 /* QRCodeReader.swift */, + ); + path = QRCode; + sourceTree = ""; + }; + 4C172F72263303B300E66397 /* CardReaderView */ = { + isa = PBXGroup; + children = ( + 4C172F6D263303B000E66397 /* CardReaderView.swift */, + ); + path = CardReaderView; + sourceTree = ""; + }; + 4C172F7B263306EC00E66397 /* Helpers */ = { + isa = PBXGroup; + children = ( + 4C172F76263306DD00E66397 /* CardFactory.swift */, + ); + path = Helpers; + sourceTree = ""; + }; 50BF34CF4C318572786E1701303A92B1 /* Pods-QRCodeReader_Example */ = { isa = PBXGroup; children = ( @@ -400,11 +479,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4C172F3E2632F52700E66397 /* CardReader.swift in Sources */, 0CC2C063233CDCBF009A2245 /* QRCodeReader.swift in Sources */, - 0CC2C066233CDCBF009A2245 /* QRCodeFocusView.swift in Sources */, - 0CC2C065233CDCBF009A2245 /* QRCodeReaderView.swift in Sources */, - 0CC2C064233CDCBF009A2245 /* QRCodeOverlayView.swift in Sources */, + 0CC2C066233CDCBF009A2245 /* FocusView.swift in Sources */, + 0CC2C065233CDCBF009A2245 /* BaseReaderView.swift in Sources */, + 4C172F432632FFF200E66397 /* BaseReader.swift in Sources */, + 4C172F4E2633032400E66397 /* QRCodeReaderView.swift in Sources */, + 0CC2C064233CDCBF009A2245 /* OverlayView.swift in Sources */, 130551717DB770EA0651FE8D93C23675 /* QRCodeReader-dummy.m in Sources */, + 4C172F77263306DD00E66397 /* CardFactory.swift in Sources */, + 4C172F6E263303B000E66397 /* CardReaderView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/QRCodeReader/Classes/Core/QRCodeReader.swift b/QRCodeReader/Classes/Core/Base/BaseReader.swift similarity index 60% rename from QRCodeReader/Classes/Core/QRCodeReader.swift rename to QRCodeReader/Classes/Core/Base/BaseReader.swift index 7063585..af58b43 100644 --- a/QRCodeReader/Classes/Core/QRCodeReader.swift +++ b/QRCodeReader/Classes/Core/Base/BaseReader.swift @@ -23,93 +23,21 @@ import UIKit import AVFoundation -open class QRCodeReader: NSObject, AVCaptureMetadataOutputObjectsDelegate { - - private let sessionQueue = DispatchQueue(label: "qr_capture_session_queue") - private let metadataObjectsQueue = DispatchQueue(label: "qr_metadata_objects_queue", attributes: [], target: nil) - - internal weak var readerView: QRCodeReaderView? - - lazy var defaultDeviceInput: AVCaptureDeviceInput? = { - guard let defaultDevice = defaultDevice else { - return nil - } - - return try? AVCaptureDeviceInput(device: defaultDevice) - }() +open class BaseReader: NSObject { // MARK: - Public Properties - let session = AVCaptureSession() - - let metadataOutput = AVCaptureMetadataOutput() - - let previewLayer: AVCaptureVideoPreviewLayer - public let defaultDevice: AVCaptureDevice? = .default(for: .video) - - public var stopScanningWhenCodeIsFound: Bool = true - - public var didFindCode: ((AVMetadataMachineReadableCodeObject) -> Void)? - + + public var stopScanningWhenCodeIsFound: Bool = false + + public var didFind: ((Result) -> Void)? public var didFailDecoding: (() -> Void)? - - // MARK: - Public Initializer - - public override init() { - previewLayer = AVCaptureVideoPreviewLayer(session: session) - - super.init() - - sessionQueue.async { - self.configureDefaultComponents() - } - } - // MARK: - Deinitializer - - deinit { - isTorchEnabled = false - } - - // MARK: - Checking the Reader Availabilities - - public class func isAvailable() -> Bool { - guard let captureDevice = AVCaptureDevice.default(for: .video) else { - return false - } - - return (try? AVCaptureDeviceInput(device: captureDevice)) != nil - } - - // MARK: - Controlling Reader - - public func startScanning() { - readerView?.updateRectOfInterestBasedOnFocusView() - - sessionQueue.async { - guard !self.session.isRunning else { - return - } - - self.session.startRunning() - } - } - - public func stopScanning() { - sessionQueue.async { - guard self.session.isRunning else { - return - } - - self.session.stopRunning() - } - } - public var isRunning: Bool { return session.isRunning } - + public var isTorchAvailable: Bool { return defaultDevice?.isTorchAvailable ?? false } @@ -124,75 +52,91 @@ open class QRCodeReader: NSObject, AVCaptureMetadataOutputObjectsDelegate { defer { defaultDevice?.unlockForConfiguration() } - + let newTorchMode: AVCaptureDevice.TorchMode = newValue ? .on : .off let isTorchModeSupported = defaultDevice?.isTorchModeSupported(newTorchMode) ?? false - + guard isTorchAvailable, isTorchModeSupported else { return } - + defaultDevice?.torchMode = newTorchMode } catch _ { } } - } - // MARK: - Private Methods + // MARK: - Checking the Reader Availabilities - private func configureDefaultComponents() { - - for output in session.outputs { - session.removeOutput(output) - } - for input in session.inputs { - session.removeInput(input) + public class func isAvailable() -> Bool { + guard let captureDevice = AVCaptureDevice.default(for: .video) else { + return false } - if let defaultDeviceInput = defaultDeviceInput { - session.addInput(defaultDeviceInput) - } - - session.addOutput(metadataOutput) - metadataOutput.setMetadataObjectsDelegate(self, queue: metadataObjectsQueue) - metadataOutput.metadataObjectTypes = [.qr, .aztec, .dataMatrix] - previewLayer.videoGravity = .resizeAspectFill - - session.commitConfiguration() + return (try? AVCaptureDeviceInput(device: captureDevice)) != nil } + + // MARK: - Internal Properties + + let session = AVCaptureSession() + let previewLayer: AVCaptureVideoPreviewLayer + let sessionQueue = DispatchQueue(label: "session_queue") + + lazy var defaultDeviceInput: AVCaptureDeviceInput? = { + guard let defaultDevice = defaultDevice else { + return nil + } + + return try? AVCaptureDeviceInput(device: defaultDevice) + }() + + var onUpdateRectOfInterest: (() -> Void)? + + // MARK: - Public Initializer - // MARK: - AVCaptureMetadataOutputObjectsDelegate + public init(onUpdateRectOfInterest: (() -> Void)?) { + self.onUpdateRectOfInterest = onUpdateRectOfInterest - public func metadataOutput(_ output: AVCaptureMetadataOutput, - didOutput metadataObjects: [AVMetadataObject], - from connection: AVCaptureConnection) { - - sessionQueue.async { [weak self] in - guard let self = self else { + previewLayer = AVCaptureVideoPreviewLayer(session: session) + + super.init() + + sessionQueue.async { + self.configureDefaultComponents() + } + } + + // MARK: - Deinitializer + + deinit { + isTorchEnabled = false + } + + // MARK: - Controlling Reader + public func startScanning() { + onUpdateRectOfInterest?() + + sessionQueue.async { + guard !self.session.isRunning else { return } - - for current in metadataObjects { - if let readableCodeObject = current as? AVMetadataMachineReadableCodeObject, - readableCodeObject.stringValue != nil { - - guard self.session.isRunning else { - return - } - - if self.stopScanningWhenCodeIsFound { - self.session.stopRunning() - } - - DispatchQueue.main.async { - self.didFindCode?(readableCodeObject) - } - } else { - DispatchQueue.main.async { - self.didFailDecoding?() - } - } - } + + self.session.startRunning() } } + + public func stopScanning() { + sessionQueue.async { + guard self.session.isRunning else { + return + } + + self.session.stopRunning() + } + } + + // MARK: - Private Methods + + func configureDefaultComponents() { + // override + } } diff --git a/QRCodeReader/Classes/Core/Card/CardReader.swift b/QRCodeReader/Classes/Core/Card/CardReader.swift new file mode 100644 index 0000000..84bdac7 --- /dev/null +++ b/QRCodeReader/Classes/Core/Card/CardReader.swift @@ -0,0 +1,128 @@ +// +// Copyright (c) 2019 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit +import AVFoundation +import Vision + +@available(iOS 13, *) +open class CardReader: BaseReader { + + private let scannerObjectsQueue = DispatchQueue(label: "scanner_objects_queue", attributes: [], target: nil) + private let factory: CardFactory + + private var request: VNRecognizeTextRequest { + let request = VNRecognizeTextRequest(completionHandler: recognizeTextHandler) + request.recognitionLevel = .accurate + request.usesLanguageCorrection = false + + return request + } + + private let videoDataOutput = AVCaptureVideoDataOutput() + + init(factory: CardFactory, onUpdateRectOfInterest: (() -> Void)?) { + self.factory = factory + + super.init(onUpdateRectOfInterest: onUpdateRectOfInterest) + } + + // MARK: - Private Methods + + override func configureDefaultComponents() { + + session.beginConfiguration() + + for output in session.outputs { + session.removeOutput(output) + } + for input in session.inputs { + session.removeInput(input) + } + + if let defaultDeviceInput = defaultDeviceInput { + session.addInput(defaultDeviceInput) + } + + videoDataOutput.alwaysDiscardsLateVideoFrames = true + videoDataOutput.setSampleBufferDelegate(self, queue: scannerObjectsQueue) + videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA] + + session.addOutput(videoDataOutput) + + previewLayer.videoGravity = .resizeAspectFill + + session.commitConfiguration() + } + + func recognizeTextHandler(request: VNRequest, error: Error?) { + + guard let results = request.results as? [VNRecognizedTextObservation] else { + DispatchQueue.main.async { + self.didFailDecoding?() + } + + return + } + + let maximumCandidates = 1 + + let lines = results.flatMap { $0.topCandidates(maximumCandidates).map { $0.string } } + + guard let card = factory.create(lines) else { + return + } + + guard self.session.isRunning else { + return + } + + if self.stopScanningWhenCodeIsFound { + self.session.stopRunning() + } + + DispatchQueue.main.async { + self.didFind?(card) + } + } +} + +@available(iOS 13, *) +extension CardReader: AVCaptureVideoDataOutputSampleBufferDelegate { + + public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { + guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { + return + } + + let ciImage = CIImage(cvImageBuffer: pixelBuffer) + let requestHandler = VNImageRequestHandler(ciImage: ciImage, orientation: .right, options: [:]) + + sessionQueue.async { [weak self] in + guard let request = self?.request else { + return + } + + try? requestHandler.perform([request]) + } + } +} diff --git a/QRCodeReader/Classes/Core/Card/Helpers/CardFactory.swift b/QRCodeReader/Classes/Core/Card/Helpers/CardFactory.swift new file mode 100644 index 0000000..fceb353 --- /dev/null +++ b/QRCodeReader/Classes/Core/Card/Helpers/CardFactory.swift @@ -0,0 +1,34 @@ +// +// Copyright (c) 2019 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public struct Card { + let number: String +} + +open class CardFactory { + + func create(_ values: [String]) -> Card? { + nil + } +} diff --git a/QRCodeReader/Classes/Core/QRCode/QRCodeReader.swift b/QRCodeReader/Classes/Core/QRCode/QRCodeReader.swift new file mode 100644 index 0000000..e907712 --- /dev/null +++ b/QRCodeReader/Classes/Core/QRCode/QRCodeReader.swift @@ -0,0 +1,89 @@ +// +// Copyright (c) 2019 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit +import AVFoundation + +open class QRCodeReader: BaseReader, AVCaptureMetadataOutputObjectsDelegate { + + private let metadataObjectsQueue = DispatchQueue(label: "qr_metadata_objects_queue", attributes: [], target: nil) + + let metadataOutput = AVCaptureMetadataOutput() + + // MARK: - Private Methods + + override func configureDefaultComponents() { + + for output in session.outputs { + session.removeOutput(output) + } + for input in session.inputs { + session.removeInput(input) + } + + if let defaultDeviceInput = defaultDeviceInput { + session.addInput(defaultDeviceInput) + } + + session.addOutput(metadataOutput) + metadataOutput.setMetadataObjectsDelegate(self, queue: metadataObjectsQueue) + metadataOutput.metadataObjectTypes = [.qr, .aztec, .dataMatrix] + previewLayer.videoGravity = .resizeAspectFill + + session.commitConfiguration() + } + + // MARK: - AVCaptureMetadataOutputObjectsDelegate + + public func metadataOutput(_ output: AVCaptureMetadataOutput, + didOutput metadataObjects: [AVMetadataObject], + from connection: AVCaptureConnection) { + + sessionQueue.async { [weak self] in + guard let self = self else { + return + } + + for current in metadataObjects { + if let readableCodeObject = current as? AVMetadataMachineReadableCodeObject, + readableCodeObject.stringValue != nil { + + guard self.session.isRunning else { + return + } + + if self.stopScanningWhenCodeIsFound { + self.session.stopRunning() + } + + DispatchQueue.main.async { + self.didFind?(readableCodeObject) + } + } else { + DispatchQueue.main.async { + self.didFailDecoding?() + } + } + } + } + } +} diff --git a/QRCodeReader/Classes/Views/Base/BaseReaderView.swift b/QRCodeReader/Classes/Views/Base/BaseReaderView.swift new file mode 100644 index 0000000..c525f20 --- /dev/null +++ b/QRCodeReader/Classes/Views/Base/BaseReaderView.swift @@ -0,0 +1,47 @@ +// +// Copyright (c) 2019 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit + +open class BaseReaderView: UIView { + + public let overlay = OverlayView() + + let cameraView = UIView() + + // MARK: - Intializers + + public init() { + super.init(frame: .zero) + + overlay.backgroundColor = UIColor.black.withAlphaComponent(0.5) + + addSubview(cameraView) + addSubview(overlay) + } + + @available(*, unavailable) + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + diff --git a/QRCodeReader/Classes/Views/CardReaderView/CardReaderView.swift b/QRCodeReader/Classes/Views/CardReaderView/CardReaderView.swift new file mode 100644 index 0000000..fe48a4a --- /dev/null +++ b/QRCodeReader/Classes/Views/CardReaderView/CardReaderView.swift @@ -0,0 +1,48 @@ +// +// Copyright (c) 2019 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import UIKit + +@available(iOS 13, *) +open class CardReaderView: BaseReaderView { + + private weak var reader: CardReader? + + // MARK: - Public Methods + + public func setReader(_ reader: CardReader) { + self.reader = reader + + cameraView.layer.sublayers?.forEach { + $0.removeFromSuperlayer() + } + + cameraView.layer.insertSublayer(reader.previewLayer, at: 0) + } + + override open func layoutSubviews() { + super.layoutSubviews() + + reader?.previewLayer.frame = bounds + overlay.frame = bounds + } +} diff --git a/QRCodeReader/Classes/Views/QRCodeReaderView.swift b/QRCodeReader/Classes/Views/QRCode/QRCodeReaderView.swift similarity index 80% rename from QRCodeReader/Classes/Views/QRCodeReaderView.swift rename to QRCodeReader/Classes/Views/QRCode/QRCodeReaderView.swift index c1dec8a..90223c2 100644 --- a/QRCodeReader/Classes/Views/QRCodeReaderView.swift +++ b/QRCodeReader/Classes/Views/QRCode/QRCodeReaderView.swift @@ -22,34 +22,15 @@ import UIKit -open class QRCodeReaderView: UIView { - - private let cameraView = UIView() +open class QRCodeReaderView: BaseReaderView { private weak var reader: QRCodeReader? - public let overlay = QRCodeOverlayView() - - // MARK: - Intializers - - public init() { - super.init(frame: .zero) - - overlay.backgroundColor = UIColor.black.withAlphaComponent(0.5) - - addSubview(cameraView) - addSubview(overlay) - } - - required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - // MARK: - Public Methods public func setReader(_ reader: QRCodeReader) { - reader.readerView = self self.reader = reader + self.reader?.onUpdateRectOfInterest = updateRectOfInterestBasedOnFocusView cameraView.layer.sublayers?.forEach { $0.removeFromSuperlayer() diff --git a/QRCodeReader/Classes/Views/QRCodeFocusView.swift b/QRCodeReader/Classes/Views/Subviews/FocusView.swift similarity index 96% rename from QRCodeReader/Classes/Views/QRCodeFocusView.swift rename to QRCodeReader/Classes/Views/Subviews/FocusView.swift index f82fab8..05654b7 100644 --- a/QRCodeReader/Classes/Views/QRCodeFocusView.swift +++ b/QRCodeReader/Classes/Views/Subviews/FocusView.swift @@ -22,7 +22,7 @@ import UIKit -open class QRCodeFocusView: UIView { +open class FocusView: UIView { public var cornerColor: UIColor { didSet { @@ -74,7 +74,7 @@ open class QRCodeFocusView: UIView { var currentSuperview = superview while let view = currentSuperview { - if view is QRCodeOverlayView { + if view is OverlayView { view.setNeedsDisplay() return } else { diff --git a/QRCodeReader/Classes/Views/QRCodeOverlayView.swift b/QRCodeReader/Classes/Views/Subviews/OverlayView.swift similarity index 96% rename from QRCodeReader/Classes/Views/QRCodeOverlayView.swift rename to QRCodeReader/Classes/Views/Subviews/OverlayView.swift index 0a45b44..688315b 100644 --- a/QRCodeReader/Classes/Views/QRCodeOverlayView.swift +++ b/QRCodeReader/Classes/Views/Subviews/OverlayView.swift @@ -22,9 +22,9 @@ import UIKit -open class QRCodeOverlayView: UIView { +open class OverlayView: UIView { - public weak var focusView: QRCodeFocusView? + public weak var focusView: FocusView? override open func draw(_ rect: CGRect) {