Qua bài này, các bạn sẽ có thể thêm tính năng mở camera để quét văn bản (text), mã vạch (barcode), ... vào ứng dụng của mình một cách dễ dàng.
Giới thiệu DataScannerViewController
Từ iOS 16, Apple giới thiệu DataScannerViewController - một api mạnh mẽ và dễ dàng tích hợp chức năng scanning sử dụng chính camera của device. DataScannerViewController có thể scan được nhiều loại data như chữ và barcode, ... Mặc dù rất hữu ích, nhưng tài liệu hoặc example về DataScannerViewController tương đối ít, điều này có thể khiến việc tích hợp vào ứng dụng của bạn gặp thách thức. Qua bài viết này, bạn có thể nhanh chóng học cách sử dụng để thêm các tính năng sáng tạo, tiện nghi vào ứng dụng của mình.
DataScannerViewController kế thừa từ UIViewController, có thể override các phương thức sau để tùy chỉnh thêm
Tương tự như tính năng Live Text trong ứng dụng Camera. API này cung cấp nhiều tính năng hữu ích như:
- Hướng dẫn người dùng: hiển thị các chỉ dẫn trực quan để người dùng biết họ đang quét nội dung gì.
- Highlight: văn bản hoặc mã QR được nhận diện sẽ được đánh dấu trực tiếp trên màn hình để người dùng dễ nhận biết.
- Pinch-to-Zoom: tính năng phóng to và thu nhỏ, giúp việc quét chính xác hơn khi cần.
Cách sử dụng DataScannerViewController
Yêu cầu quyền truy cập camera (bắt buộc)
Bạn cần yêu cầu quyền sử dụng camera bằng cách thêm khóa NSCameraUsageDescription
vào file Info.plist
của project. Khóa này cung cấp lý do sử dụng camera, được hiển thị cho người dùng lần đầu tiên khi ứng dụng yêu cầu quyền truy cập.
<key>NSCameraUsageDescription</key>
<string>Chúng tôi cần quyền truy cập vào camera để quét mã QR và văn bản.</string>
Kiểm tra tính khả dụng
Trước khi hiển thị DataScannerViewController, hãy đảm bảo rằng thiết bị hỗ trợ và nó có thể sử dụng được. Sử dụng thuộc tính isSupported và isAvailable để kiểm tra:
if DataScannerViewController.isSupported && DataScannerViewController.isAvailable { // Tiến hành khởi tạo DataScannerViewController
}
Tạo DataScannerViewController
Khởi tạo một đối tượng DataScannerViewController. API này cho phép sử dụng camera của thiết bị để quét các loại dữ liệu như văn bản, mã vạch hoặc mã QR. Bạn có thể tùy chỉnh các loại dữ liệu muốn quét.
var scannerViewController: DataScannerViewController = DataScannerViewController( recognizedDataTypes: [.text(), .barcode()], qualityLevel: .accurate, recognizesMultipleItems: false, isHighFrameRateTrackingEnabled: false, isHighlightingEnabled: true )
Các loại data được quét như Barcode/QRCode được liệt kê ở đây: https://developer.apple.com/documentation/vision/vnbarcodesymbology
Implement các phương thức delegate
Để xử lý dữ liệu quét được, cần triển khai các phương thức của DataScannerViewControllerDelegate. Điều này cho phép bạn xử lý các mục được nhận diện bởi máy quét, chẳng hạn như văn bản hoặc mã QR. Ví dụ, khi người dùng nhấn vào một mục được nhận diện, ứng dụng có thể kích hoạt các hành động cụ thể (ví dụ: mở URL, gọi số điện thoại).
func dataScanner(_ scanner: DataScannerViewController, didTapOn item: RecognizedItem) { switch item { case .barcode(let barcode): if let payload = barcode.payloadStringValue { // Xử lý mã QR hoặc mã vạch openURL(payload) } case .text(let text): // Xử lý văn bản được nhận diện print("Văn bản nhận diện: \(text.transcript)") default: break }
}
Hiển thị:
Cuối cùng, hiển thị DataScannerViewController để cho phép người dùng quét:
present(scannerViewController, animated: true)
Sử dụng trong SwiftUI
Mặc dù tiếp cận DataScannerViewController trong UIKit tương đối đơn giản, đối với dự án sử dụng SwiftUI, cần cách tiếp cận khác đi 1 chút, cùng theo dõi ví dụ dưới đây nhé:
import SwiftUI
import VisionKit @MainActor // đảm bảo mọi update trên UI đều được thực hiện ở Mainthread
// Sử dụng UIViewControllerRepresentable để tích hợp một view controller từ UIKit (DataScannerViewController) vào SwiftUI
struct DocumentScannerView: UIViewControllerRepresentable { // khởi tạo sanning view var scannerViewController: DataScannerViewController = DataScannerViewController( recognizedDataTypes: [.text(), .barcode()], qualityLevel: .accurate, recognizesMultipleItems: false, isHighFrameRateTrackingEnabled: false, isHighlightingEnabled: true ) func makeUIViewController(context: Context) -> DataScannerViewController { scannerViewController.delegate = context.coordinator // Add a button to start scanning let scanButton = UIButton(type: .system) scanButton.backgroundColor = UIColor.systemBlue scanButton.setTitle("Start Scan", for: .normal) scanButton.setTitleColor(UIColor.white, for: .normal) scanButton.addTarget(context.coordinator, action: #selector(Coordinator.startScanning(_:)), for: .touchUpInside) scannerViewController.view.addSubview(scanButton) // Set up button constraints scanButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ scanButton.centerXAnchor.constraint(equalTo: scannerViewController.view.centerXAnchor), scanButton.bottomAnchor.constraint(equalTo: scannerViewController.view.safeAreaLayoutGuide.bottomAnchor, constant: -20) ]) return scannerViewController } // Tạo một đối tượng Coordinator để xử lý các sự kiện và tương tác giữa SwiftUI và UIKit. // Coordinator giúp giao tiếp giữa DataScannerViewController và SwiftUI. func makeCoordinator() -> Coordinator { return Coordinator(self) } // Class quản lý các tương tác giữa DataScannerViewController và SwiftUI. class Coordinator: NSObject, DataScannerViewControllerDelegate { var parent: DocumentScannerView var roundBoxMappings: [UUID: UIView] = [:] init(_ parent: DocumentScannerView) { self.parent = parent } // DataScannerViewControllerDelegate - methods starts here func dataScanner(_ dataScanner: DataScannerViewController, didAdd addedItems: [RecognizedItem], allItems: [RecognizedItem]) { //ToDo } func dataScanner(_ dataScanner: DataScannerViewController, didRemove removedItems: [RecognizedItem], allItems: [RecognizedItem]) { //ToDo } func dataScanner(_ dataScanner: DataScannerViewController, didUpdate updatedItems: [RecognizedItem], allItems: [RecognizedItem]) { //ToDo } func dataScanner(_ dataScanner: DataScannerViewController, didTapOn item: RecognizedItem) { //ToDo } // DataScannerViewControllerDelegate - methods ends here // Phương thức này bắt đầu quá trình quét khi người dùng nhấn vào nút "Start Scan". Nó gọi startScanning trên scannerViewController. @objc func startScanning(_ sender: UIButton) { try? parent.scannerViewController.startScanning() } }
}
Customize Overlay view
DataScannerViewController
API cung cấp quyền truy cập vào overlayContainerView
, cho phép các nhà phát triển tùy chỉnh giao diện bằng cách thêm các lớp phủ lên trên các văn bản hoặc mã vạch đã được nhận diện. Ví dụ, bạn có thể tạo các khung bao quanh hoặc thêm thông tin bổ sung về các đối tượng đã được nhận diện.
scannerViewController.overlayContainerView.addSubview(UIView())
Ngoài overlayContainerView
, DataScannerViewController
còn cung cấp nhiều thuộc tính và phương thức có thể tùy chỉnh khác. Bạn có thể thay đổi giao diện của văn bản hướng dẫn xuất hiện trên các văn bản hoặc mã vạch đã được nhận diện, và tùy chỉnh màu giao diện của máy quét. Thêm vào đó, bạn có thể thêm các view tùy chỉnh để cung cấp phản hồi chi tiết hơn cho người dùng, như làm nổi bật các phần cụ thể của văn bản hoặc mã vạch đã quét.
DataScannerViewController cũng cho phép bạn điều chỉnh hành vi quét tùy theo trường hợp sử dụng của bạn. Bạn có thể chỉ định loại dữ liệu nhận diện , chất lượng quét, có nhận diện nhiều đối tượng cùng lúc hay không, và có bật theo dõi tốc độ khung hình cao hay không.
static let textDataType: DataScannerViewController.RecognizedDataType = .text( languages: [ "en-US", "ja_JP" ] )
var scannerViewController: DataScannerViewController = DataScannerViewController( recognizedDataTypes: [DocumentScannerView.textDataType, .barcode()], qualityLevel: .accurate, recognizesMultipleItems: false, isHighFrameRateTrackingEnabled: false, isHighlightingEnabled: false
)
Cùng xem code hoàn chỉnh với hàm processItem
để lấy các giá trị được quét từ máy ảnh.
import Foundation
import SwiftUI
import VisionKit @MainActor
struct DocumentScannerView: UIViewControllerRepresentable { static let startScanLabel = "Start Scan" static let stopScanLabel = "Stop Scan" static let textDataType: DataScannerViewController.RecognizedDataType = .text( languages: [ "en-US", "ja_JP" ] ) var scannerViewController: DataScannerViewController = DataScannerViewController( recognizedDataTypes: [DocumentScannerView.textDataType, .barcode()], qualityLevel: .accurate, recognizesMultipleItems: false, isHighFrameRateTrackingEnabled: false, isHighlightingEnabled: false ) func makeUIViewController(context: Context) -> DataScannerViewController { scannerViewController.delegate = context.coordinator // Add a button to start scanning let scanButton = UIButton(type: .system) scanButton.backgroundColor = UIColor.systemBlue scanButton.setTitle(DocumentScannerView.startScanLabel, for: .normal) scanButton.setTitleColor(UIColor.white, for: .normal) var config = UIButton.Configuration.filled() config.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5) scanButton.configuration = config scanButton.addTarget(context.coordinator, action: #selector(Coordinator.startScanning(_:)), for: .touchUpInside) scanButton.layer.cornerRadius = 5.0 scannerViewController.view.addSubview(scanButton) // Set up button constraints scanButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ scanButton.centerXAnchor.constraint(equalTo: scannerViewController.view.centerXAnchor), scanButton.bottomAnchor.constraint(equalTo: scannerViewController.view.safeAreaLayoutGuide.bottomAnchor, constant: -20) ]) return scannerViewController } func updateUIViewController(_ uiViewController: DataScannerViewController, context: Context) { // Update any view controller settings here } func makeCoordinator() -> Coordinator { return Coordinator(self) } class Coordinator: NSObject, DataScannerViewControllerDelegate { var parent: DocumentScannerView var roundBoxMappings: [UUID: UIView] = [:] init(_ parent: DocumentScannerView) { self.parent = parent } func dataScanner(_ dataScanner: DataScannerViewController, didAdd addedItems: [RecognizedItem], allItems: [RecognizedItem]) { processAddedItems(items: addedItems) } func dataScanner(_ dataScanner: DataScannerViewController, didRemove removedItems: [RecognizedItem], allItems: [RecognizedItem]) { processRemovedItems(items: removedItems) } func dataScanner(_ dataScanner: DataScannerViewController, didUpdate updatedItems: [RecognizedItem], allItems: [RecognizedItem]) { processUpdatedItems(items: updatedItems) } func dataScanner(_ dataScanner: DataScannerViewController, didTapOn item: RecognizedItem) { processItem(item: item) } func processAddedItems(items: [RecognizedItem]) { for item in items { processItem(item: item) } } func processRemovedItems(items: [RecognizedItem]) { for item in items { removeRoundBoxFromItem(item: item) } } func processUpdatedItems(items: [RecognizedItem]) { for item in items { updateRoundBoxToItem(item: item) } } func processItem(item: RecognizedItem) { switch item { case .text(let text): print("Text Observation - \(text.observation)") print("Text transcript - \(text.transcript)") let frame = getRoundBoxFrame(item: item) // Adding the round box overlay to detected text addRoundBoxToItem(frame: frame, text: text.transcript, item: item) case .barcode: break @unknown default: print("Should not happen") } } func addRoundBoxToItem(frame: CGRect, text: String, item: RecognizedItem) { //let roundedRectView = RoundRectView(frame: frame) let roundedRectView = RoundedRectLabel(frame: frame) roundedRectView.setText(text: text) parent.scannerViewController.overlayContainerView.addSubview(roundedRectView) roundBoxMappings[item.id] = roundedRectView } func removeRoundBoxFromItem(item: RecognizedItem) { if let roundBoxView = roundBoxMappings[item.id] { if roundBoxView.superview != nil { roundBoxView.removeFromSuperview() roundBoxMappings.removeValue(forKey: item.id) } } } func updateRoundBoxToItem(item: RecognizedItem) { if let roundBoxView = roundBoxMappings[item.id] { if roundBoxView.superview != nil { let frame = getRoundBoxFrame(item: item) roundBoxView.frame = frame } } } func getRoundBoxFrame(item: RecognizedItem) -> CGRect { let frame = CGRect( x: item.bounds.topLeft.x, y: item.bounds.topLeft.y, width: abs(item.bounds.topRight.x - item.bounds.topLeft.x) + 15, height: abs(item.bounds.topLeft.y - item.bounds.bottomLeft.y) + 15 ) return frame } // Add this method to start scanning @objc func startScanning(_ sender: UIButton) { if sender.title(for: .normal) == startScanLabel { try? parent.scannerViewController.startScanning() sender.setTitle(stopScanLabel, for: .normal) } else { parent.scannerViewController.stopScanning() sender.setTitle(startScanLabel, for: .normal) } } } class RoundedRectLabel: UIView { let label = UILabel() let cornerRadius: CGFloat = 5.0 let padding: CGFloat = 5 var text: String = "" override init(frame: CGRect) { super.init(frame: frame) // Configure the label label.textColor = .white label.font = UIFont.systemFont(ofSize: 10) label.textAlignment = .left label.numberOfLines = 0 label.text = text label.translatesAutoresizingMaskIntoConstraints = false addSubview(label) // Add constraints for the label NSLayoutConstraint.activate([ label.topAnchor.constraint(equalTo: topAnchor, constant: padding), label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: padding), label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -padding), label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -padding) ]) // Configure the background backgroundColor = .magenta layer.cornerRadius = cornerRadius layer.opacity = 0.75 } func setText(text: String) { label.text = text setNeedsDisplay() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
}
Sử dụng trong SwiftUI
Rất đơn giản như sau:
import Foundation
import SwiftUI struct ContentView: View { var body: some View { NavigationView { VStack { Text("Scan a document") .font(.title) .padding() DocumentScannerView() .navigationBarTitle("") .navigationBarHidden(true) } } }
}
Sau đó thì tận hưởng thành quả của bạn thôi