- vừa được xem lúc

Quét văn bản và mã vạch sử dụng VisionKit trong SwiftUI - iOS

0 0 3

Người đăng: Nguyễn Quang

Theo Viblo Asia

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

image.png 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.plistcủ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

image.pngimage.png

Bình luận

Bài viết tương tự

- vừa được xem lúc

Học Flutter từ cơ bản đến nâng cao. Phần 1: Làm quen cô nàng Flutter

Lời mở đầu. Gần đây, Flutter nổi lên và được Google PR như một xu thế của lập trình di động vậy.

0 0 281

- vừa được xem lúc

Học Flutter từ cơ bản đến nâng cao. Phần 3: Lột trần cô nàng Flutter, BuildContext là gì?

Lời mở đầu. Màn làm quen cô nàng FLutter ở Phần 1 đã gieo rắc vào đầu chúng ta quá nhiều điều bí ẩn về nàng Flutter.

0 0 206

- vừa được xem lúc

Swift: Tạo custom phép toán tử (Operator) của riêng bạn!

Swift cho phép bạn tạo các toán tử có thể tùy chỉnh của riêng bạn. Điều này đặc biệt hữu ích khi bạn xử lý các loại dữ liệu của riêng mình. Operator Types in Swift. Có năm loại toán tử chính trong Swift.

0 0 56

- vừa được xem lúc

Chương 6 Protocol oriented programming.

Cuốn sách này là về lập trình hướng protocol. Khi Apple thông báo swift 2 ở WWDC 2015.

0 0 46

- vừa được xem lúc

Ví dụ về UIActivityViewController

Trên iOS, UIActivityViewController cung cấp giao diện thống nhất để người dùng chia sẻ và thực hiện các hành động trên văn bản, hình ảnh, URL và các mục khác trong ứng dụng. let string = "Hello, world!". let url = URL(string: "https://nshipster.com").

0 0 58

- vừa được xem lúc

Quản lý self và cancellable trong Combine.

. . Công việc quản lý bộ nhớ memory management thường trở nên phức tạp khi chúng ta thực hiện các tác vụ bất đồng bộ asynchronous vì chúng ta thường phải lưu giữ một số object nằm ngoài scope mà object được define trong khi vẫn phải đảm bảo được việc giải phóng object đó được thực hiện đúng quy trìn

0 0 41