
实现的效果就是可以识别出来四个角点的位置,并绘制出来内容区域和角点,这个是使用swift+uikit原生开发的,为了实现这个目标,我们需要完成以下几个核心步骤:
1、配置相机权限:允许应用访问相机。
2、构建相机视图 (UIViewRepresentable):因为 SwiftUI 原生没有相机组件,我们需要封装 UIKit 的 AVCaptureSession。
3、处理元数据 (AVCaptureMetadataOutput):识别二维码并获取其角点(Corners)。
4、坐标转换:将相机感光元件的坐标(0.0 - 1.0)转换为屏幕的像素坐标。
5、绘制覆盖层:在 SwiftUI 中用不同颜色绘制四个点。
1、配置权限
在项目中要允许相机权限

2、定义数据模型
我们需要一个简单的方式来在相机逻辑和 SwiftUI 视图之间传递这四个点。
Swift
// 定义存储四个顶点的坐标
struct QRCorners {
var topLeft: CGPoint = .zero
var topRight: CGPoint = .zero
var bottomLeft: CGPoint = .zero
var bottomRight: CGPoint = .zero
// 只有当所有点都不是原点的时候才能被认为是有效的
var isValid: Bool {
return topLeft != .zero && topRight != .zero && bottomLeft != .zero && bottomRight != .zero
}
}
3、构建相机扫描器 (核心逻辑)
这是最复杂的部分。我们需要创建一个 UIViewRepresentable,它负责管理相机流,并将识别到的坐标传回给 SwiftUI。其中要注册一个协调器,用于处理实现AVCaptureMetadataOutputObjectsDelegate,就是处理扫描的二维码结果,要实现metadataOutput方法, 当扫描到二维码的时候,就会被系统自动调用

构建相机扫描器,并显示在SwiftUI视图中,代码如下:
Swift
// 构建相机扫描器,并显示在SwiftUI视图中
struct CameraScanner: UIViewRepresentable {
// 使用binding将识别到的角点回传给父视图
@Binding var corners: QRCorners
func makeUIView(context: Context) -> some UIView {
// 创建CameraView实例,自定义的UIView
let cameraView = CameraPreView()
// 设置代理
cameraView.delegate = context.coordinator
// 初始化相机配置,设置代理为Coordinator
cameraView.setupCamera()
// 让coordinator持有cameraView的弱引用,以便在代理方法中使用
context.coordinator.cameraView = cameraView
// 返回给SwiftUI使用
return cameraView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
// 更新视图的逻辑
}
// 实现makeCoordinator方法,创建协调器实例
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
// 创建协调器,用于处理相机输出的代理方法
class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
// 持有父视图的引用
var parent: CameraScanner
// 持有CameraView的弱引用
weak var cameraView: CameraPreView?
init(parent: CameraScanner) {
self.parent = parent
}
// 要实现这个函数,用于处理识别到的二维码,会被系统调用
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
print("coordinator 识别到二维码")
// 处理识别到的二维码逻辑,metadataObjects就是识别到的二维码数组
if metadataObjects.isEmpty {
// 没有检测到二维码,重置角点
DispatchQueue.main.async {
self.parent.corners = QRCorners()
}
return
}
// 获取检测到的第一个二维码对象
if let metadataObj = metadataObjects.first as? AVMetadataMachineReadableCodeObject {
// 确保是二维码类型
if metadataObj.type == .qr {
// 获取四个角点,这四个点坐标分别是左上、右上、右下、左下
let corners = metadataObj.corners
// 如果有四个角点,则打印它们的坐标
print("Coordinator QR Code Corners:")
// 打印四个角点坐标
for (index, corner) in corners.enumerated() {
print("Corner \(index + 1): \(corner)")
}
// 获取previewLayer进行坐标转换
guard let previewLayer = cameraView?.previewLayer,
let transformeObj = previewLayer.transformedMetadataObject(for: metadataObj) as? AVMetadataMachineReadableCodeObject
else {
return
}
// 获取转换后的角点
let transformCorners = transformeObj.corners
if transformCorners.count == 4 {
// 更新父视图的角点状态,注意顺序转换
parent.corners = QRCorners(
topLeft: transformCorners[0],
topRight: transformCorners[1],
bottomLeft: transformCorners[3],
bottomRight: transformCorners[2]
)
}
}
}
}
}
}
CameraPreView 用于处理二维码识别结果,并显示相机预览:
Swift
// 自定义一个CameraView 来持有AVCaptureSession和PreviewLayer,并实现AVCaptureMetadataOutputObjectsDelegate协议,
// 用于处理二维码识别结果,并显示相机预览
class CameraPreView: UIView {
// 相机会话和预览图层
var session: AVCaptureSession!
// 相机预览图层
var previewLayer: AVCaptureVideoPreviewLayer!
// 相机元数据输出代理
weak var delegate: AVCaptureMetadataOutputObjectsDelegate?
// 初始化方法
override init(frame: CGRect) {
super.init(frame: frame)
// ❌ 问题1:这里 bounds 可能是错误的
print("init 中的 bounds: \(bounds)")
// 可能输出:init 中的 bounds: (0.0, 0.0, 0.0, 0.0)
// ❌ 问题2:frame 可能还没设置
print("init 中的 frame: \(frame)")
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// 一定要在 layoutSubviews 中设置 bounds
override func layoutSubviews() {
super.layoutSubviews()
print("layoutSubviews 中的 bounds: \(bounds)")
// 给预览层设置正确的帧大小
previewLayer.frame = bounds // 这行必须要有!
}
// 设置相机
func setupCamera() {
// 配置相机会话和预览图层的逻辑
let session = AVCaptureSession()
// 获取默认的视频捕捉设备
guard let device = AVCaptureDevice.default(for: .video) else {
return
}
// 创建输入设备
guard let input = try? AVCaptureDeviceInput(device: device) else { return }
// 添加输入设备到会话
if session.canAddInput(input) {
session.addInput(input)
}
// 创建元数据输出
let output = AVCaptureMetadataOutput()
// 添加输出设备到会话
if session.canAddOutput(output) {
session.addOutput(output)
// 设置元数据输出代理
output.setMetadataObjectsDelegate(delegate, queue: DispatchQueue.main)
// 设置识别类型为二维码,可以为多个
output.metadataObjectTypes = [.qr]
}
// 创建预览图层
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
// 设置预览图层的填充模式
previewLayer.videoGravity = .resizeAspectFill
// 设置预览图层的帧大小
// previewLayer.frame = bounds
print("setupCamera 中的 bounds: \(bounds)")
// 将预览图层添加到视图的图层中
layer.addSublayer(previewLayer)
// 启动会话
self.session = session
self.previewLayer = previewLayer
// 启动相机会话,这么做会不会阻塞UI?
DispatchQueue.global(qos: .background).async {
session.startRunning()
}
}
}
Swift
// 辅助视图,画原点
struct CircleView: View {
let color: Color
let point: CGPoint
// 视图主体
var body: some View {
Circle()
.fill(color)
.frame(width: 10, height: 10)
.position(point)
.shadow(radius: 2)
}
}
主界面绘制层,就是将相机视图放在底层,然后在顶层绘制四个不同颜色的圆圈:
Swift
struct ContentView: View {
// 存储识别到的二维码角点
@State var corners = QRCorners()
var body: some View {
ZStack {
// 相机扫描器视图
CameraScanner(corners: $corners)
// 如果识别到有效的角点,则绘制边框
if corners.isValid {
ZStack {
CircleView(color: .red, point: corners.topLeft)
CircleView(color: .green, point: corners.topRight)
CircleView(color: .blue, point: corners.bottomLeft)
CircleView(color: .yellow, point: corners.bottomRight)
// 绘制连接四个角点的边框
Path { path in
path.move(to: corners.topLeft)
path.addLine(to: corners.topRight)
path.addLine(to: corners.bottomRight)
path.addLine(to: corners.bottomLeft)
path.closeSubpath()
}.stroke(Color.white, lineWidth: 2)
}
}
}
}
}