// // QRScannerVC.swift // Sirani // // Created by Anhar Solehudin on 10/04/19. // Copyright © 2019 Anhar Solehudin. All rights reserved. // import UIKit import AVFoundation class QRScannerVC: UIViewController { // Outlets @IBOutlet weak var scannerView: UIView! @IBOutlet weak var overlayTop: UIView! @IBOutlet weak var overlayLeft: UIView! @IBOutlet weak var overlayRight: UIView! @IBOutlet weak var overlayBottom: UIView! @IBOutlet weak var btnCloseScanner: UIButton! @IBOutlet weak var resultLayout: UIView! @IBOutlet weak var contentLayout: UIView! @IBOutlet weak var lblNoSertifikat: UILabel! @IBOutlet weak var lblTanggalSertifikat: UILabel! @IBOutlet weak var lblPlgId: UILabel! @IBOutlet weak var lblPerusahaan: UILabel! @IBOutlet weak var lblNamaPerangkat: UILabel! @IBOutlet weak var lblMerk: UILabel! @IBOutlet weak var lblModel: UILabel! @IBOutlet weak var lblImei: UILabel! @IBOutlet weak var lblPabrikan: UILabel! @IBOutlet weak var lblNegara: UILabel! @IBOutlet weak var lblAcuan: UILabel! @IBOutlet weak var lblFrek: UILabel! @IBOutlet weak var lblPower: UILabel! @IBOutlet weak var progressLoadingLayout: UIView! @IBOutlet weak var progressLoading: UIActivityIndicatorView! private var captureSession = AVCaptureSession() private var videoPreviewLayer: AVCaptureVideoPreviewLayer? private var qrCodeFrameView: UIView? private var dataCaptured = false private let supportedCodeTypes = [ AVMetadataObject.ObjectType.upce, AVMetadataObject.ObjectType.code39, AVMetadataObject.ObjectType.code39Mod43, AVMetadataObject.ObjectType.code93, AVMetadataObject.ObjectType.code128, AVMetadataObject.ObjectType.ean8, AVMetadataObject.ObjectType.ean13, AVMetadataObject.ObjectType.aztec, AVMetadataObject.ObjectType.pdf417, AVMetadataObject.ObjectType.itf14, AVMetadataObject.ObjectType.dataMatrix, AVMetadataObject.ObjectType.interleaved2of5, AVMetadataObject.ObjectType.qr ] // Initialize private var presenter: QRScannerPresenter = QRScannerPresenter(siraniApiServices: SiraniApiServices()) // Constant Values private let alertNoConnectionTitle = "Informasi" private let alertNoConnectionMsg = "Sepertinya Anda sedang tidak tersambung pada koneksi internet" private let alertNoConnectionActionYes = "OK" // variables private var loading = false var kode : Int? var isSertifikatNew = false var isSertifikatPublic = false var isLabel = false // View Lifecycles override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent } override func viewWillAppear(_ animated: Bool) { AppUtility.lockOrientation(.portrait) if let kode = self.kode { onQrDataAvailable(decodedString: "\(kode)") dataCaptured = true } } override func viewDidLoad() { super.viewDidLoad() styleDetailLayout() lblFrek.numberOfLines = 0 lblFrek.lineBreakMode = .byWordWrapping lblFrek.sizeToFit() lblNamaPerangkat.numberOfLines = 0 lblPerusahaan.numberOfLines = 0 lblModel.numberOfLines = 0 lblAcuan.numberOfLines = 0 presenter.attachView(view: self) dataCaptured = false // scanner view // Get the back-facing camera for capturing videos let deviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: AVMediaType.video, position: .back) guard let captureDevice = deviceDiscoverySession.devices.first else { print("Failed to get the camera device") return } do { // Get an instance of the AVCaptureDeviceInput class using the previous device object. let input = try AVCaptureDeviceInput(device: captureDevice) // Set the input device on the capture session. captureSession.addInput(input) // Initialize a AVCaptureMetadataOutput object and set it as the output device to the capture session. let captureMetadataOutput = AVCaptureMetadataOutput() captureSession.addOutput(captureMetadataOutput) // Set delegate and use the default dispatch queue to execute the call back captureMetadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) // captureMetadataOutput.metadataObjectTypes = supportedCodeTypes captureMetadataOutput.metadataObjectTypes = [AVMetadataObject.ObjectType.qr] } catch { // If any error occurs, simply print it out and don't continue any more. print(error) return } // Initialize the video preview layer and add it as a sublayer to the viewPreview view's layer. videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession) videoPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill videoPreviewLayer?.frame = scannerView.layer.bounds scannerView.layer.addSublayer(videoPreviewLayer!) // Start video capture. captureSession.startRunning() // bring scanner controller views scannerView.bringSubviewToFront(overlayTop) scannerView.bringSubviewToFront(overlayLeft) scannerView.bringSubviewToFront(overlayRight) scannerView.bringSubviewToFront(overlayBottom) scannerView.bringSubviewToFront(btnCloseScanner) // Initialize QR Code Frame to highlight the QR code qrCodeFrameView = UIView() if let qrCodeFrameView = qrCodeFrameView { qrCodeFrameView.layer.borderColor = UIColor.green.cgColor qrCodeFrameView.layer.borderWidth = 2 scannerView.addSubview(qrCodeFrameView) scannerView.bringSubviewToFront(qrCodeFrameView) } } override func viewWillDisappear(_ animated: Bool) { AppUtility.lockOrientation(.all) } override func viewDidDisappear(_ animated: Bool) { presenter.detachView() } // private function private func loadData(code: String) { if !loading { if ConnectionHelper.isConnectedToNetwork() { self.presenter.loadData(code: code) } else { showNoConnection() } } } private func onQrDataEmpty() { resultLayout.show() scannerView.hide() DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { self.setEmptyQrCode() } } private func onQrDataAvailable(decodedString: String) { resultLayout.show() scannerView.hide() DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { self.loadData(code: decodedString) } } // Actions @IBAction func onBtnBackTap(_ sender: Any) { if(kode != nil) { if(isSertifikatNew){ performSegue(withIdentifier: "unwindToSertifikatSegueId", sender: self) }else if(isSertifikatPublic){ performSegue(withIdentifier: "unwindToSertifikatPublicSegueId", sender: self) }else if(isLabel){ performSegue(withIdentifier: "unwindToLabelSegueId", sender: self) }else{ performSegue(withIdentifier: "unwindToDaftarSertifikatSegueId", sender: self) } } else { performSegue(withIdentifier: "unwindToScanQrSegueId", sender: self) } } @IBAction func onBtnCloseScannerTap(_ sender: Any) { if(kode != nil) { if(isSertifikatNew){ performSegue(withIdentifier: "unwindToSertifikatSegueId", sender: self) }else if(isSertifikatPublic){ performSegue(withIdentifier: "unwindToSertifikatPublicSegueId", sender: self) }else{ performSegue(withIdentifier: "unwindToDaftarSertifikatSegueId", sender: self) } } else { performSegue(withIdentifier: "unwindToScanQrSegueId", sender: self) } } private func styleDetailLayout() { resultLayout.backgroundColor = UIColor(red: 247/255, green: 249/255, blue: 252/255, alpha: 1.0) // Style contentLayout container contentLayout.backgroundColor = .clear contentLayout.layer.borderWidth = 0 contentLayout.layer.shadowColor = UIColor.clear.cgColor // Highlight Certificate Number & Key Titles lblNoSertifikat.font = UIFont.systemFont(ofSize: 17, weight: .bold) lblNoSertifikat.textColor = UIColor.siraniPrimaryBlue lblTanggalSertifikat.font = UIFont.systemFont(ofSize: 13, weight: .medium) lblTanggalSertifikat.textColor = UIColor.siraniTextSecondary lblPerusahaan.font = UIFont.systemFont(ofSize: 14, weight: .bold) lblPerusahaan.textColor = UIColor.siraniTextPrimary lblPerusahaan.numberOfLines = 0 lblNamaPerangkat.font = UIFont.systemFont(ofSize: 14, weight: .bold) lblNamaPerangkat.textColor = UIColor.siraniTextPrimary lblNamaPerangkat.numberOfLines = 0 lblAcuan.numberOfLines = 0 lblAcuan.lineBreakMode = .byWordWrapping lblFrek.numberOfLines = 0 lblFrek.lineBreakMode = .byWordWrapping let valueLabels: [UILabel?] = [lblPlgId, lblMerk, lblModel, lblImei, lblPabrikan, lblNegara, lblAcuan, lblFrek] for lbl in valueLabels { lbl?.font = UIFont.systemFont(ofSize: 13, weight: .semibold) lbl?.textColor = UIColor.siraniTextPrimary } // Style each section card and inner value containers for section in contentLayout.subviews { section.backgroundColor = .white section.layer.cornerRadius = 16 section.layer.borderWidth = 1 section.layer.borderColor = UIColor(red: 228/255, green: 235/255, blue: 245/255, alpha: 1.0).cgColor section.layer.shadowColor = UIColor.black.cgColor section.layer.shadowOffset = CGSize(width: 0, height: 2) section.layer.shadowRadius = 8 section.layer.shadowOpacity = 0.05 section.layer.masksToBounds = false for subview in section.subviews { if let label = subview as? UILabel { if !valueLabels.compactMap({ $0 }).contains(label), label != lblNoSertifikat, label != lblTanggalSertifikat, label != lblPerusahaan, label != lblNamaPerangkat { label.font = UIFont.systemFont(ofSize: 11, weight: .bold) label.textColor = UIColor(red: 120/255, green: 135/255, blue: 155/255, alpha: 1.0) } } else if !(subview is UILabel) { // Inner value box container subview.backgroundColor = UIColor(red: 248/255, green: 250/255, blue: 254/255, alpha: 1.0) subview.layer.cornerRadius = 10 subview.layer.borderWidth = 1 subview.layer.borderColor = UIColor(red: 235/255, green: 240/255, blue: 248/255, alpha: 1.0).cgColor subview.layer.masksToBounds = true } } } } } extension QRScannerVC: QRScannerViewDelegate { func showNoConnection() { let alertNoConnection = UIAlertController( title: alertNoConnectionTitle, message: alertNoConnectionMsg, preferredStyle: .alert ) alertNoConnection.addAction( UIAlertAction(title: alertNoConnectionActionYes, style: .default, handler: nil) ) self.present(alertNoConnection, animated: true, completion: nil) } func showLoading() { loading = true progressLoading.startAnimating() progressLoadingLayout.show() } func hideLoading() { loading = false progressLoading.stopAnimating() progressLoadingLayout.hide() } func setResulQrCode(resultQrCode: ResultQrCode) { lblNoSertifikat.text = resultQrCode.lic_no.isEmpty ? "-" : resultQrCode.lic_no lblTanggalSertifikat.text = CommonHelper.formatDate(from: resultQrCode.lic_date) lblPlgId.text = resultQrCode.cust_id == 0 ? "-" : String(resultQrCode.cust_id) lblPerusahaan.text = resultQrCode.pmh_name.isEmpty ? "-" : resultQrCode.pmh_name lblNamaPerangkat.text = resultQrCode.eqp_name.isEmpty ? "-" : resultQrCode.eqp_name lblMerk.text = resultQrCode.merk.isEmpty ? "-" : resultQrCode.merk lblModel.text = resultQrCode.model.isEmpty ? "-" : resultQrCode.model let imei = resultQrCode.imei.trimmingCharacters(in: .whitespacesAndNewlines) lblImei.text = (imei.isEmpty || imei == "null") ? "-" : imei let pabrikan = resultQrCode.mfc_name.trimmingCharacters(in: .whitespacesAndNewlines) lblPabrikan.text = (pabrikan.isEmpty || pabrikan == "null") ? "-" : pabrikan let negara = resultQrCode.negara.trimmingCharacters(in: .whitespacesAndNewlines) lblNegara.text = (negara.isEmpty || negara == "null") ? "-" : negara // Format Acuan Regulasi neatly lblAcuan.attributedText = formatAcuanRegulasi(raw: resultQrCode.acuan) // Format Frequency data cleanly into structured sections lblFrek.attributedText = formatStructuredFrequency(raw: resultQrCode.eqp_frek) } private func formatAcuanRegulasi(raw: String) -> NSAttributedString { let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty || trimmed == "null" || trimmed == "-" { return NSAttributedString(string: "-", attributes: [ .font: UIFont.systemFont(ofSize: 13, weight: .regular), .foregroundColor: UIColor.siraniTextSecondary ]) } let items = trimmed.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty } let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineSpacing = 4 paragraphStyle.paragraphSpacing = 6 let result = NSMutableAttributedString() for (index, item) in items.enumerated() { let bulletStr = (index == 0 ? "" : "\n") + "📜 \(item)" let attr = NSAttributedString(string: bulletStr, attributes: [ .font: UIFont.systemFont(ofSize: 12, weight: .semibold), .foregroundColor: UIColor(red: 35/255, green: 45/255, blue: 60/255, alpha: 1.0), .paragraphStyle: paragraphStyle ]) result.append(attr) } return result } private func formatStructuredFrequency(raw: String) -> NSAttributedString { var text = raw if text.contains("<") && text.contains(">") { text = text.replacingOccurrences(of: "", with: "\n", options: .regularExpression) text = text.replacingOccurrences(of: "", with: "\n", options: .regularExpression) text = text.replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression) } text = text.trimmingCharacters(in: .whitespacesAndNewlines) if text.isEmpty || text == "null" || text == "-" { return NSAttributedString(string: "-", attributes: [ .font: UIFont.systemFont(ofSize: 13, weight: .regular), .foregroundColor: UIColor.siraniTextSecondary ]) } // Insert clean section breaks before radio band names if it is a run-on block let bandBreakPattern = "(?i)(?<=[^\\n])\\s+(?=(?:Bluetooth|WLAN|NFC|RFID|GSM|WCDMA|LTE|5G|Radar|UWB|SRD|LoRa|Zigbee|DVB)\\b)" text = text.replacingOccurrences(of: bandBreakPattern, with: "\n\n", options: .regularExpression) let sections = text.components(separatedBy: "\n\n").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty } let fullAttr = NSMutableAttributedString() for (sIdx, sectionText) in sections.enumerated() { if sIdx > 0 { fullAttr.append(NSAttributedString(string: "\n\n")) } // Format each section // Check if it matches: Protocol + Frekuensi + Daya Pancar var bandTitle = "" var frekVal = "" var powerVal = "" // Regex to extract Protocol, Frekuensi range, and Daya Pancar let pattern = "^(Bluetooth|WLAN[^\nFrekuensi]*|[A-Z0-9- ]+?)?\\s*(?:Frekuensi\\s*:?\\s*)?([0-9.]+\\s*[kMG]Hz\\s*-\\s*[0-9.]+\\s*[kMG]Hz)?\\s*(?:(?:Bluetooth|WLAN[^ ]*|[A-Z0-9]+)?\\s*Daya\\s*Pancar\\s*:?\\s*)?([<>]?\\s*[0-9.]+\\s*m?Watt)?" if let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]), let match = regex.firstMatch(in: sectionText, options: [], range: NSRange(location: 0, length: sectionText.utf16.count)) { if let r1 = Range(match.range(at: 1), in: sectionText) { bandTitle = String(sectionText[r1]).trimmingCharacters(in: .whitespacesAndNewlines) } if let r2 = Range(match.range(at: 2), in: sectionText) { frekVal = String(sectionText[r2]).trimmingCharacters(in: .whitespacesAndNewlines) } if let r3 = Range(match.range(at: 3), in: sectionText) { powerVal = String(sectionText[r3]).trimmingCharacters(in: .whitespacesAndNewlines) } } if !bandTitle.isEmpty && (!frekVal.isEmpty || !powerVal.isEmpty) { // Render as structured radio card let icon = bandTitle.lowercased().contains("wlan") ? "📶" : (bandTitle.lowercased().contains("bluetooth") ? "📻" : "📡") let titleAttr = NSAttributedString(string: "\(icon) \(bandTitle)\n", attributes: [ .font: UIFont.systemFont(ofSize: 13, weight: .bold), .foregroundColor: UIColor.siraniPrimaryBlue ]) fullAttr.append(titleAttr) let detailPara = NSMutableParagraphStyle() detailPara.lineSpacing = 3 detailPara.paragraphSpacing = 2 if !frekVal.isEmpty { let frekAttr = NSAttributedString(string: " • Frekuensi: \(frekVal)\n", attributes: [ .font: UIFont.systemFont(ofSize: 12, weight: .medium), .foregroundColor: UIColor(red: 40/255, green: 50/255, blue: 65/255, alpha: 1.0), .paragraphStyle: detailPara ]) fullAttr.append(frekAttr) } if !powerVal.isEmpty { let powerAttr = NSAttributedString(string: " • Daya Pancar: \(powerVal)", attributes: [ .font: UIFont.systemFont(ofSize: 12, weight: .medium), .foregroundColor: UIColor(red: 90/255, green: 105/255, blue: 125/255, alpha: 1.0), .paragraphStyle: detailPara ]) fullAttr.append(powerAttr) } } else { // Fallback: render each line with a bullet let lines = sectionText.components(separatedBy: "\n").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty } let genericPara = NSMutableParagraphStyle() genericPara.lineSpacing = 3 for (lIdx, line) in lines.enumerated() { if lIdx > 0 { fullAttr.append(NSAttributedString(string: "\n")) } let bulletLine = "• \(line)" let lineAttr = NSAttributedString(string: bulletLine, attributes: [ .font: UIFont.systemFont(ofSize: 12, weight: .medium), .foregroundColor: UIColor(red: 40/255, green: 50/255, blue: 65/255, alpha: 1.0), .paragraphStyle: genericPara ]) fullAttr.append(lineAttr) } } } return fullAttr } func setEmptyQrCode() { let alertNoConnection = UIAlertController( title: "Informasi", message: "Data tidak ditemukan", preferredStyle: .alert ) alertNoConnection.addAction( UIAlertAction(title: "OK", style: .default, handler: nil) ) self.present(alertNoConnection, animated: true, completion: nil) } } extension QRScannerVC: AVCaptureMetadataOutputObjectsDelegate { func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { if !dataCaptured { print("TEST SCAN") print(metadataObjects.count) print(metadataObjects) // Check if the metadataObjects array is not nil and it contains at least one object. if metadataObjects.count == 0 { onQrDataEmpty() dataCaptured = true return } // Get the metadata object. let metadataObj = metadataObjects[0] as! AVMetadataMachineReadableCodeObject if supportedCodeTypes.contains(metadataObj.type) { // If the found metadata is equal to the QR code metadata (or barcode) then update the status label's text and set the bounds let barCodeObject = videoPreviewLayer?.transformedMetadataObject(for: metadataObj) qrCodeFrameView?.frame = barCodeObject!.bounds if metadataObj.stringValue != nil { if let url = URL(string: metadataObj.stringValue ?? "") { let lic_id = url.lastPathComponent onQrDataAvailable(decodedString: lic_id) dataCaptured = true } } } } } }