You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift UITextField一键唤起系统Scan Text功能方案问询

实现点击UITextField直接唤起系统Scan Text功能的方案

核心思路

要跳过繁琐的五步操作直接唤起系统Scan Text,有两种主要方向:要么直接触发系统内置的Scan Text动作,要么基于系统框架自定义接近原生体验的扫描界面。


方案一:直接触发系统Scan Text动作(推荐,iOS 15+)

系统的Scan Text是文本输入框菜单的一部分,我们可以在UITextField被点击时,自动触发这个内置动作,无需弹出键盘。

代码实现(Swift)

import UIKit

class VINInputTextField: UITextField {
    override func awakeFromNib() {
        super.awakeFromNib()
        // 替换默认键盘为空视图,避免弹出键盘
        inputView = UIView(frame: .zero)
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        // 查找系统的Scan Text动作并触发
        triggerSystemScanTextAction()
    }
    
    private func triggerSystemScanTextAction() {
        guard let contextMenu = textInputContextMenu?.menu else {
            //  fallback:系统不支持时弹出键盘
            becomeFirstResponder()
            return
        }
        
        // 通过本地化字符串匹配动作(避免硬编码标题导致多语言失效)
        let scanTextTitle = NSLocalizedString("UIMenuItemScanText", comment: "")
        for element in contextMenu.children {
            guard let action = element as? UIAction, action.title == scanTextTitle else {
                continue
            }
            action.perform(withSender: self)
            return
        }
        
        // 未找到动作时的 fallback
        becomeFirstResponder()
    }
}

注意事项

  • 该方案依赖系统文本菜单的结构,需测试不同iOS版本的兼容性。
  • 使用系统本地化字符串UIMenuItemScanText确保多语言环境下能正确匹配动作。

方案二:自定义接近原生的扫描界面(兼容iOS 13+)

如果无法直接调用系统Scan Text,可基于VisionKit和Vision框架自定义界面,还原原生体验:

实现步骤

  1. 创建扫描控制器:用VNDocumentCameraViewController获取相机画面,自定义导航栏按钮替换默认快门。
  2. 实时文本识别:用Vision框架的VNRecognizeTextRequest实时识别VIN码,添加捕获框预览。
  3. 关联UITextField:点击输入框时直接弹出自定义扫描控制器,识别完成后回填文本。

核心代码片段

import UIKit
import VisionKit
import Vision

class VINScanController: UIViewController, VNDocumentCameraViewControllerDelegate {
    private var onCompletion: ((String?) -> Void)?
    
    static func show(from parent: UIViewController, completion: @escaping (String?) -> Void) {
        let controller = VINScanController()
        controller.onCompletion = completion
        
        let docCamera = VNDocumentCameraViewController()
        docCamera.delegate = controller
        // 自定义导航栏按钮
        docCamera.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: controller, action: #selector(cancelScan))
        docCamera.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Insert", style: .done, target: controller, action: #selector(insertScanResult))
        
        parent.present(UINavigationController(rootViewController: docCamera), animated: true)
    }
    
    @objc private func cancelScan() {
        dismiss(animated: true) { self.onCompletion?(nil) }
    }
    
    @objc private func insertScanResult() {
        // 这里需要结合实时识别获取当前识别的VIN码
        // 简化示例:假设已获取到识别文本
        dismiss(animated: true) { self.onCompletion?("识别到的VIN码") }
    }
    
    // MARK: - VNDocumentCameraDelegate
    func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) {
        // 处理拍照后的识别逻辑
        let image = scan.imageOfPage(at: 0)
        let request = VNRecognizeTextRequest { [weak self] _, error in
            guard let observations = request.results as? [VNRecognizedTextObservation], error == nil else {
                self?.onCompletion?(nil)
                return
            }
            let text = observations.compactMap { $0.topCandidates(1).first?.string }.joined()
            // 过滤17位VIN码
            let vinRegex = try! NSRegularExpression(pattern: "^[A-HJ-NPR-Z0-9]{17}$")
            if let match = vinRegex.firstMatch(in: text, range: NSRange(text.startIndex..., in: text)) {
                let vin = String(text[Range(match.range, in: text)!])
                self?.onCompletion?(vin)
            } else {
                self?.onCompletion?(text)
            }
        }
        request.recognitionLevel = .accurate
        try! VNImageRequestHandler(cgImage: image.cgImage!).perform([request])
    }
}

// 在UITextField子类中调用
class VINInputTextField: UITextField, UITextFieldDelegate {
    override func awakeFromNib() {
        super.awakeFromNib()
        delegate = self
    }
    
    func textFieldDidBeginEditing(_ textField: UITextField) {
        textField.resignFirstResponder()
        VINScanController.show(from: window!.rootViewController!) { [weak self] vin in
            self?.text = vin
        }
    }
}

补充:添加实时捕获框

要在预览中显示识别文本的捕获框,需要结合AVCaptureSession获取实时视频帧,通过Vision识别后在预览层绘制边框,这部分可以参考Vision框架的实时文本识别示例。


全局扩展方案

如果需要在多处UITextField使用该功能,可以创建扩展:

extension UITextField {
    func enableVINScanOnTap() {
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(triggerVINScan))
        addGestureRecognizer(tapGesture)
        inputView = UIView(frame: .zero)
    }
    
    @objc private func triggerVINScan() {
        // 调用方案一的触发逻辑或方案二的扫描控制器
        VINScanController.show(from: window!.rootViewController!) { [weak self] vin in
            self?.text = vin
        }
    }
}

在Storyboard中,只需给目标UITextField添加enableVINScanOnTap()调用即可。

内容的提问来源于stack exchange,提问作者Ben Hill

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 00:24:52