Quét văn bản và mã vạch sử dụng VisionKit trong SwiftUI - iOS
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
All rights reserved