From c927ba53c82109501bc575629b96a245ad5dc4f1 Mon Sep 17 00:00:00 2001 From: MarcSteven <137871653@qq.com> Date: Sat, 11 Nov 2023 20:56:24 +0800 Subject: [PATCH] add Scan QR code component --- MemoryChainKit.xcodeproj/project.pbxproj | 44 ++ .../ScanQRCode/ARIPermission.swift | 73 +++ .../ScanQRCode/ARIScanLineAnimation.swift | 64 +++ .../ScanQRCode/ARIScanNetAnimation.swift | 71 +++ .../ScanQRCode/ARIScanResult.swift | 32 ++ .../ScanQRCode/ARIScanView+GetScanRect.swift | 34 ++ .../UI Component/ScanQRCode/ARIScanView.swift | 376 ++++++++++++ .../ScanQRCode/ARIScanViewController.swift | 193 +++++++ .../ScanQRCode/ARIScanViewStyle.swift | 73 +++ .../ScanQRCode/ARIScanWrapper.swift | 537 ++++++++++++++++++ 10 files changed, 1497 insertions(+) create mode 100644 MemoryChainKit/Sources/UI Component/ScanQRCode/ARIPermission.swift create mode 100644 MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanLineAnimation.swift create mode 100644 MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanNetAnimation.swift create mode 100644 MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanResult.swift create mode 100644 MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanView+GetScanRect.swift create mode 100644 MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanView.swift create mode 100644 MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanViewController.swift create mode 100644 MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanViewStyle.swift create mode 100644 MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanWrapper.swift diff --git a/MemoryChainKit.xcodeproj/project.pbxproj b/MemoryChainKit.xcodeproj/project.pbxproj index 56b72ca..a485278 100644 --- a/MemoryChainKit.xcodeproj/project.pbxproj +++ b/MemoryChainKit.xcodeproj/project.pbxproj @@ -32,6 +32,15 @@ 4279590F2AFFAD32000FE2F4 /* CBCharacteristicExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4279590A2AFFAD32000FE2F4 /* CBCharacteristicExtension.swift */; }; 427959102AFFAD32000FE2F4 /* CBPeripheralExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4279590B2AFFAD32000FE2F4 /* CBPeripheralExtension.swift */; }; 427959112AFFAD32000FE2F4 /* CBServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4279590C2AFFAD32000FE2F4 /* CBServiceExtension.swift */; }; + 4279591C2AFFB158000FE2F4 /* ARIScanResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427959132AFFB158000FE2F4 /* ARIScanResult.swift */; }; + 4279591D2AFFB158000FE2F4 /* ARIScanView+GetScanRect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427959142AFFB158000FE2F4 /* ARIScanView+GetScanRect.swift */; }; + 4279591E2AFFB158000FE2F4 /* ARIScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427959152AFFB158000FE2F4 /* ARIScanView.swift */; }; + 4279591F2AFFB158000FE2F4 /* ARIScanLineAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427959162AFFB158000FE2F4 /* ARIScanLineAnimation.swift */; }; + 427959202AFFB158000FE2F4 /* ARIScanViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427959172AFFB158000FE2F4 /* ARIScanViewStyle.swift */; }; + 427959212AFFB158000FE2F4 /* ARIScanWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427959182AFFB158000FE2F4 /* ARIScanWrapper.swift */; }; + 427959222AFFB158000FE2F4 /* ARIPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427959192AFFB158000FE2F4 /* ARIPermission.swift */; }; + 427959232AFFB158000FE2F4 /* ARIScanNetAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4279591A2AFFB158000FE2F4 /* ARIScanNetAnimation.swift */; }; + 427959242AFFB158000FE2F4 /* ARIScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4279591B2AFFB158000FE2F4 /* ARIScanViewController.swift */; }; 42987362277361C70063A70D /* EncodableExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42987361277361C70063A70D /* EncodableExtension.swift */; }; 42987364277364790063A70D /* UIPressExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42987363277364790063A70D /* UIPressExtension.swift */; }; 42999FE52754D0CE0045E00D /* LocaleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42999FE42754D0CE0045E00D /* LocaleExtension.swift */; }; @@ -383,6 +392,15 @@ 4279590A2AFFAD32000FE2F4 /* CBCharacteristicExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CBCharacteristicExtension.swift; sourceTree = ""; }; 4279590B2AFFAD32000FE2F4 /* CBPeripheralExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CBPeripheralExtension.swift; sourceTree = ""; }; 4279590C2AFFAD32000FE2F4 /* CBServiceExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CBServiceExtension.swift; sourceTree = ""; }; + 427959132AFFB158000FE2F4 /* ARIScanResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARIScanResult.swift; sourceTree = ""; }; + 427959142AFFB158000FE2F4 /* ARIScanView+GetScanRect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ARIScanView+GetScanRect.swift"; sourceTree = ""; }; + 427959152AFFB158000FE2F4 /* ARIScanView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARIScanView.swift; sourceTree = ""; }; + 427959162AFFB158000FE2F4 /* ARIScanLineAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARIScanLineAnimation.swift; sourceTree = ""; }; + 427959172AFFB158000FE2F4 /* ARIScanViewStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARIScanViewStyle.swift; sourceTree = ""; }; + 427959182AFFB158000FE2F4 /* ARIScanWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARIScanWrapper.swift; sourceTree = ""; }; + 427959192AFFB158000FE2F4 /* ARIPermission.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARIPermission.swift; sourceTree = ""; }; + 4279591A2AFFB158000FE2F4 /* ARIScanNetAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARIScanNetAnimation.swift; sourceTree = ""; }; + 4279591B2AFFB158000FE2F4 /* ARIScanViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARIScanViewController.swift; sourceTree = ""; }; 42987361277361C70063A70D /* EncodableExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodableExtension.swift; sourceTree = ""; }; 42987363277364790063A70D /* UIPressExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIPressExtension.swift; sourceTree = ""; }; 42999FE42754D0CE0045E00D /* LocaleExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocaleExtension.swift; sourceTree = ""; }; @@ -792,6 +810,22 @@ path = CoreBluetooth; sourceTree = ""; }; + 427959122AFFB144000FE2F4 /* ScanQRCode */ = { + isa = PBXGroup; + children = ( + 427959192AFFB158000FE2F4 /* ARIPermission.swift */, + 427959162AFFB158000FE2F4 /* ARIScanLineAnimation.swift */, + 4279591A2AFFB158000FE2F4 /* ARIScanNetAnimation.swift */, + 427959132AFFB158000FE2F4 /* ARIScanResult.swift */, + 427959152AFFB158000FE2F4 /* ARIScanView.swift */, + 427959142AFFB158000FE2F4 /* ARIScanView+GetScanRect.swift */, + 4279591B2AFFB158000FE2F4 /* ARIScanViewController.swift */, + 427959172AFFB158000FE2F4 /* ARIScanViewStyle.swift */, + 427959182AFFB158000FE2F4 /* ARIScanWrapper.swift */, + ); + path = ScanQRCode; + sourceTree = ""; + }; 42999FEE2755176A0045E00D /* Utils */ = { isa = PBXGroup; children = ( @@ -1186,6 +1220,7 @@ A6D40B782417B6B400A88B5B /* UI Component */ = { isa = PBXGroup; children = ( + 427959122AFFB144000FE2F4 /* ScanQRCode */, 427958F12AFF44EF000FE2F4 /* PagingView */, A6DC0362254984D5001F3E64 /* TabBar */, A6D4885D2548FD65005CA39E /* MarkDown */, @@ -1786,6 +1821,7 @@ 42999FE52754D0CE0045E00D /* LocaleExtension.swift in Sources */, A6DDD53924168D0A008E1703 /* LocalStorageImpl.swift in Sources */, A65417AF2421E5FE00077AD8 /* UIBarButtonItemExtension.swift in Sources */, + 427959202AFFB158000FE2F4 /* ARIScanViewStyle.swift in Sources */, A63D396A2419298000937CCD /* UIViewController+Extension.swift in Sources */, A63D39652419298000937CCD /* UIAlertControllerExtension.swift in Sources */, A6D40BF42417B6B500A88B5B /* LinearProgressBar.swift in Sources */, @@ -1805,6 +1841,7 @@ A6E85DAD24496C0500645A2D /* Navigator.swift in Sources */, A6651FBB24378E3D00DCBF85 /* PresentationContext.swift in Sources */, A6652022243A186700DCBF85 /* ValidRules.swift in Sources */, + 427959242AFFB158000FE2F4 /* ARIScanViewController.swift in Sources */, A6D40BDA2417B6B500A88B5B /* RefreshView.swift in Sources */, 427959112AFFAD32000FE2F4 /* CBServiceExtension.swift in Sources */, A6D40BFC2417B6B500A88B5B /* MCInformationTipView.swift in Sources */, @@ -1857,6 +1894,7 @@ A695CC5E2420A51C007C68DD /* UIWindowExtension.swift in Sources */, A6D40C142417B6B500A88B5B /* TabBarDelegate.swift in Sources */, A6E85DB22449753A00645A2D /* TimeZone+Identifier.swift in Sources */, + 427959232AFFB158000FE2F4 /* ARIScanNetAnimation.swift in Sources */, A6C3E35E2444339900132000 /* ZoomIn.swift in Sources */, A6911B6226D8B12100D85AD9 /* WaveForm.swift in Sources */, A69310D42417100E004ADF4F /* OperationQueue+Extension.swift in Sources */, @@ -1874,6 +1912,7 @@ A6020D502450481000B6DA00 /* ParameterEncoding.swift in Sources */, A6D4886F254902B2005CA39E /* DimmableLayout.swift in Sources */, A6D40C0E2417B6B500A88B5B /* CountriesViewControllerDelegate.swift in Sources */, + 4279591C2AFFB158000FE2F4 /* ARIScanResult.swift in Sources */, A6D40BF12417B6B500A88B5B /* CustomNavBar.swift in Sources */, A6D40C102417B6B500A88B5B /* PageControl.swift in Sources */, A69BC0D3244598FC00E600FB /* UINavigationBar+Style.swift in Sources */, @@ -1903,6 +1942,7 @@ A6020D3D244FE90200B6DA00 /* PropertyStoring.swift in Sources */, 4279590D2AFFAD32000FE2F4 /* CBManagerStateExtension.swift in Sources */, A69B38B92532F0D6007D279D /* CGSizeExtension.swift in Sources */, + 4279591D2AFFB158000FE2F4 /* ARIScanView+GetScanRect.swift in Sources */, 427958F72AFF45B2000FE2F4 /* IndicatorScrollableStyle.swift in Sources */, A69310BE2417100E004ADF4F /* Timer+Extension.swift in Sources */, A6D40BE02417B6B500A88B5B /* MCDropdownMenu+Animations.swift in Sources */, @@ -1927,6 +1967,7 @@ 42999FE72754D2120045E00D /* NSDateExtension.swift in Sources */, A6DDD52924168CC1008E1703 /* String+HMAC.swift in Sources */, A69B38B32532DF0E007D279D /* ExtensibleAsslckatedObject.swift in Sources */, + 4279591F2AFFB158000FE2F4 /* ARIScanLineAnimation.swift in Sources */, A69B38BD2532F44B007D279D /* UIRectCornerExtension.swift in Sources */, A69B38BF2532F6E3007D279D /* UIGestureRecognizer+Closure.swift in Sources */, 427959042AFF4936000FE2F4 /* MCPagingViewDelegate.swift in Sources */, @@ -2083,12 +2124,14 @@ A6356D142423B43700B46CDF /* ExecutableQueue.swift in Sources */, A695D0D8254A7AC500DE403A /* WKWebViewExtension.swift in Sources */, 427959002AFF4829000FE2F4 /* MCPagingScrollView.swift in Sources */, + 427959222AFFB158000FE2F4 /* ARIPermission.swift in Sources */, A66F3F3B2429EDA80081D52F /* JSONUtils.swift in Sources */, A6D40C0C2417B6B500A88B5B /* CountiresViewController.swift in Sources */, A6D40BE12417B6B500A88B5B /* NSCalendar+Extension.swift in Sources */, A6DDD45324168C6E008E1703 /* StringRepresentable.swift in Sources */, 4260A1FE2AFBAB8A0036C357 /* ImageExtension.swift in Sources */, A6D488652548FDAA005CA39E /* MarkupText.swift in Sources */, + 427959212AFFB158000FE2F4 /* ARIScanWrapper.swift in Sources */, A6DDD52324168C90008E1703 /* KeyChainSwift.swift in Sources */, A63D3909241923ED00937CCD /* PopTransition.swift in Sources */, A6DDD45024168C6E008E1703 /* BackgroundColor.swift in Sources */, @@ -2102,6 +2145,7 @@ A6D40B772417AB5C00A88B5B /* TimeIntervalExtension.swift in Sources */, A63D39602419298000937CCD /* UILayoutPriority+Extension.swift in Sources */, A6DDD46824168C6E008E1703 /* KeyboardInfo.swift in Sources */, + 4279591E2AFFB158000FE2F4 /* ARIScanView.swift in Sources */, 427958F02AFF308C000FE2F4 /* ScannerExtension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIPermission.swift b/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIPermission.swift new file mode 100644 index 0000000..dedbb17 --- /dev/null +++ b/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIPermission.swift @@ -0,0 +1,73 @@ +// +// ARIPermission.swift +// ARIScanCodeKit +// +// Created by marc zhao on 2022/4/7. +// + + + +import UIKit +import AVFoundation +import Photos +import AssetsLibrary + + + +class ARIPermissions: NSObject { + + //MARK: ----获取相册权限 + static func authorizePhotoWith(comletion: @escaping (Bool) -> Void) { + let granted = PHPhotoLibrary.authorizationStatus() + switch granted { + case PHAuthorizationStatus.authorized: + comletion(true) + case PHAuthorizationStatus.denied, PHAuthorizationStatus.restricted: + comletion(false) + case PHAuthorizationStatus.notDetermined: + PHPhotoLibrary.requestAuthorization({ status in + DispatchQueue.main.async { + comletion(status == PHAuthorizationStatus.authorized) + } + }) + case .limited: + comletion(true) + @unknown default: + comletion(false) + } + } + + //MARK: ---相机权限 + static func authorizeCameraWith(completion: @escaping (Bool) -> Void) { + let granted = AVCaptureDevice.authorizationStatus(for: AVMediaType.video) + switch granted { + case .authorized: + completion(true) + case .denied: + completion(false) + case .restricted: + completion(false) + case .notDetermined: + AVCaptureDevice.requestAccess(for: AVMediaType.video, completionHandler: { (granted: Bool) in + DispatchQueue.main.async { + completion(granted) + } + }) + @unknown default: + completion(false) + } + } + + //MARK: 跳转到APP系统设置权限界面 + static func jumpToSystemPrivacySetting() { + guard let appSetting = URL(string: UIApplication.openSettingsURLString) else { + return + } + if #available(iOS 10, *) { + UIApplication.shared.open(appSetting, options: [:], completionHandler: nil) + } else { + UIApplication.shared.openURL(appSetting) + } + } + +} diff --git a/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanLineAnimation.swift b/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanLineAnimation.swift new file mode 100644 index 0000000..738e834 --- /dev/null +++ b/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanLineAnimation.swift @@ -0,0 +1,64 @@ +// +// ARIScanLineAnimation.swift +// ARIScanCodeKit +// +// Created by marc zhao on 2022/4/7. +// + +import UIKit + +class ARIScanLineAnimation: UIImageView { + + var isAnimationing = false + var animationRect = CGRect.zero + + func startAnimatingWithRect(animationRect: CGRect, parentView: UIView, image: UIImage?) { + self.image = image + self.animationRect = animationRect + parentView.addSubview(self) + + isHidden = false + isAnimationing = true + if image != nil { + stepAnimation() + } + } + + @objc func stepAnimation() { + guard isAnimationing else { + return + } + var frame = animationRect + let hImg = image!.size.height * animationRect.size.width / image!.size.width + + frame.origin.y -= hImg + frame.size.height = hImg + self.frame = frame + alpha = 0.0 + + UIView.animate(withDuration: 1.4, animations: { + self.alpha = 1.0 + var frame = self.animationRect + let hImg = self.image!.size.height * self.animationRect.size.width / self.image!.size.width + frame.origin.y += (frame.size.height - hImg) + frame.size.height = hImg + self.frame = frame + }, completion: { _ in + self.perform(#selector(ARIScanLineAnimation.stepAnimation), with: nil, afterDelay: 0.3) + }) + } + + func stopStepAnimating() { + isHidden = true + isAnimationing = false + } + + public static func instance() -> ARIScanLineAnimation { + return ARIScanLineAnimation() + } + + deinit { + stopStepAnimating() + } + +} diff --git a/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanNetAnimation.swift b/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanNetAnimation.swift new file mode 100644 index 0000000..92fabe2 --- /dev/null +++ b/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanNetAnimation.swift @@ -0,0 +1,71 @@ +// +// ARIScanNetAnimation.swift +// ARIScanCodeKit +// +// Created by marc zhao on 2022/4/7. +// + +import UIKit + + + + +class ARIScanNetAnimation: UIImageView { + + var isAnimationing = false + var animationRect = CGRect.zero + + public static func instance() -> ARIScanNetAnimation { + return ARIScanNetAnimation() + } + + func startAnimatingWithRect(animationRect: CGRect, parentView: UIView, image: UIImage?) { + self.image = image + self.animationRect = animationRect + parentView.addSubview(self) + + isHidden = false + + isAnimationing = true + + if image != nil { + stepAnimation() + } + } + + @objc func stepAnimation() { + guard isAnimationing else { + return + } + var frame = animationRect + + let hImg = image!.size.height * animationRect.size.width / image!.size.width + + frame.origin.y -= hImg + frame.size.height = hImg + self.frame = frame + + alpha = 0.0 + + UIView.animate(withDuration: 1.2, animations: { + self.alpha = 1.0 + + var frame = self.animationRect + let hImg = self.image!.size.height * self.animationRect.size.width / self.image!.size.width + + frame.origin.y += (frame.size.height - hImg) + frame.size.height = hImg + + self.frame = frame + + }, completion: { _ in + self.perform(#selector(ARIScanNetAnimation.stepAnimation), with: nil, afterDelay: 0.3) + }) + } + + func stopStepAnimating() { + isHidden = true + isAnimationing = false + } + +} diff --git a/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanResult.swift b/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanResult.swift new file mode 100644 index 0000000..e886296 --- /dev/null +++ b/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanResult.swift @@ -0,0 +1,32 @@ +// +// ARIScanResult.swift +// ARIScanCodeKit +// +// Created by marc zhao on 2022/4/19. +// + +import Foundation +import UIKit + + +public struct ARIScanResult { + + /// 码内容 + public var strScanned: String? + + /// 扫描图像 + public var imgScanned: UIImage? + + /// 码的类型 + public var strBarCodeType: String? + + /// 码在图像中的位置 + public var arrayCorner: [AnyObject]? + + public init(str: String?, img: UIImage?, barCodeType: String?, corner: [AnyObject]?) { + strScanned = str + imgScanned = img + strBarCodeType = barCodeType + arrayCorner = corner + } +} diff --git a/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanView+GetScanRect.swift b/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanView+GetScanRect.swift new file mode 100644 index 0000000..2416d0a --- /dev/null +++ b/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanView+GetScanRect.swift @@ -0,0 +1,34 @@ +// +// ARIScanView+GetScanRect.swift +// ARIScanCodeKit +// +// Created by marc zhao on 2022/4/7. +// + +import UIKit + + +//MARK: - 公开方法 +public extension ARIScanView { + + /// 获取扫描动画的Rect + func getScanRectForAnimation() -> CGRect { + let XRetangleLeft = viewStyle.xScanRetangleOffset + var sizeRetangle = CGSize(width: frame.size.width - XRetangleLeft * 2, + height: frame.size.width - XRetangleLeft * 2) + + if viewStyle.whRatio != 1 { + let w = sizeRetangle.width + let h = w / viewStyle.whRatio + sizeRetangle = CGSize(width: w, height: CGFloat(Int(h))) + } + + // 扫码区域Y轴最小坐标 + let YMinRetangle = frame.size.height / 2.0 - sizeRetangle.height / 2.0 - viewStyle.centerUpOffset + // 扫码区域坐标 + let cropRect = CGRect(x: XRetangleLeft, y: YMinRetangle, width: sizeRetangle.width, height: sizeRetangle.height) + + return cropRect + } + +} diff --git a/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanView.swift b/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanView.swift new file mode 100644 index 0000000..d5541c5 --- /dev/null +++ b/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanView.swift @@ -0,0 +1,376 @@ +// +// ARIScanView.swift +// ARIScanCodeKit +// +// Created by marc zhao on 2022/4/7. +// + +import UIKit + + + + +open class ARIScanView: UIView { + + // 扫码区域各种参数 + var viewStyle = ARIScanViewStyle() + + // 扫码区域 + var scanRetangleRect = CGRect.zero + + // 线条扫码动画封装 + var scanLineAnimation: ARIScanLineAnimation? + + // 网格扫码动画封装 + var scanNetAnimation: ARIScanNetAnimation? + + // 线条在中间位置,不移动 + var scanLineStill: UIImageView? + + // 启动相机时 菊花等待 + var activityView: UIActivityIndicatorView? + + // 启动相机中的提示文字 + var labelReadying: UILabel? + + // 记录动画状态 + var isAnimationing = false + + /** + 初始化扫描界面 + - parameter frame: 界面大小,一般为视频显示区域 + - parameter vstyle: 界面效果参数 + + - returns: instancetype + */ + public init(frame: CGRect, vstyle: ARIScanViewStyle) { + viewStyle = vstyle + + switch viewStyle.anmiationStyle { + case ARIScanViewAnimationStyle.LineMove: + scanLineAnimation = ARIScanLineAnimation.instance() + case ARIScanViewAnimationStyle.NetGrid: + scanNetAnimation = ARIScanNetAnimation.instance() + case ARIScanViewAnimationStyle.LineStill: + scanLineStill = UIImageView() + scanLineStill?.image = viewStyle.animationImage + default: + break + } + + var frameTmp = frame + frameTmp.origin = CGPoint.zero + + super.init(frame: frameTmp) + + backgroundColor = UIColor.clear + } + + override init(frame: CGRect) { + var frameTmp = frame + frameTmp.origin = CGPoint.zero + + super.init(frame: frameTmp) + + backgroundColor = UIColor.clear + } + + public required init?(coder aDecoder: NSCoder) { + self.init() + } + + deinit { + if scanLineAnimation != nil { + scanLineAnimation!.stopStepAnimating() + } + if scanNetAnimation != nil { + scanNetAnimation!.stopStepAnimating() + } + } + + + // 开始扫描动画 + func startScanAnimation() { + guard !isAnimationing else { + return + } + isAnimationing = true + + let cropRect = getScanRectForAnimation() + + switch viewStyle.anmiationStyle { + case .LineMove: + scanLineAnimation?.startAnimatingWithRect(animationRect: cropRect, + parentView: self, + image: viewStyle.animationImage) + case .NetGrid: + scanNetAnimation?.startAnimatingWithRect(animationRect: cropRect, + parentView: self, + image: viewStyle.animationImage) + case .LineStill: + let stillRect = CGRect(x: cropRect.origin.x + 20, + y: cropRect.origin.y + cropRect.size.height / 2, + width: cropRect.size.width - 40, + height: 2) + scanLineStill?.frame = stillRect + + addSubview(scanLineStill!) + scanLineStill?.isHidden = false + default: break + } + } + + // 开始扫描动画 + func stopScanAnimation() { + isAnimationing = false + switch viewStyle.anmiationStyle { + case .LineMove: + scanLineAnimation?.stopStepAnimating() + case .NetGrid: + scanNetAnimation?.stopStepAnimating() + case .LineStill: + scanLineStill?.isHidden = true + default: break + } + } + + // Only override drawRect: if you perform custom drawing. + // An empty implementation adversely affects performance during animation. + open override func draw(_ rect: CGRect) { + drawScanRect() + } + + //MARK: ----- 绘制扫码效果----- + func drawScanRect() { + let XRetangleLeft = viewStyle.xScanRetangleOffset + var sizeRetangle = CGSize(width: frame.size.width - XRetangleLeft * 2.0, height: frame.size.width - XRetangleLeft * 2.0) + + if viewStyle.whRatio != 1.0 { + let w = sizeRetangle.width + var h = w / viewStyle.whRatio + h = CGFloat(Int(h)) + sizeRetangle = CGSize(width: w, height: h) + } + + // 扫码区域Y轴最小坐标 + let YMinRetangle = frame.size.height / 2.0 - sizeRetangle.height / 2.0 - viewStyle.centerUpOffset + let YMaxRetangle = YMinRetangle + sizeRetangle.height + let XRetangleRight = frame.size.width - XRetangleLeft + + guard let context = UIGraphicsGetCurrentContext() else { + return + } + + // 非扫码区域半透明 + // 设置非识别区域颜色 + context.setFillColor(viewStyle.color_NotRecoginitonArea.cgColor) + // 填充矩形 + // 扫码区域上面填充 + var rect = CGRect(x: 0, y: 0, width: frame.size.width, height: YMinRetangle) + context.fill(rect) + + // 扫码区域左边填充 + rect = CGRect(x: 0, y: YMinRetangle, width: XRetangleLeft, height: sizeRetangle.height) + context.fill(rect) + + // 扫码区域右边填充 + rect = CGRect(x: XRetangleRight, y: YMinRetangle, width: XRetangleLeft, height: sizeRetangle.height) + context.fill(rect) + + // 扫码区域下面填充 + rect = CGRect(x: 0, y: YMaxRetangle, width: frame.size.width, height: frame.size.height - YMaxRetangle) + context.fill(rect) + // 执行绘画 + context.strokePath() + + if viewStyle.isNeedShowRetangle { + // 中间画矩形(正方形) + context.setStrokeColor(viewStyle.colorRetangleLine.cgColor) + context.setLineWidth(viewStyle.widthRetangleLine) + context.addRect(CGRect(x: XRetangleLeft, y: YMinRetangle, width: sizeRetangle.width, height: sizeRetangle.height)) + + // CGContextMoveToPoint(context, XRetangleLeft, YMinRetangle); + // CGContextAddLineToPoint(context, XRetangleLeft+sizeRetangle.width, YMinRetangle); + context.strokePath() + } + + scanRetangleRect = CGRect(x: XRetangleLeft, y: YMinRetangle, width: sizeRetangle.width, height: sizeRetangle.height) + + + // 画矩形框4格外围相框角 + // 相框角的宽度和高度 + let wAngle = viewStyle.photoframeAngleW + let hAngle = viewStyle.photoframeAngleH + + // 4个角的 线的宽度 + let linewidthAngle = viewStyle.photoframeLineW // 经验参数:6和4 + // 画扫码矩形以及周边半透明黑色坐标参数 + var diffAngle = linewidthAngle / 3 + diffAngle = linewidthAngle / 2 // 框外面4个角,与框有缝隙 + diffAngle = linewidthAngle / 2 // 框4个角 在线上加4个角效果 + diffAngle = 0 // 与矩形框重合 + + switch viewStyle.photoframeAngleStyle { + case .Outer: diffAngle = linewidthAngle / 3 // 框外面4个角,与框紧密联系在一起 + case .On: diffAngle = 0 + case .Inner: diffAngle = -viewStyle.photoframeLineW / 2 + } + + context.setStrokeColor(viewStyle.colorAngle.cgColor) + context.setFillColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) + + // Draw them with a 2.0 stroke width so they are a bit more visible. + context.setLineWidth(linewidthAngle) + + + // + let leftX = XRetangleLeft - diffAngle + let topY = YMinRetangle - diffAngle + let rightX = XRetangleRight + diffAngle + let bottomY = YMaxRetangle + diffAngle + + // 左上角水平线 + context.move(to: CGPoint(x: leftX - linewidthAngle / 2, y: topY)) + context.addLine(to: CGPoint(x: leftX + wAngle, y: topY)) + + // 左上角垂直线 + context.move(to: CGPoint(x: leftX, y: topY - linewidthAngle / 2)) + context.addLine(to: CGPoint(x: leftX, y: topY + hAngle)) + + // 左下角水平线 + context.move(to: CGPoint(x: leftX - linewidthAngle / 2, y: bottomY)) + context.addLine(to: CGPoint(x: leftX + wAngle, y: bottomY)) + + // 左下角垂直线 + context.move(to: CGPoint(x: leftX, y: bottomY + linewidthAngle / 2)) + context.addLine(to: CGPoint(x: leftX, y: bottomY - hAngle)) + + // 右上角水平线 + context.move(to: CGPoint(x: rightX + linewidthAngle / 2, y: topY)) + context.addLine(to: CGPoint(x: rightX - wAngle, y: topY)) + + // 右上角垂直线 + context.move(to: CGPoint(x: rightX, y: topY - linewidthAngle / 2)) + context.addLine(to: CGPoint(x: rightX, y: topY + hAngle)) + + // 右下角水平线 + context.move(to: CGPoint(x: rightX + linewidthAngle / 2, y: bottomY)) + context.addLine(to: CGPoint(x: rightX - wAngle, y: bottomY)) + + // 右下角垂直线 + context.move(to: CGPoint(x: rightX, y: bottomY + linewidthAngle / 2)) + context.addLine(to: CGPoint(x: rightX, y: bottomY - hAngle)) + + context.strokePath() + } + + // 根据矩形区域,获取识别区域 + static func getScanRectWithPreView(preView: UIView, style: ARIScanViewStyle) -> CGRect { + let XRetangleLeft = style.xScanRetangleOffset + let width = preView.frame.size.width - XRetangleLeft * 2 + let height = width + var sizeRetangle = CGSize(width: width, height: height) + + if style.whRatio != 1 { + let w = sizeRetangle.width + var h = w / style.whRatio + + let hInt: Int = Int(h) + h = CGFloat(hInt) + + sizeRetangle = CGSize(width: w, height: h) + } + + // 扫码区域Y轴最小坐标 + let YMinRetangle = preView.frame.size.height / 2.0 - sizeRetangle.height / 2.0 - style.centerUpOffset + // 扫码区域坐标 + let cropRect = CGRect(x: XRetangleLeft, y: YMinRetangle, width: sizeRetangle.width, height: sizeRetangle.height) + + // 计算兴趣区域 + var rectOfInterest: CGRect + + // ref:http://www.cocoachina.com/ios/20141225/10763.html + let size = preView.bounds.size + let p1 = size.height / size.width + + let p2: CGFloat = 1920.0 / 1080.0 // 使用了1080p的图像输出 + if p1 < p2 { + let fixHeight = size.width * 1920.0 / 1080.0 + let fixPadding = (fixHeight - size.height) / 2 + rectOfInterest = CGRect(x: (cropRect.origin.y + fixPadding) / fixHeight, + y: cropRect.origin.x / size.width, + width: cropRect.size.height / fixHeight, + height: cropRect.size.width / size.width) + + } else { + let fixWidth = size.height * 1080.0 / 1920.0 + let fixPadding = (fixWidth - size.width) / 2 + rectOfInterest = CGRect(x: cropRect.origin.y / size.height, + y: (cropRect.origin.x + fixPadding) / fixWidth, + width: cropRect.size.height / size.height, + height: cropRect.size.width / fixWidth) + } + + return rectOfInterest + } + + func getRetangeSize() -> CGSize { + let XRetangleLeft = viewStyle.xScanRetangleOffset + var sizeRetangle = CGSize(width: frame.size.width - XRetangleLeft * 2, height: frame.size.width - XRetangleLeft * 2) + + let w = sizeRetangle.width + var h = w / viewStyle.whRatio + h = CGFloat(Int(h)) + sizeRetangle = CGSize(width: w, height: h) + + return sizeRetangle + } + + func deviceStartReadying(readyStr: String) { + let XRetangleLeft = viewStyle.xScanRetangleOffset + let sizeRetangle = getRetangeSize() + + // 扫码区域Y轴最小坐标 + let YMinRetangle = frame.size.height / 2.0 - sizeRetangle.height / 2.0 - viewStyle.centerUpOffset + + // 设备启动状态提示 + if activityView == nil { + activityView = UIActivityIndicatorView(frame: CGRect(x: 0, y: 0, width: 30, height: 30)) + + activityView?.center = CGPoint(x: XRetangleLeft + sizeRetangle.width / 2 - 50, y: YMinRetangle + sizeRetangle.height / 2) + if #available(iOS 13.0, *) { + activityView?.style = UIActivityIndicatorView.Style.large + }else { + activityView?.style = UIActivityIndicatorView.Style.whiteLarge + } + addSubview(activityView!) + + let labelReadyRect = CGRect(x: activityView!.frame.origin.x + activityView!.frame.size.width + 10, + y: activityView!.frame.origin.y, + width: 100, + height: 30) + labelReadying = UILabel(frame: labelReadyRect) + labelReadying?.text = readyStr + labelReadying?.backgroundColor = UIColor.clear + labelReadying?.textColor = UIColor.white + labelReadying?.font = UIFont.systemFont(ofSize: 18.0) + addSubview(labelReadying!) + } + + addSubview(labelReadying!) + activityView?.startAnimating() + } + + func deviceStopReadying() { + if activityView != nil { + activityView?.stopAnimating() + activityView?.removeFromSuperview() + labelReadying?.removeFromSuperview() + + activityView = nil + labelReadying = nil + } + } + +} + diff --git a/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanViewController.swift b/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanViewController.swift new file mode 100644 index 0000000..e895228 --- /dev/null +++ b/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanViewController.swift @@ -0,0 +1,193 @@ +// +// ARIScanViewController.swift +// ARIScanCodeKit +// +// Created by marc zhao on 2022/4/7. +// + +import UIKit +import Foundation +import AVFoundation + +public protocol ARIScanViewControllerDelegate: AnyObject { + func scanFinished(scanResult: ARIScanResult, error: String?) +} + +public protocol QRRectDelegate { + func drawwed() +} + +open class ARIScanViewController: UIViewController { + + // 返回扫码结果,也可以通过继承本控制器,改写该handleCodeResult方法即可 + open weak var scanResultDelegate: ARIScanViewControllerDelegate? + + open var delegate: QRRectDelegate? + + open var scanObj: ARIScanWrapper? + + open var scanStyle: ARIScanViewStyle? = ARIScanViewStyle() + + open var qRScanView: ARIScanView? + + // 启动区域识别功能 + open var isOpenInterestRect = false + + //连续扫码 + open var isSupportContinuous = false; + + // 识别码的类型 + public var arrayCodeType: [AVMetadataObject.ObjectType]? + + // 是否需要识别后的当前图像 + public var isNeedCodeImage = false + + // 相机启动提示文字 + public var readyString: String! = "loading" + + open override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + // [self.view addSubview:_qRScanView]; + view.backgroundColor = UIColor.black + edgesForExtendedLayout = UIRectEdge(rawValue: 0) + } + + open func setNeedCodeImage(needCodeImg: Bool) { + isNeedCodeImage = needCodeImg + } + + // 设置框内识别 + open func setOpenInterestRect(isOpen: Bool) { + isOpenInterestRect = isOpen + } + + open override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + drawScanView() + perform(#selector(ARIScanViewController.startScan), with: nil, afterDelay: 0.3) + } + + @objc open func startScan() { + if scanObj == nil { + var cropRect = CGRect.zero + if isOpenInterestRect { + cropRect = ARIScanView.getScanRectWithPreView(preView: view, style: scanStyle!) + } + + // 指定识别几种码 + if arrayCodeType == nil { + arrayCodeType = [AVMetadataObject.ObjectType.qr as NSString, + AVMetadataObject.ObjectType.ean13 as NSString, + AVMetadataObject.ObjectType.code128 as NSString] as [AVMetadataObject.ObjectType] + } + + scanObj = ARIScanWrapper(videoPreView: view, + objType: arrayCodeType!, + isCaptureImg: isNeedCodeImage, + cropRect: cropRect, + success: { [weak self] (arrayResult) -> Void in + guard let strongSelf = self else { + return + } + if !strongSelf.isSupportContinuous { + // 停止扫描动画 + strongSelf.qRScanView?.stopScanAnimation() + } + strongSelf.handleCodeResult(arrayResult: arrayResult) + }) + } + + scanObj?.supportContinuous = isSupportContinuous; + + // 结束相机等待提示 + qRScanView?.deviceStopReadying() + + // 开始扫描动画 + qRScanView?.startScanAnimation() + + // 相机运行 + scanObj?.start() + } + + open func drawScanView() { + if qRScanView == nil { + qRScanView = ARIScanView(frame: view.frame, vstyle: scanStyle!) + view.addSubview(qRScanView!) + delegate?.drawwed() + } + qRScanView?.deviceStartReadying(readyStr: readyString) + } + + + /** + 处理扫码结果,如果是继承本控制器的,可以重写该方法,作出相应地处理,或者设置delegate作出相应处理 + */ + open func handleCodeResult(arrayResult: [ARIScanResult]) { + guard let delegate = scanResultDelegate else { + fatalError("you must set scanResultDelegate or override this method without super keyword") + } + + if !isSupportContinuous { + navigationController?.popViewController(animated: true) + + } + + if let result = arrayResult.first { + delegate.scanFinished(scanResult: result, error: nil) + } else { + let result = ARIScanResult(str: nil, img: nil, barCodeType: nil, corner: nil) + delegate.scanFinished(scanResult: result, error: "no scan result") + } + } + + open override func viewWillDisappear(_ animated: Bool) { + NSObject.cancelPreviousPerformRequests(withTarget: self) + qRScanView?.stopScanAnimation() + scanObj?.stop() + } + + @objc open func openPhotoAlbum() { + ARIPermissions.authorizePhotoWith { [weak self] _ in + let picker = UIImagePickerController() + picker.sourceType = UIImagePickerController.SourceType.photoLibrary + picker.delegate = self + picker.allowsEditing = true + self?.present(picker, animated: true, completion: nil) + } + } +} + +//MARK: - 图片选择代理方法 +extension ARIScanViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { + + //MARK: -----相册选择图片识别二维码 (条形码没有找到系统方法) + public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + picker.dismiss(animated: true, completion: nil) + + let editedImage = info[UIImagePickerController.InfoKey.editedImage] as? UIImage + let originalImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage + guard let image = editedImage ?? originalImage else { + showAlert(title: nil, message: NSLocalizedString("Identify failed", comment: "Identify failed")) + return + } + let arrayResult = ARIScanWrapper.recognizeQRImage(image: image) + if !arrayResult.isEmpty { + handleCodeResult(arrayResult: arrayResult) + } + } + +} + +//MARK: - 私有方法 +private extension ARIScanViewController { + + func showAlert(title: String?, message: String?) { + let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert) + let alertAction = UIAlertAction(title: NSLocalizedString("OK", comment: "OK"), style: .default, handler: nil) + alertController.addAction(alertAction) + present(alertController, animated: true, completion: nil) + } + +} diff --git a/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanViewStyle.swift b/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanViewStyle.swift new file mode 100644 index 0000000..dfb381c --- /dev/null +++ b/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanViewStyle.swift @@ -0,0 +1,73 @@ +// +// ARIScanViewStyle.swift +// ARIScanCodeKit +// +// Created by marc zhao on 2022/4/7. +// + +import UIKit + +/// 扫码区域动画效果 +public enum ARIScanViewAnimationStyle { + case LineMove // 线条上下移动 + case NetGrid // 网格 + case LineStill // 线条停止在扫码区域中央 + case None // 无动画 +} + +/// 扫码区域4个角位置类型 +public enum ARIScanViewPhotoframeAngleStyle { + case Inner // 内嵌,一般不显示矩形框情况下 + case Outer // 外嵌,包围在矩形框的4个角 + case On // 在矩形框的4个角上,覆盖 +} + + +public struct ARIScanViewStyle { + + // MARK: - 中心位置矩形框 + /// 是否需要绘制扫码矩形框,默认YES + public var isNeedShowRetangle = true + + /// 默认扫码区域为正方形,如果扫码区域不是正方形,设置宽高比 + public var whRatio: CGFloat = 1.0 + + /// 矩形框(视频显示透明区)域向上移动偏移量,0表示扫码透明区域在当前视图中心位置,如果负值表示扫码区域下移 + public var centerUpOffset: CGFloat = 44 + + /// 矩形框(视频显示透明区)域离界面左边及右边距离,默认60 + public var xScanRetangleOffset: CGFloat = 60 + + /// 矩形框线条颜色,默认白色 + public var colorRetangleLine = UIColor.white + + /// 矩形框线条宽度,默认1 + public var widthRetangleLine: CGFloat = 1.0 + + //MARK: - 矩形框(扫码区域)周围4个角 + /// 扫码区域的4个角类型 + public var photoframeAngleStyle = ARIScanViewPhotoframeAngleStyle.Outer + + /// 4个角的颜色 + public var colorAngle = UIColor(red: 0.0, green: 167.0 / 255.0, blue: 231.0 / 255.0, alpha: 1.0) + + /// 扫码区域4个角的宽度和高度 + public var photoframeAngleW: CGFloat = 24.0 + public var photoframeAngleH: CGFloat = 24.0 + + /// 扫码区域4个角的线条宽度,默认6,建议8到4之间 + public var photoframeLineW: CGFloat = 6 + + //MARK: - 动画效果 + /// 扫码动画效果:线条或网格 + public var anmiationStyle = ARIScanViewAnimationStyle.LineMove + + /// 动画效果的图像,如线条或网格的图像 + public var animationImage: UIImage? + + //MARK: - 非识别区域颜色, 默认 RGBA (0,0,0,0.5),范围(0--1) + public var color_NotRecoginitonArea = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.5) + + public init() { } + +} diff --git a/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanWrapper.swift b/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanWrapper.swift new file mode 100644 index 0000000..9424457 --- /dev/null +++ b/MemoryChainKit/Sources/UI Component/ScanQRCode/ARIScanWrapper.swift @@ -0,0 +1,537 @@ +// +// ARIScanWrapper.swift +// ARIScanCodeKit +// +// Created by marc zhao on 2022/4/7. +// + + + + + +import UIKit +import AVFoundation + + + + +open class ARIScanWrapper: NSObject,AVCaptureMetadataOutputObjectsDelegate, AVCapturePhotoCaptureDelegate { + + let device = AVCaptureDevice.default(for: AVMediaType.video) + var input: AVCaptureDeviceInput? + var output: AVCaptureMetadataOutput + + let session = AVCaptureSession() + var previewLayer: AVCaptureVideoPreviewLayer? + var stillImageOutput: AVCapturePhotoOutput + + // 存储返回结果 + var arrayResult = [ARIScanResult]() + + // 扫码结果返回block + var successBlock: ([ARIScanResult]) -> Void + + // 是否需要拍照 + var isNeedCaptureImage: Bool + + // 当前扫码结果是否处理 + var isNeedScanResult = true + + //连续扫码 + var supportContinuous = false + + + /** + 初始化设备 + - parameter videoPreView: 视频显示UIView + - parameter objType: 识别码的类型,缺省值 QR二维码 + - parameter isCaptureImg: 识别后是否采集当前照片 + - parameter cropRect: 识别区域 + - parameter success: 返回识别信息 + - returns: + */ + init(videoPreView: UIView, + objType: [AVMetadataObject.ObjectType] = [(AVMetadataObject.ObjectType.qr as NSString) as AVMetadataObject.ObjectType], + isCaptureImg: Bool, + cropRect: CGRect = .zero, + success: @escaping (([ARIScanResult]) -> Void)) { + + successBlock = success + output = AVCaptureMetadataOutput() + isNeedCaptureImage = isCaptureImg + stillImageOutput = AVCapturePhotoOutput() + + super.init() + + guard let device = device else { + return + } + do { + input = try AVCaptureDeviceInput(device: device) + } catch let error as NSError { + print("AVCaptureDeviceInput(): \(error)") + } + guard let input = input else { + return + } + + if session.canAddInput(input) { + session.addInput(input) + } + + if session.canAddOutput(output) { + session.addOutput(output) + } + + if session.canAddOutput(stillImageOutput) { + session.addOutput(stillImageOutput) + } + let settings = AVCapturePhotoSettings() + settings.livePhotoVideoCodecType = .jpeg + stillImageOutput.capturePhoto(with: settings , delegate:self ) + + + session.sessionPreset = AVCaptureSession.Preset.high + + // 参数设置 + output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) + + output.metadataObjectTypes = objType + + // output.metadataObjectTypes = [AVMetadataObjectTypeQRCode,AVMetadataObjectTypeEAN13Code, AVMetadataObjectTypeEAN8Code, AVMetadataObjectTypeCode128Code] + if !cropRect.equalTo(CGRect.zero) { + // 启动相机后,直接修改该参数无效 + output.rectOfInterest = cropRect + } + + previewLayer = AVCaptureVideoPreviewLayer(session: session) + previewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill + + var frame: CGRect = videoPreView.frame + frame.origin = CGPoint.zero + previewLayer?.frame = frame + + videoPreView.layer.insertSublayer(previewLayer!, at: 0) + + if device.isFocusPointOfInterestSupported && device.isFocusModeSupported(.continuousAutoFocus) { + do { + try input.device.lockForConfiguration() + input.device.focusMode = AVCaptureDevice.FocusMode.continuousAutoFocus + input.device.unlockForConfiguration() + } catch let error as NSError { + print("device.lockForConfiguration(): \(error)") + } + } + } + + public func metadataOutput(_ output: AVCaptureMetadataOutput, + didOutput metadataObjects: [AVMetadataObject], + from connection: AVCaptureConnection) { + captureOutput(output, didOutputMetadataObjects: metadataObjects, from: connection) + } + + func start() { + if !session.isRunning { + isNeedScanResult = true + session.startRunning() + } + } + + func stop() { + if session.isRunning { + isNeedScanResult = false + session.stopRunning() + } + } + + open func captureOutput(_ captureOutput: AVCaptureOutput, + didOutputMetadataObjects metadataObjects: [Any], + from connection: AVCaptureConnection!) { + guard isNeedScanResult else { + // 上一帧处理中 + return + } + isNeedScanResult = false + + arrayResult.removeAll() + + // 识别扫码类型 + for current in metadataObjects { + guard let code = current as? AVMetadataMachineReadableCodeObject else { + continue + } + + #if !targetEnvironment(simulator) + + + arrayResult.append(ARIScanResult(str: code.stringValue, img: UIImage(), barCodeType: code.type.rawValue, corner: code.corners as [AnyObject]?)) + + + #endif + } + + if arrayResult.isEmpty || supportContinuous { + isNeedScanResult = true + } + if !arrayResult.isEmpty { + + if supportContinuous { + successBlock(arrayResult) + } + else if isNeedCaptureImage { + captureImage() + } else { + stop() + successBlock(arrayResult) + } + } + } + @available(iOS 11.0, *) + public func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { + + guard let imageData = photo.fileDataRepresentation() else { + return + } + let scanImage = UIImage(data: imageData) + for idx in 0 ... self.arrayResult.count - 1 { + self.arrayResult[idx].imgScanned = scanImage + } + } + //MARK: ----拍照 + open func captureImage() { + + let photoSettings = AVCapturePhotoSettings() + if let photoPreviewType = photoSettings.availablePreviewPhotoPixelFormatTypes.first { + photoSettings.previewPhotoFormat = [kCVPixelBufferPixelFormatTypeKey as String: photoPreviewType] + stillImageOutput.capturePhoto(with: photoSettings, delegate: self) + } + + } + + open func connectionWithMediaType(mediaType: AVMediaType, connections: [AnyObject]) -> AVCaptureConnection? { + for connection in connections { + guard let connectionTmp = connection as? AVCaptureConnection else { + continue + } + for port in connectionTmp.inputPorts where port.mediaType == mediaType { + return connectionTmp + } + } + return nil + } + + + //MARK: 切换识别区域 + open func changeScanRect(cropRect: CGRect) { + // 待测试,不知道是否有效 + stop() + output.rectOfInterest = cropRect + start() + } + + //MARK: 切换识别码的类型 + open func changeScanType(objType: [AVMetadataObject.ObjectType]) { + // 待测试中途修改是否有效 + output.metadataObjectTypes = objType + } + + open func isGetFlash() -> Bool { + return device != nil && device!.hasFlash && device!.hasTorch + } + + /** + 打开或关闭闪关灯 + - parameter torch: true:打开闪关灯 false:关闭闪光灯 + */ + open func setTorch(torch: Bool) { + guard isGetFlash() else { + return + } + do { + try input?.device.lockForConfiguration() + input?.device.torchMode = torch ? AVCaptureDevice.TorchMode.on : AVCaptureDevice.TorchMode.off + input?.device.unlockForConfiguration() + } catch let error as NSError { + print("device.lockForConfiguration(): \(error)") + } + } + + + /// 闪光灯打开或关闭 + open func changeTorch() { + let torch = input?.device.torchMode == .off + setTorch(torch: torch) + } + + //MARK: ------获取系统默认支持的码的类型 + static func defaultMetaDataObjectTypes() -> [AVMetadataObject.ObjectType] { + var types = + [ + AVMetadataObject.ObjectType.qr, + AVMetadataObject.ObjectType.upce, + AVMetadataObject.ObjectType.code39, + AVMetadataObject.ObjectType.code39Mod43, + AVMetadataObject.ObjectType.ean13, + AVMetadataObject.ObjectType.ean8, + AVMetadataObject.ObjectType.code93, + AVMetadataObject.ObjectType.code128, + AVMetadataObject.ObjectType.pdf417, + AVMetadataObject.ObjectType.aztec, + ] + // if #available(iOS 8.0, *) + types.append(AVMetadataObject.ObjectType.interleaved2of5) + types.append(AVMetadataObject.ObjectType.itf14) + types.append(AVMetadataObject.ObjectType.dataMatrix) + return types + } + + /** + 识别二维码码图像 + + - parameter image: 二维码图像 + + - returns: 返回识别结果 + */ + public static func recognizeQRImage(image: UIImage) -> [ARIScanResult] { + guard let cgImage = image.cgImage else { + return [] + } + let detector = CIDetector(ofType: CIDetectorTypeQRCode, + context: nil, + options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])! + let img = CIImage(cgImage: cgImage) + let features = detector.features(in: img, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]) + return features.filter { + $0.isKind(of: CIQRCodeFeature.self) + }.map { + $0 as! CIQRCodeFeature + }.map { + ARIScanResult(str: $0.messageString, + img: image, + barCodeType: AVMetadataObject.ObjectType.qr.rawValue, + corner: nil) + } + } + + + //MARK: -- - 生成二维码,背景色及二维码颜色设置 + public static func createCode(codeType: String, codeString: String, size: CGSize, qrColor: UIColor, bkColor: UIColor) -> UIImage? { + let stringData = codeString.data(using: .utf8) + + // 系统自带能生成的码 + // CIAztecCodeGenerator + // CICode128BarcodeGenerator + // CIPDF417BarcodeGenerator + // CIQRCodeGenerator + let qrFilter = CIFilter(name: codeType) + qrFilter?.setValue(stringData, forKey: "inputMessage") + qrFilter?.setValue("H", forKey: "inputCorrectionLevel") + + // 上色 + let colorFilter = CIFilter(name: "CIFalseColor", + parameters: [ + "inputImage": qrFilter!.outputImage!, + "inputColor0": CIColor(cgColor: qrColor.cgColor), + "inputColor1": CIColor(cgColor: bkColor.cgColor), + ] + ) + + guard let qrImage = colorFilter?.outputImage, + let cgImage = CIContext().createCGImage(qrImage, from: qrImage.extent) else { + return nil + } + + UIGraphicsBeginImageContext(size) + let context = UIGraphicsGetCurrentContext()! + context.interpolationQuality = CGInterpolationQuality.none + context.scaleBy(x: 1.0, y: -1.0) + context.draw(cgImage, in: context.boundingBoxOfClipPath) + let codeImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return codeImage + } + + public static func createCode128(codeString: String, size: CGSize, qrColor: UIColor, bkColor: UIColor) -> UIImage? { + let stringData = codeString.data(using: String.Encoding.utf8) + + // 系统自带能生成的码 + // CIAztecCodeGenerator 二维码 + // CICode128BarcodeGenerator 条形码 + // CIPDF417BarcodeGenerator + // CIQRCodeGenerator 二维码 + let qrFilter = CIFilter(name: "CICode128BarcodeGenerator") + qrFilter?.setDefaults() + qrFilter?.setValue(stringData, forKey: "inputMessage") + + guard let outputImage = qrFilter?.outputImage else { + return nil + } + let context = CIContext() + guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else { + return nil + } + let image = UIImage(cgImage: cgImage, scale: 1.0, orientation: UIImage.Orientation.up) + + // Resize without interpolating + return resizeImage(image: image, quality: CGInterpolationQuality.none, rate: 20.0) + } + + + // 根据扫描结果,获取图像中得二维码区域图像(如果相机拍摄角度故意很倾斜,获取的图像效果很差) + static func getConcreteCodeImage(srcCodeImage: UIImage, codeResult: ARIScanResult) -> UIImage? { + let rect = getConcreteCodeRectFromImage(srcCodeImage: srcCodeImage, codeResult: codeResult) + guard !rect.isEmpty, let img = imageByCroppingWithStyle(srcImg: srcCodeImage, rect: rect) else { + return nil + } + return imageRotation(image: img, orientation: UIImage.Orientation.right) + } + + // 根据二维码的区域截取二维码区域图像 + public static func getConcreteCodeImage(srcCodeImage: UIImage, rect: CGRect) -> UIImage? { + guard !rect.isEmpty, let img = imageByCroppingWithStyle(srcImg: srcCodeImage, rect: rect) else { + return nil + } + return imageRotation(image: img, orientation: UIImage.Orientation.right) + } + + // 获取二维码的图像区域 + public static func getConcreteCodeRectFromImage(srcCodeImage: UIImage, codeResult: ARIScanResult) -> CGRect { + guard let corner = codeResult.arrayCorner as? [[String: Float]], corner.count >= 4 else { + return .zero + } + + let dicTopLeft = corner[0] + let dicTopRight = corner[1] + let dicBottomRight = corner[2] + let dicBottomLeft = corner[3] + + let xLeftTopRatio = dicTopLeft["X"]! + let yLeftTopRatio = dicTopLeft["Y"]! + + let xRightTopRatio = dicTopRight["X"]! + let yRightTopRatio = dicTopRight["Y"]! + + let xBottomRightRatio = dicBottomRight["X"]! + let yBottomRightRatio = dicBottomRight["Y"]! + + let xLeftBottomRatio = dicBottomLeft["X"]! + let yLeftBottomRatio = dicBottomLeft["Y"]! + + // 由于截图只能矩形,所以截图不规则四边形的最大外围 + let xMinLeft = CGFloat(min(xLeftTopRatio, xLeftBottomRatio)) + let xMaxRight = CGFloat(max(xRightTopRatio, xBottomRightRatio)) + + let yMinTop = CGFloat(min(yLeftTopRatio, yRightTopRatio)) + let yMaxBottom = CGFloat(max(yLeftBottomRatio, yBottomRightRatio)) + + let imgW = srcCodeImage.size.width + let imgH = srcCodeImage.size.height + + // 宽高反过来计算 + return CGRect(x: xMinLeft * imgH, + y: yMinTop * imgW, + width: (xMaxRight - xMinLeft) * imgH, + height: (yMaxBottom - yMinTop) * imgW) + } + + //MARK: ----图像处理 + + /** + @brief 图像中间加logo图片 + @param srcImg 原图像 + @param LogoImage logo图像 + @param logoSize logo图像尺寸 + @return 加Logo的图像 + */ + public static func addImageLogo(srcImg: UIImage, logoImg: UIImage, logoSize: CGSize) -> UIImage { + UIGraphicsBeginImageContext(srcImg.size) + srcImg.draw(in: CGRect(x: 0, y: 0, width: srcImg.size.width, height: srcImg.size.height)) + let rect = CGRect(x: srcImg.size.width / 2 - logoSize.width / 2, + y: srcImg.size.height / 2 - logoSize.height / 2, + width: logoSize.width, + height: logoSize.height) + logoImg.draw(in: rect) + let resultingImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return resultingImage! + } + + //图像缩放 + static func resizeImage(image: UIImage, quality: CGInterpolationQuality, rate: CGFloat) -> UIImage? { + var resized: UIImage? + let width = image.size.width * rate + let height = image.size.height * rate + + UIGraphicsBeginImageContext(CGSize(width: width, height: height)) + let context = UIGraphicsGetCurrentContext() + context?.interpolationQuality = quality + image.draw(in: CGRect(x: 0, y: 0, width: width, height: height)) + + resized = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return resized + } + + // 图像裁剪 + static func imageByCroppingWithStyle(srcImg: UIImage, rect: CGRect) -> UIImage? { + guard let imagePartRef = srcImg.cgImage?.cropping(to: rect) else { + return nil + } + return UIImage(cgImage: imagePartRef) + } + + // 图像旋转 + static func imageRotation(image: UIImage, orientation: UIImage.Orientation) -> UIImage { + var rotate: Double = 0.0 + var rect: CGRect + var translateX: CGFloat = 0.0 + var translateY: CGFloat = 0.0 + var scaleX: CGFloat = 1.0 + var scaleY: CGFloat = 1.0 + + switch orientation { + case .left: + rotate = .pi / 2 + rect = CGRect(x: 0, y: 0, width: image.size.height, height: image.size.width) + translateX = 0 + translateY = -rect.size.width + scaleY = rect.size.width / rect.size.height + scaleX = rect.size.height / rect.size.width + case .right: + rotate = 3 * .pi / 2 + rect = CGRect(x: 0, y: 0, width: image.size.height, height: image.size.width) + translateX = -rect.size.height + translateY = 0 + scaleY = rect.size.width / rect.size.height + scaleX = rect.size.height / rect.size.width + case .down: + rotate = .pi + rect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height) + translateX = -rect.size.width + translateY = -rect.size.height + default: + rotate = 0.0 + rect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height) + translateX = 0 + translateY = 0 + } + + UIGraphicsBeginImageContext(rect.size) + let context = UIGraphicsGetCurrentContext()! + // 做CTM变换 + context.translateBy(x: 0.0, y: rect.size.height) + context.scaleBy(x: 1.0, y: -1.0) + context.rotate(by: CGFloat(rotate)) + context.translateBy(x: translateX, y: translateY) + + context.scaleBy(x: scaleX, y: scaleY) + // 绘制图片 + context.draw(image.cgImage!, in: CGRect(x: 0, y: 0, width: rect.size.width, height: rect.size.height)) + return UIGraphicsGetImageFromCurrentImageContext()! + } + +} + +