apple/SwiftUI & Combine
[SwiftUI] QRcodeReader
lgvv
2022. 5. 23. 12:00
QRcodeReader
✅ QRcodeReader를 알아보자.
코드는 어렵지 않다.
눈여겨 볼 점은 하나의 파일에서 여러개의 뷰를 생성하는 것을 볼 수 있겠다
🟠 아래는 오픈소스 !_!
https://github.com/twostraws/CodeScanner
✅ QRCodeScannerExampleView
import SwiftUI
struct QRCodeScannerExampleView: View {
@State var isPresentingScanner = false // sheet 띄워주기 위한 변수
@State var scannedCode: String?
var body: some View {
ZStack{
if self.scannedCode != nil {
MyWebview(urlToLoad: self.scannedCode!)
} else {
MyWebview(urlToLoad: "https://www.naver.com")
}
VStack{
Spacer()
Button(action: {
self.isPresentingScanner = true
}){
Text("로또번호확인")
.font(.system(size: 20))
.fontWeight(.bold)
.padding()
.background(Color.yellow)
.cornerRadius(12)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(lineWidth: 5)
)
}
.sheet(isPresented: $isPresentingScanner) {
self.scannerSheet
}
Spacer().frame(height: 30) // spacer에도 frame지정 가능
}
}
}
// 하나의 파일에서 view를 사용할 때의 구조
var scannerSheet: some View {
ZStack {
CodeScannerView(
codeTypes: [.qr],
completion: { result in
if case let .success(code) = result {
self.scannedCode = code
self.isPresentingScanner = false
}
}
)
QRCodeGuideLineView() // ZStack으로 가이드라인 뷰를 만들기
}
}
}
✅ QRCodeGuideLineView
import SwiftUI
struct QRCodeGuideLineView: View {
var body: some View {
GeometryReader{ geometryProxy in
RoundedRectangle(cornerRadius: 20)
// stroke를 통해 내부를 비게 만듭니다, dash: 점선으로 만듭니다.
.stroke(style: StrokeStyle(lineWidth: 10, dash: [11]))
.frame(
width: geometryProxy.size.width / 2,
height: geometryProxy.size.height / 3
)
// geometry 프록시로 포지션을 주면 된다.
.position(
x: geometryProxy.size.width / 2,
y: geometryProxy.size.height / 2
)
.foregroundColor(Color.yellow)
}
}
}
1. 스캐너를 찍기전에 기본 url을 네이버로 세팅
2. 스캐너 가이드라인과 카메라로 찍는다.
3. 인식되면 해당 url로 이동
🟠 주의사항
카메라를 사용하려면 info.plist에 해당 권한을 넣어주어야 한다.
✅ 오픈소스에서 제공하는 코드!
//
// CodeScannerView.swift
//
// Created by Paul Hudson on 10/12/2019.
// Copyright © 2019 Paul Hudson. All rights reserved.
//
import AVFoundation
import SwiftUI
/// A SwiftUI view that is able to scan barcodes, QR codes, and more, and send back what was found.
/// To use, set `codeTypes` to be an array of things to scan for, e.g. `[.qr]`, and set `completion` to
/// a closure that will be called when scanning has finished. This will be sent the string that was detected or a `ScanError`.
/// For testing inside the simulator, set the `simulatedData` property to some test data you want to send back.
public struct CodeScannerView: UIViewControllerRepresentable {
public enum ScanError: Error {
case badInput, badOutput
}
public class ScannerCoordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
var parent: CodeScannerView
var codeFound = false
init(parent: CodeScannerView) {
self.parent = parent
}
public func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
if let metadataObject = metadataObjects.first {
guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
guard let stringValue = readableObject.stringValue else { return }
guard codeFound == false else { return }
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
found(code: stringValue)
// make sure we only trigger scans once per use
codeFound = true
}
}
func found(code: String) {
parent.completion(.success(code))
}
func didFail(reason: ScanError) {
parent.completion(.failure(reason))
}
}
#if targetEnvironment(simulator)
public class ScannerViewController: UIViewController,UIImagePickerControllerDelegate,UINavigationControllerDelegate{
var delegate: ScannerCoordinator?
override public func loadView() {
view = UIView()
view.isUserInteractionEnabled = true
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.text = "에뮬레이터로 돌리셨군요 - 실제 기기로 테스트 부탁바랍니다."
label.textAlignment = .center
// let button = UIButton()
// button.translatesAutoresizingMaskIntoConstraints = false
// button.setTitle("Or tap here to select a custom image", for: .normal)
// button.setTitleColor(UIColor.systemBlue, for: .normal)
// button.setTitleColor(UIColor.gray, for: .highlighted)
// button.addTarget(self, action: #selector(self.openGallery), for: .touchUpInside)
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 50
stackView.addArrangedSubview(label)
// stackView.addArrangedSubview(button)
view.addSubview(stackView)
NSLayoutConstraint.activate([
// button.heightAnchor.constraint(equalToConstant: 50),
stackView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let simulatedData = delegate?.parent.simulatedData else {
print("Simulated Data Not Provided!")
return
}
delegate?.found(code: simulatedData)
}
@objc func openGallery(_ sender: UIButton){
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
self.present(imagePicker, animated: true, completion: nil)
}
public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]){
if let qrcodeImg = info[.originalImage] as? UIImage {
let detector:CIDetector=CIDetector(ofType: CIDetectorTypeQRCode, context: nil, options: [CIDetectorAccuracy:CIDetectorAccuracyHigh])!
let ciImage:CIImage=CIImage(image:qrcodeImg)!
var qrCodeLink=""
let features=detector.features(in: ciImage)
for feature in features as! [CIQRCodeFeature] {
qrCodeLink += feature.messageString!
}
if qrCodeLink=="" {
delegate?.didFail(reason: .badOutput)
}else{
delegate?.found(code: qrCodeLink)
}
}
else{
print("Something went wrong")
}
self.dismiss(animated: true, completion: nil)
}
}
#else
public class ScannerViewController: UIViewController {
var captureSession: AVCaptureSession!
var previewLayer: AVCaptureVideoPreviewLayer!
var delegate: ScannerCoordinator?
override public func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,
selector: #selector(updateOrientation),
name: Notification.Name("UIDeviceOrientationDidChangeNotification"),
object: nil)
view.backgroundColor = UIColor.black
captureSession = AVCaptureSession()
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { return }
let videoInput: AVCaptureDeviceInput
do {
videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
} catch {
return
}
if (captureSession.canAddInput(videoInput)) {
captureSession.addInput(videoInput)
} else {
delegate?.didFail(reason: .badInput)
return
}
let metadataOutput = AVCaptureMetadataOutput()
if (captureSession.canAddOutput(metadataOutput)) {
captureSession.addOutput(metadataOutput)
metadataOutput.setMetadataObjectsDelegate(delegate, queue: DispatchQueue.main)
metadataOutput.metadataObjectTypes = delegate?.parent.codeTypes
} else {
delegate?.didFail(reason: .badOutput)
return
}
}
override public func viewWillLayoutSubviews() {
previewLayer?.frame = view.layer.bounds
}
@objc func updateOrientation() {
guard let orientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation else { return }
guard let connection = captureSession.connections.last, connection.isVideoOrientationSupported else { return }
connection.videoOrientation = AVCaptureVideoOrientation(rawValue: orientation.rawValue) ?? .portrait
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer.frame = view.layer.bounds
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
updateOrientation()
captureSession.startRunning()
}
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if (captureSession?.isRunning == false) {
captureSession.startRunning()
}
}
override public func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if (captureSession?.isRunning == true) {
captureSession.stopRunning()
}
NotificationCenter.default.removeObserver(self)
}
override public var prefersStatusBarHidden: Bool {
return true
}
override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .all
}
}
#endif
public let codeTypes: [AVMetadataObject.ObjectType]
public var simulatedData = ""
public var completion: (Result<String, ScanError>) -> Void
public init(codeTypes: [AVMetadataObject.ObjectType], simulatedData: String = "", completion: @escaping (Result<String, ScanError>) -> Void) {
self.codeTypes = codeTypes
self.simulatedData = simulatedData
self.completion = completion
}
public func makeCoordinator() -> ScannerCoordinator {
return ScannerCoordinator(parent: self)
}
public func makeUIViewController(context: Context) -> ScannerViewController {
let viewController = ScannerViewController()
viewController.delegate = context.coordinator
return viewController
}
public func updateUIViewController(_ uiViewController: ScannerViewController, context: Context) {
}
}
struct CodeScannerView_Previews: PreviewProvider {
static var previews: some View {
CodeScannerView(codeTypes: [.qr]) { result in
// do nothing
}
}
}