uni-app x iOS 扫码模糊怎么办?从 <camera> 到 AVFoundation 原生扫码插件实战
前言
最近在一个 uni-app x 项目中遇到了一个比较典型的跨平台问题:
Android 使用 <camera mode="scanCode"> 扫描设备底部的二维码时基本正常,但在 iOS 上,近距离拍摄出来的画面非常模糊,二维码始终无法识别。
用<camera>是因为可以自定义扫码样式

一开始很容易把问题归因于:
- 相机分辨率不够 ?
- 没有开启自动对焦 ?
- 二维码尺寸太小 ?
- uni-app x 的
<camera>渲染不清晰 ?
但经过多轮排查后发现,问题的核心并不是分辨率,而是:
iPhone 普通广角镜头存在最短对焦距离,而
<camera>组件没有提供选择虚拟多摄设备和控制微距镜头切换的能力。
最终方案是为 iOS 单独实现一个基于 AVFoundation 的原生扫码插件。
原来的实现
页面最初使用 uni-app x 的 <camera> 组件:
html
<camera
class="camera-view"
mode="scanCode"
device-position="back"
@scancode="onScanCode"
@error="onCameraError"
/>
这种方式的优点很明显:
- 跨平台
- 接入成本低
- 自带扫码事件
- 不需要编写原生代码
但是它暴露的相机配置非常有限。
业务层可以选择前后摄像头、闪光灯和扫码模式,却无法控制:
- 使用哪一个后置物理镜头
- 是否使用多摄虚拟设备
- 是否允许自动切换到超广角微距镜头
- 对焦点和曝光点
- 自动对焦策略
- 扫码码制
- AVFoundation Session 配置
对于普通距离的二维码,这些限制可能没有影响;但当二维码很小、必须把手机贴近设备时,问题就出现了。
为什么提高分辨率没有用
排查过程中尝试过这些方案:
- 使用 1080p
- 使用
.high预设 - 降到 720p 提高对焦速度
- 开启
continuousAutoFocus - 每隔一段时间重新触发
autoFocus - 设置中心对焦点
- 对画面进行数字变焦
这些修改有时会让画面稍微改善,但始终无法从根本上解决问题。
原因是:
如果拍摄距离已经小于当前镜头的最短对焦距离,那么镜头物理上就无法合焦。
此时无论输出 720p、1080p 还是更高分辨率,得到的都只是分辨率更高的模糊图像。
数字变焦同样不能解决问题,因为它只是裁切并放大模糊画面。
定时触发自动对焦甚至可能产生副作用:镜头还没有完成一次对焦,就被下一次对焦操作打断,最终出现反复"拉风箱"的现象。
为什么系统相机可以拍清楚
同一台 iPhone,系统相机靠近二维码时却可以正常拍清楚。
这是因为部分多摄 iPhone 会在近距离场景下自动切换镜头:
- 默认使用普通广角镜头;
- 系统发现当前距离无法合焦;
- 自动切换到包含超广角镜头的微距方案;
- 超广角镜头完成近距离对焦。
因此,真正需要解决的问题不是"如何让普通广角镜头对焦更快",而是:
如何让扫码页面像系统相机一样,使用支持镜头切换的虚拟多摄设备。
使用 UTS + Swift 实现原生插件
插件目录结构如下:
text
uni_modules/qr-scanner/
├── package.json
└── utssdk/app-ios/
├── config.json
├── Info.plist
├── index.uts
└── QRScannerView.swift
各文件职责:
package.json:声明这是一个 uni_modules 插件config.json:配置 iOS deployment target 和系统框架Info.plist:声明相机权限用途QRScannerView.swift:实现 AVFoundation 相机及扫码逻辑index.uts:连接 Swift 原生视图和 uni-app x 页面
这里踩到的第一个打包问题就是遗漏了 package.json:
text
Cannot find module 'uni_modules/qr-scanner/package.json'
因此,即使是项目内部使用的本地 UTS 插件,也需要完整的 uni_modules 元数据。
将原生 UIView 嵌入页面
iOS 页面改用 <native-view>:
html
<!-- #ifdef APP-IOS -->
<native-view
class="camera-view"
@init="onNativeScanInit"
/>
<!-- #endif -->
Android 则继续保留原来的 <camera>:
html
<!-- #ifndef APP-IOS -->
<camera
class="camera-view"
mode="scanCode"
device-position="back"
@scancode="onScanCode"
@error="onCameraError"
/>
<!-- #endif -->
这样可以把改动控制在 iOS,避免影响已经正常工作的 Android 扫码逻辑。
UTS 层负责创建 Swift 视图并绑定到 native-view:
uts
export class NativeQRScanner {
private scannerView: QRScannerView | null = null
constructor(element: UniNativeViewElement) {
const view = new QRScannerView()
this.scannerView = view
element.bindIOSView(view as UIView)
}
@UTSJS.keepAlive
start(onResult: (code: string) => void): void {
if (this.scannerView == null) return
this.scannerView!.onScan = (code: string): void => {
onResult(code)
}
this.scannerView!.start()
}
resume(): void {
this.scannerView?.resume()
}
stop(): void {
this.scannerView?.stop()
}
}
@UTSJS.keepAlive 很重要,它保证扫码结果回调在 start() 方法执行结束后仍然有效。
选择支持微距切换的虚拟相机
原先使用的是:
swift
AVCaptureDevice.default(for: .video)
这种写法能获得默认后置摄像头,但不能保证拿到支持超广角切换的虚拟设备。
修改后优先选择:
swift
private func preferredBackCamera() -> AVCaptureDevice? {
if let device = AVCaptureDevice.default(
.builtInTripleCamera,
for: .video,
position: .back
) {
return device
}
if let device = AVCaptureDevice.default(
.builtInDualWideCamera,
for: .video,
position: .back
) {
return device
}
return AVCaptureDevice.default(
.builtInWideAngleCamera,
for: .video,
position: .back
)
}
选择顺序为:
- 三摄虚拟设备
- 双广角虚拟设备
- 普通广角设备兜底
需要注意,builtInDualCamera 通常是广角加长焦,不一定包含超广角,因此并不能直接解决近距离微距问题。
开启虚拟设备自动镜头切换
拿到虚拟设备后,还需要允许系统自动选择合适的物理镜头:
swift
if device.isVirtualDevice,
device.activePrimaryConstituentDeviceSwitchingBehavior != .unsupported {
device.setPrimaryConstituentDeviceSwitchingBehavior(
.auto,
restrictedSwitchingBehaviorConditions: []
)
}
.auto 表示系统可以根据以下条件自动选择镜头:
- 当前变焦倍率
- 光照条件
- 对焦距离
- 当前镜头能否完成曝光和对焦
当普通广角镜头无法在近距离合焦时,系统便有机会切换到超广角镜头。
这一步才是解决 iOS 近距离扫码模糊的核心。
恢复连续自动对焦
对焦策略改为系统式连续对焦:
swift
if device.isFocusPointOfInterestSupported {
device.focusPointOfInterest = CGPoint(x: 0.5, y: 0.5)
}
if device.isFocusModeSupported(.continuousAutoFocus) {
device.focusMode = .continuousAutoFocus
} else if device.isFocusModeSupported(.autoFocus) {
device.focusMode = .autoFocus
}
同时设置中心曝光点:
swift
if device.isExposurePointOfInterestSupported,
device.isExposureModeSupported(.continuousAutoExposure) {
device.exposurePointOfInterest = CGPoint(x: 0.5, y: 0.5)
device.exposureMode = .continuousAutoExposure
}
最终删除了定时重新触发对焦的方案,因为它会不断打断对焦收敛过程。

PS:图一的二维码模糊是我手动打了码,可清晰看到摄像头里面细小灰尘,已经是实现超广角聚焦了
清晰后为什么仍然扫不到
下面是我自己项目的坑(可忽略)
完成镜头切换后,画面已经明显清晰,但又出现了一个新问题:
二维码已经清楚可见,页面却没有任何反应。
这里分别遇到了两个问题。
问题一:识别区域坐标错误
为了只扫描白色框内的内容,最初设置了 rectOfInterest。
但是 metadataOutputRectConverted 的调用时机太早:Session 尚未启动、Preview Layer 的连接和方向还没有完全建立,导致换算出的识别区域位置错误。
最终表现就是:
- 相机预览正常
- 二维码清晰
- AVFoundation 永远检测不到二维码
为了提高稳定性,最终移除了识别区域限制,恢复全画面检测。页面上的白框只作为视觉引导。
对于扫码性能来说,全画面检测一个二维码的开销通常可以接受,比错误的坐标限制更可靠。
问题二:只开启了 QR 码
原生实现最初只配置了:
swift
output.metadataObjectTypes = [.qr]
但工业标签不一定都是 QR Code,也可能是:
- DataMatrix
- Aztec
- PDF417
- Code 128
- Code 39
- EAN
Android <camera mode="scanCode"> 默认可能支持多种码制,而原生 iOS 实现如果只开启 .qr,遇到 DataMatrix 时就不会产生任何回调。
最终改为根据设备支持情况动态启用:
swift
let desiredTypes: [AVMetadataObject.ObjectType] = [
.qr,
.dataMatrix,
.aztec,
.pdf417,
.code128,
.code39,
.code93,
.ean13,
.ean8,
.itf14,
.upce,
.interleaved2of5
]
let supportedTypes = desiredTypes.filter {
output.availableMetadataObjectTypes.contains($0)
}
output.metadataObjectTypes = supportedTypes
不能直接把所有类型全部设置进去,因为向 metadataObjectTypes 写入设备不支持的类型可能产生异常。
页面生命周期管理
相机会占用系统硬件资源,因此必须跟随页面生命周期启停:
uts
onShow(() => {
isProcessing.value = false
startNativeScan()
})
onHide(() => {
stopNativeScan()
})
onUnload(() => {
stopNativeScan()
qrScanner = null
})
扫描成功后暂停 Session,防止同一个码连续触发多次。
如果业务匹配失败,则延迟恢复扫描:
uts
setTimeout(() => {
isProcessing.value = false
qrScanner?.resume()
}, 2000)
减少自定义基座打包次数
UTS 原生插件的一个现实问题是:每次修改 Swift 或原生 UTS 代码,都需要重新打包自定义基座。
为了避免等到云端打包时才发现 Swift 语法错误,项目增加了一个本地检查脚本:
bash
./scripts/check-ios-plugin.sh qr-scanner
核心命令是:
bash
swiftc -typecheck QRScannerView.swift \
-sdk "$(xcrun --sdk iphoneos --show-sdk-path)" \
-target arm64-apple-ios15.0
它不能替代真机测试,也不能验证 UTS 桥接的运行时行为,但可以提前发现:
- Swift 语法错误
- API 名称错误
- 只读属性误赋值
- iOS Deployment Target 不兼容
例如,自动镜头切换不能写成:
swift
device.primaryConstituentDeviceSwitchingBehavior = .auto
因为该属性只读,正确方式是:
swift
device.setPrimaryConstituentDeviceSwitchingBehavior(
.auto,
restrictedSwitchingBehaviorConditions: []
)
这个问题就是通过本地 swiftc -typecheck 提前发现的。
总结
这次问题最重要的经验是:
iOS 扫码模糊不一定是分辨率问题,也可能是当前镜头已经超过物理对焦范围。
如果业务需要扫描非常小、距离很近的二维码,仅仅调整分辨率、数字变焦和自动对焦模式,很可能无法解决。
最终方案的关键点包括:
- 使用 AVFoundation 获取相机控制权;
- 优先选择包含超广角的虚拟多摄设备;
- 允许系统根据对焦距离自动切换镜头;
- 使用连续自动对焦,而不是定时重置对焦;
- 正确配置设备支持的扫码码制;
- 谨慎处理
rectOfInterest的坐标和调用时机; - 完整管理相机会话生命周期;
- 使用本地 Swift 类型检查减少无效打包。
<camera> 组件并不是不能扫码,而是它提供的是通用、跨平台能力。当业务开始依赖镜头切换、微距和精细对焦策略时,就需要下沉到原生层。
这也是跨平台开发中一个很典型的边界:
通用组件负责覆盖大多数场景,原生插件负责解决平台特有的深度需求。