Allowing the scan area configuration #157 #106

This commit is contained in:
Yannick Loriot 2018-12-28 14:17:33 +01:00
parent dff48390b3
commit a8b4576243
8 changed files with 104 additions and 85 deletions

View File

@ -1,5 +1,9 @@
# Change log # Change log
## Version 10.0.0
- [ADD] Allowing the scan area configuration (#157 #106)
## [Version 9.0.0](https://github.com/yannickl/QRCodeReader.swift/releases/tag/9.0.0) ## [Version 9.0.0](https://github.com/yannickl/QRCodeReader.swift/releases/tag/9.0.0)
Release on 2018-09-19 Release on 2018-09-19

View File

@ -30,7 +30,11 @@ import UIKit
class ViewController: UIViewController, QRCodeReaderViewControllerDelegate { class ViewController: UIViewController, QRCodeReaderViewControllerDelegate {
@IBOutlet weak var previewView: QRCodeReaderView! { @IBOutlet weak var previewView: QRCodeReaderView! {
didSet { didSet {
previewView.setupComponents(showCancelButton: false, showSwitchCameraButton: false, showTorchButton: false, showOverlayView: true, reader: reader) previewView.setupComponents(with: QRCodeReaderViewControllerBuilder {
$0.reader = reader
$0.showTorchButton = false
$0.showSwitchCameraButton = false
})
} }
} }
lazy var reader: QRCodeReader = QRCodeReader() lazy var reader: QRCodeReader = QRCodeReader()
@ -39,6 +43,7 @@ class ViewController: UIViewController, QRCodeReaderViewControllerDelegate {
$0.reader = QRCodeReader(metadataObjectTypes: [.qr], captureDevicePosition: .back) $0.reader = QRCodeReader(metadataObjectTypes: [.qr], captureDevicePosition: .back)
$0.showTorchButton = true $0.showTorchButton = true
$0.preferredStatusBarStyle = .lightContent $0.preferredStatusBarStyle = .lightContent
$0.rectOfInterest = CGRect(x: 0.15, y: 0.15, width: 0.7, height: 0.7)
$0.reader.stopScanningWhenCodeIsFound = false $0.reader.stopScanningWhenCodeIsFound = false
} }

View File

@ -37,8 +37,8 @@ public final class QRCodeReader: NSObject, AVCaptureMetadataOutputObjectsDelegat
private let sessionQueue = DispatchQueue(label: "session queue") private let sessionQueue = DispatchQueue(label: "session queue")
private let metadataObjectsQueue = DispatchQueue(label: "com.yannickloriot.qr", attributes: [], target: nil) private let metadataObjectsQueue = DispatchQueue(label: "com.yannickloriot.qr", attributes: [], target: nil)
var defaultDevice: AVCaptureDevice? = AVCaptureDevice.default(for: .video) let defaultDevice: AVCaptureDevice? = AVCaptureDevice.default(for: .video)
var frontDevice: AVCaptureDevice? = { let frontDevice: AVCaptureDevice? = {
if #available(iOS 10, *) { if #available(iOS 10, *) {
return AVCaptureDevice.default(.builtInWideAngleCamera, for: AVMediaType.video, position: .front) return AVCaptureDevice.default(.builtInWideAngleCamera, for: AVMediaType.video, position: .front)
} }
@ -67,8 +67,8 @@ public final class QRCodeReader: NSObject, AVCaptureMetadataOutputObjectsDelegat
return nil return nil
}() }()
public var metadataOutput = AVCaptureMetadataOutput() let session = AVCaptureSession()
var session = AVCaptureSession() let metadataOutput = AVCaptureMetadataOutput()
weak var lifeCycleDelegate: QRCodeReaderLifeCycleDelegate? weak var lifeCycleDelegate: QRCodeReaderLifeCycleDelegate?

View File

@ -73,16 +73,16 @@ final public class QRCodeReaderView: UIView, QRCodeReaderDisplayable {
private weak var reader: QRCodeReader? private weak var reader: QRCodeReader?
public func setupComponents(showCancelButton: Bool, showSwitchCameraButton: Bool, showTorchButton: Bool, showOverlayView: Bool, reader: QRCodeReader?) { public func setupComponents(with builder: QRCodeReaderViewControllerBuilder) {
self.reader = reader self.reader = builder.reader
reader?.lifeCycleDelegate = self reader?.lifeCycleDelegate = self
addComponents() addComponents()
cancelButton?.isHidden = !showCancelButton cancelButton?.isHidden = !builder.showCancelButton
switchCameraButton?.isHidden = !showSwitchCameraButton switchCameraButton?.isHidden = !builder.showSwitchCameraButton
toggleTorchButton?.isHidden = !showTorchButton toggleTorchButton?.isHidden = !builder.showTorchButton
overlayView?.isHidden = !showOverlayView overlayView?.isHidden = !builder.showOverlayView
guard let cb = cancelButton, let scb = switchCameraButton, let ttb = toggleTorchButton, let ov = overlayView else { return } guard let cb = cancelButton, let scb = switchCameraButton, let ttb = toggleTorchButton, let ov = overlayView else { return }
@ -90,7 +90,7 @@ final public class QRCodeReaderView: UIView, QRCodeReaderDisplayable {
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[cv]|", options: [], metrics: nil, views: views)) addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[cv]|", options: [], metrics: nil, views: views))
if showCancelButton { if builder.showCancelButton {
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[cv][cb(40)]|", options: [], metrics: nil, views: views)) addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[cv][cb(40)]|", options: [], metrics: nil, views: views))
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[cb]-|", options: [], metrics: nil, views: views)) addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[cb]-|", options: [], metrics: nil, views: views))
} }
@ -98,12 +98,12 @@ final public class QRCodeReaderView: UIView, QRCodeReaderDisplayable {
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[cv]|", options: [], metrics: nil, views: views)) addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[cv]|", options: [], metrics: nil, views: views))
} }
if showSwitchCameraButton { if builder.showSwitchCameraButton {
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-[scb(50)]", options: [], metrics: nil, views: views)) addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-[scb(50)]", options: [], metrics: nil, views: views))
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:[scb(70)]|", options: [], metrics: nil, views: views)) addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:[scb(70)]|", options: [], metrics: nil, views: views))
} }
if showTorchButton { if builder.showTorchButton {
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-[ttb(50)]", options: [], metrics: nil, views: views)) addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-[ttb(50)]", options: [], metrics: nil, views: views))
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[ttb(70)]", options: [], metrics: nil, views: views)) addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[ttb(70)]", options: [], metrics: nil, views: views))
} }
@ -111,6 +111,10 @@ final public class QRCodeReaderView: UIView, QRCodeReaderDisplayable {
for attribute in Array<NSLayoutConstraint.Attribute>([.left, .top, .right, .bottom]) { for attribute in Array<NSLayoutConstraint.Attribute>([.left, .top, .right, .bottom]) {
addConstraint(NSLayoutConstraint(item: ov, attribute: attribute, relatedBy: .equal, toItem: cameraView, attribute: attribute, multiplier: 1, constant: 0)) addConstraint(NSLayoutConstraint(item: ov, attribute: attribute, relatedBy: .equal, toItem: cameraView, attribute: attribute, multiplier: 1, constant: 0))
} }
if let readerOverlayView = overlayView as? ReaderOverlayView {
readerOverlayView.rectOfInterest = builder.rectOfInterest
}
} }
public override func layoutSubviews() { public override func layoutSubviews() {

View File

@ -47,15 +47,11 @@ public protocol QRCodeReaderDisplayable {
func setNeedsUpdateOrientation() func setNeedsUpdateOrientation()
/** /**
Method called by the container to allows you to layout your view properly using the given flags. Method called by the container to allows you to layout your view properly using the QR code reader builder.
- Parameter showCancelButton: Flag to know whether you should display the cancel button. - Parameter builder: A QR code reader builder.
- Parameter showSwitchCameraButton: Flag to know whether you should display the switch camera button.
- Parameter showTorchButton: Flag to know whether you should display the toggle torch button.
- Parameter showOverlayView: Flag to know whether you should display the overlay.
- Parameter reader: A reference to the code reader.
*/ */
func setupComponents(showCancelButton: Bool, showSwitchCameraButton: Bool, showTorchButton: Bool, showOverlayView: Bool, reader: QRCodeReader?) func setupComponents(with builder: QRCodeReaderViewControllerBuilder)
} }
/// The `QRCodeReaderContainer` structure embed the view displayed by the controller. The embeded view must be conform to the `QRCodeReaderDisplayable` protocol. /// The `QRCodeReaderContainer` structure embed the view displayed by the controller. The embeded view must be conform to the `QRCodeReaderDisplayable` protocol.
@ -75,7 +71,7 @@ public struct QRCodeReaderContainer {
// MARK: - Convenience Methods // MARK: - Convenience Methods
func setupComponents(showCancelButton: Bool, showSwitchCameraButton: Bool, showTorchButton: Bool, showOverlayView: Bool, reader: QRCodeReader? = nil) { func setupComponents(with builder: QRCodeReaderViewControllerBuilder) {
displayable.setupComponents(showCancelButton: showCancelButton, showSwitchCameraButton: showSwitchCameraButton, showTorchButton: showTorchButton, showOverlayView: showOverlayView, reader: reader) displayable.setupComponents(with: builder)
} }
} }

View File

@ -29,16 +29,12 @@ import AVFoundation
/// Convenient controller to display a view to scan/read 1D or 2D bar codes like the QRCodes. It is based on the `AVFoundation` framework from Apple. It aims to replace ZXing or ZBar for iOS 7 and over. /// Convenient controller to display a view to scan/read 1D or 2D bar codes like the QRCodes. It is based on the `AVFoundation` framework from Apple. It aims to replace ZXing or ZBar for iOS 7 and over.
public class QRCodeReaderViewController: UIViewController { public class QRCodeReaderViewController: UIViewController {
/// The code reader object used to scan the bar code. private let builder: QRCodeReaderViewControllerBuilder
public let codeReader: QRCodeReader
let readerView: QRCodeReaderContainer /// The code reader object used to scan the bar code.
let startScanningAtLoad: Bool public var codeReader: QRCodeReader {
let showCancelButton: Bool return builder.reader
let showSwitchCameraButton: Bool }
let showTorchButton: Bool
let showOverlayView: Bool
let customPreferredStatusBarStyle: UIStatusBarStyle?
// MARK: - Managing the Callback Responders // MARK: - Managing the Callback Responders
@ -62,14 +58,7 @@ public class QRCodeReaderViewController: UIViewController {
- parameter builder: A QRCodeViewController builder object. - parameter builder: A QRCodeViewController builder object.
*/ */
required public init(builder: QRCodeReaderViewControllerBuilder) { required public init(builder: QRCodeReaderViewControllerBuilder) {
readerView = builder.readerView self.builder = builder
startScanningAtLoad = builder.startScanningAtLoad
codeReader = builder.reader
showCancelButton = builder.showCancelButton
showSwitchCameraButton = builder.showSwitchCameraButton
showTorchButton = builder.showTorchButton
showOverlayView = builder.showOverlayView
customPreferredStatusBarStyle = builder.preferredStatusBarStyle
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
@ -77,7 +66,7 @@ public class QRCodeReaderViewController: UIViewController {
codeReader.didFindCode = { [weak self] resultAsObject in codeReader.didFindCode = { [weak self] resultAsObject in
if let weakSelf = self { if let weakSelf = self {
if let qrv = weakSelf.readerView.displayable as? QRCodeReaderView { if let qrv = builder.readerView.displayable as? QRCodeReaderView {
qrv.addGreenBorder() qrv.addGreenBorder()
} }
weakSelf.completionBlock?(resultAsObject) weakSelf.completionBlock?(resultAsObject)
@ -85,11 +74,9 @@ public class QRCodeReaderViewController: UIViewController {
} }
} }
codeReader.didFailDecoding = { [weak self] in codeReader.didFailDecoding = {
if let weakSelf = self { if let qrv = builder.readerView.displayable as? QRCodeReaderView {
if let qrv = weakSelf.readerView.displayable as? QRCodeReaderView { qrv.addRedBorder()
qrv.addRedBorder()
}
} }
} }
@ -97,14 +84,7 @@ public class QRCodeReaderViewController: UIViewController {
} }
required public init?(coder aDecoder: NSCoder) { required public init?(coder aDecoder: NSCoder) {
codeReader = QRCodeReader() self.builder = QRCodeReaderViewControllerBuilder()
readerView = QRCodeReaderContainer(displayable: QRCodeReaderView())
startScanningAtLoad = false
showCancelButton = false
showTorchButton = false
showSwitchCameraButton = false
showOverlayView = false
customPreferredStatusBarStyle = nil
super.init(coder: aDecoder) super.init(coder: aDecoder)
} }
@ -114,8 +94,8 @@ public class QRCodeReaderViewController: UIViewController {
override public func viewWillAppear(_ animated: Bool) { override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
if startScanningAtLoad { if builder.startScanningAtLoad {
readerView.displayable.setNeedsUpdateOrientation() builder.readerView.displayable.setNeedsUpdateOrientation()
startScanning() startScanning()
} }
@ -134,38 +114,35 @@ public class QRCodeReaderViewController: UIViewController {
} }
public override var preferredStatusBarStyle: UIStatusBarStyle { public override var preferredStatusBarStyle: UIStatusBarStyle {
return customPreferredStatusBarStyle ?? super.preferredStatusBarStyle return builder.preferredStatusBarStyle ?? super.preferredStatusBarStyle
} }
// MARK: - Initializing the AV Components // MARK: - Initializing the AV Components
private func setupUIComponentsWithCancelButtonTitle(_ cancelButtonTitle: String) { private func setupUIComponentsWithCancelButtonTitle(_ cancelButtonTitle: String) {
view.addSubview(readerView.view) view.addSubview(builder.readerView.view)
let sscb = showSwitchCameraButton && codeReader.hasFrontDevice builder.readerView.view.translatesAutoresizingMaskIntoConstraints = false
let stb = showTorchButton && codeReader.isTorchAvailable builder.readerView.setupComponents(with: builder)
readerView.view.translatesAutoresizingMaskIntoConstraints = false
readerView.setupComponents(showCancelButton: showCancelButton, showSwitchCameraButton: sscb, showTorchButton: stb, showOverlayView: showOverlayView, reader: codeReader)
// Setup action methods // Setup action methods
readerView.displayable.switchCameraButton?.addTarget(self, action: #selector(switchCameraAction), for: .touchUpInside) builder.readerView.displayable.switchCameraButton?.addTarget(self, action: #selector(switchCameraAction), for: .touchUpInside)
readerView.displayable.toggleTorchButton?.addTarget(self, action: #selector(toggleTorchAction), for: .touchUpInside) builder.readerView.displayable.toggleTorchButton?.addTarget(self, action: #selector(toggleTorchAction), for: .touchUpInside)
readerView.displayable.cancelButton?.setTitle(cancelButtonTitle, for: .normal) builder.readerView.displayable.cancelButton?.setTitle(cancelButtonTitle, for: .normal)
readerView.displayable.cancelButton?.addTarget(self, action: #selector(cancelAction), for: .touchUpInside) builder.readerView.displayable.cancelButton?.addTarget(self, action: #selector(cancelAction), for: .touchUpInside)
// Setup constraints // Setup constraints
for attribute in [.left, .top, .right] as [NSLayoutConstraint.Attribute] { for attribute in [.left, .top, .right] as [NSLayoutConstraint.Attribute] {
NSLayoutConstraint(item: readerView.view, attribute: attribute, relatedBy: .equal, toItem: view, attribute: attribute, multiplier: 1, constant: 0).isActive = true NSLayoutConstraint(item: builder.readerView.view, attribute: attribute, relatedBy: .equal, toItem: view, attribute: attribute, multiplier: 1, constant: 0).isActive = true
} }
if #available(iOS 11.0, *) { if #available(iOS 11.0, *) {
view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: readerView.view.bottomAnchor).isActive = true view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: builder.readerView.view.bottomAnchor).isActive = true
} }
else { else {
NSLayoutConstraint(item: readerView.view, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0).isActive = true NSLayoutConstraint(item: builder.readerView.view, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0).isActive = true
} }
} }

View File

@ -68,12 +68,28 @@ public final class QRCodeReaderViewControllerBuilder {
/** /**
Flag to display the switch camera button. Flag to display the switch camera button.
*/ */
public var showSwitchCameraButton = true public var showSwitchCameraButton: Bool {
get {
return _showSwitchCameraButton && reader.hasFrontDevice
}
set {
_showSwitchCameraButton = newValue
}
}
private var _showSwitchCameraButton: Bool = true
/** /**
Flag to display the toggle torch button. If the value is true and there is no torch the button will not be displayed. Flag to display the toggle torch button. If the value is true and there is no torch the button will not be displayed.
*/ */
public var showTorchButton = false public var showTorchButton: Bool {
get {
return _showTorchButton && reader.isTorchAvailable
}
set {
_showTorchButton = newValue
}
}
private var _showTorchButton = true
/** /**
Flag to display the guide view. Flag to display the guide view.
@ -91,6 +107,22 @@ public final class QRCodeReaderViewControllerBuilder {
*/ */
public var preferredStatusBarStyle: UIStatusBarStyle? = nil public var preferredStatusBarStyle: UIStatusBarStyle? = nil
/**
Specifies a rectangle of interest for limiting the search area for visual metadata.
The value of this property is a CGRect that determines the receiver's rectangle of interest for each frame of video. The rectangle's origin is top left and is relative to the coordinate space of the device providing the metadata. Specifying a rectOfInterest may improve detection performance for certain types of metadata. The default value of this property is the value CGRectMake(0, 0, 1, 1). Metadata objects whose bounds do not intersect with the rectOfInterest will not be returned.
*/
public var rectOfInterest: CGRect = CGRect(x: 0, y: 0, width: 1, height: 1) {
didSet {
reader.metadataOutput.rectOfInterest = CGRect(
x: min(max(rectOfInterest.origin.x, 0), 1),
y: min(max(rectOfInterest.origin.y, 0), 1),
width: min(max(rectOfInterest.width, 0), 1),
height: min(max(rectOfInterest.height, 0), 1)
)
}
}
// MARK: - Initializing a Flap View // MARK: - Initializing a Flap View
/** /**

View File

@ -58,24 +58,25 @@ public final class ReaderOverlayView: UIView {
var overlayColor: UIColor = UIColor.white { var overlayColor: UIColor = UIColor.white {
didSet { didSet {
self.overlay.strokeColor = overlayColor.cgColor overlay.strokeColor = overlayColor.cgColor
self.setNeedsDisplay() setNeedsDisplay()
}
}
var rectOfInterest: CGRect = CGRect(x: 0, y: 0, width: 1, height: 1) {
didSet {
setNeedsDisplay()
} }
} }
public override func draw(_ rect: CGRect) { public override func draw(_ rect: CGRect) {
var innerRect = rect.insetBy(dx: 50, dy: 50) let innerRect = CGRect(
let minSize = min(innerRect.width, innerRect.height) x: rect.width * rectOfInterest.minX,
y: rect.height * rectOfInterest.minY,
if innerRect.width != minSize { width: rect.width * rectOfInterest.width,
innerRect.origin.x += (innerRect.width - minSize) / 2 height: rect.height * rectOfInterest.height
innerRect.size.width = minSize )
}
else if innerRect.height != minSize {
innerRect.origin.y += (innerRect.height - minSize) / 2
innerRect.size.height = minSize
}
overlay.path = UIBezierPath(roundedRect: innerRect, cornerRadius: 5).cgPath overlay.path = UIBezierPath(roundedRect: innerRect, cornerRadius: 5).cgPath
} }