184 lines
5.7 KiB
Swift
184 lines
5.7 KiB
Swift
//
|
|
// QRCodeScannerViewController.swift
|
|
// suixinkan
|
|
//
|
|
|
|
import AVFoundation
|
|
import SnapKit
|
|
import UIKit
|
|
|
|
/// AVFoundation 二维码扫描页。
|
|
final class QRCodeScannerViewController: BaseViewController, AVCaptureMetadataOutputObjectsDelegate {
|
|
|
|
var onScanResult: ((String) -> Void)?
|
|
|
|
private let captureSession = AVCaptureSession()
|
|
private var previewLayer: AVCaptureVideoPreviewLayer?
|
|
private var isCameraConfigured = false
|
|
private var hasHandledResult = false
|
|
|
|
private let maskView = UIView()
|
|
private let hintLabel = UILabel()
|
|
private let closeButton = UIButton(type: .system)
|
|
|
|
override func setupNavigationBar() {
|
|
navigationController?.setNavigationBarHidden(true, animated: false)
|
|
}
|
|
|
|
override func setupUI() {
|
|
view.backgroundColor = .black
|
|
hintLabel.text = "将二维码放入框内"
|
|
hintLabel.textColor = .white
|
|
hintLabel.font = .systemFont(ofSize: 14)
|
|
hintLabel.textAlignment = .center
|
|
|
|
closeButton.setTitle("关闭", for: .normal)
|
|
closeButton.setTitleColor(.white, for: .normal)
|
|
closeButton.addTarget(self, action: #selector(closeTapped), for: .touchUpInside)
|
|
|
|
view.addSubview(maskView)
|
|
view.addSubview(hintLabel)
|
|
view.addSubview(closeButton)
|
|
}
|
|
|
|
override func setupConstraints() {
|
|
closeButton.snp.makeConstraints { make in
|
|
make.top.equalTo(view.safeAreaLayoutGuide).offset(12)
|
|
make.trailing.equalToSuperview().inset(16)
|
|
}
|
|
hintLabel.snp.makeConstraints { make in
|
|
make.centerX.equalToSuperview()
|
|
make.bottom.equalTo(view.safeAreaLayoutGuide).inset(48)
|
|
}
|
|
maskView.snp.makeConstraints { make in
|
|
make.center.equalToSuperview()
|
|
make.size.equalTo(260)
|
|
}
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
checkCameraPermission()
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
updatePreviewLayout()
|
|
startCaptureSessionIfNeeded()
|
|
}
|
|
|
|
override func viewDidLayoutSubviews() {
|
|
super.viewDidLayoutSubviews()
|
|
updatePreviewLayout()
|
|
updateMask()
|
|
}
|
|
|
|
override func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
stopCaptureSession()
|
|
}
|
|
|
|
private func checkCameraPermission() {
|
|
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
|
case .authorized:
|
|
setupCamera()
|
|
case .notDetermined:
|
|
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
|
|
DispatchQueue.main.async {
|
|
granted ? self?.setupCamera() : self?.showPermissionDenied()
|
|
}
|
|
}
|
|
default:
|
|
showPermissionDenied()
|
|
}
|
|
}
|
|
|
|
private func showPermissionDenied() {
|
|
showToast("需要相机权限才能扫码")
|
|
dismiss(animated: true)
|
|
}
|
|
|
|
private func setupCamera() {
|
|
if isCameraConfigured {
|
|
updatePreviewLayout()
|
|
startCaptureSessionIfNeeded()
|
|
return
|
|
}
|
|
|
|
guard let device = AVCaptureDevice.default(for: .video),
|
|
let input = try? AVCaptureDeviceInput(device: device) else {
|
|
showToast("无法打开相机")
|
|
return
|
|
}
|
|
|
|
captureSession.beginConfiguration()
|
|
defer { captureSession.commitConfiguration() }
|
|
|
|
guard captureSession.canAddInput(input) else { return }
|
|
captureSession.addInput(input)
|
|
|
|
let output = AVCaptureMetadataOutput()
|
|
guard captureSession.canAddOutput(output) else { return }
|
|
captureSession.addOutput(output)
|
|
output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
|
|
output.metadataObjectTypes = [.qr]
|
|
|
|
let layer = AVCaptureVideoPreviewLayer(session: captureSession)
|
|
layer.videoGravity = .resizeAspectFill
|
|
view.layer.insertSublayer(layer, at: 0)
|
|
previewLayer = layer
|
|
isCameraConfigured = true
|
|
|
|
updatePreviewLayout()
|
|
startCaptureSessionIfNeeded()
|
|
}
|
|
|
|
private func updatePreviewLayout() {
|
|
previewLayer?.frame = view.bounds
|
|
}
|
|
|
|
private func startCaptureSessionIfNeeded() {
|
|
guard isCameraConfigured, !captureSession.isRunning else { return }
|
|
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
self?.captureSession.startRunning()
|
|
}
|
|
}
|
|
|
|
private func stopCaptureSession() {
|
|
guard captureSession.isRunning else { return }
|
|
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
self?.captureSession.stopRunning()
|
|
}
|
|
}
|
|
|
|
private func updateMask() {
|
|
maskView.layer.sublayers?.forEach { $0.removeFromSuperlayer() }
|
|
let border = CAShapeLayer()
|
|
border.path = UIBezierPath(roundedRect: maskView.bounds, cornerRadius: 12).cgPath
|
|
border.strokeColor = AppColor.primary.cgColor
|
|
border.fillColor = UIColor.clear.cgColor
|
|
border.lineWidth = 2
|
|
maskView.layer.addSublayer(border)
|
|
}
|
|
|
|
func metadataOutput(
|
|
_ output: AVCaptureMetadataOutput,
|
|
didOutput metadataObjects: [AVMetadataObject],
|
|
from connection: AVCaptureConnection
|
|
) {
|
|
guard !hasHandledResult,
|
|
let object = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
|
|
object.type == .qr,
|
|
let value = object.stringValue,
|
|
!value.isEmpty else { return }
|
|
hasHandledResult = true
|
|
stopCaptureSession()
|
|
onScanResult?(value)
|
|
dismiss(animated: true)
|
|
}
|
|
|
|
@objc private func closeTapped() {
|
|
dismiss(animated: true)
|
|
}
|
|
}
|