请求AVCaptureDevice相机权限导致TabView选中标签自动切换问题
TabView请求相机权限时自动切回首个标签的问题解决
问题描述
在以TabView为根视图的SwiftUI应用中添加二维码扫描功能时,弹出相机权限请求对话框后,当前选中的第二个标签会自动切回第一个标签。调整CameraView在TabView中的顺序(设为第一个标签)时问题消失,但需要保持相机标签为第二个标签。
复现代码
ContentView
import SwiftUI enum Tab: Int { case first = 1, second } struct ContentView: View { @SceneStorage("selectedTab") var selectedTab: Int? init() { selectedTab = Tab.first.rawValue } var body: some View { TabView { TabView(selection: $selectedTab) { AnyView() .tabItem { Label("Calendar", systemImage: "calendar") } .tag(Tab.first) CameraView() .tabItem { Label("Camera", systemImage: "person.fill") } .tag(Tab.second) } .padding() } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
CameraView
import SwiftUI import UIKit import AVFoundation struct CameraView: UIViewRepresentable { typealias UIViewType = CameraPreview private let session = AVCaptureSession() private let metadataOutput = AVCaptureMetadataOutput() func setupCamera(_ uiView: CameraPreview) { if let backCamera = AVCaptureDevice.default(for: AVMediaType.video) { if let input = try? AVCaptureDeviceInput(device: backCamera) { session.sessionPreset = .photo if session.canAddInput(input) { session.addInput(input) } if session.canAddOutput(metadataOutput) { session.addOutput(metadataOutput) } let previewLayer = AVCaptureVideoPreviewLayer(session: session) uiView.backgroundColor = UIColor.gray previewLayer.videoGravity = .resizeAspectFill uiView.layer.addSublayer(previewLayer) uiView.previewLayer = previewLayer session.startRunning() } } } func makeUIView(context: UIViewRepresentableContext<CameraView>) -> CameraView.UIViewType { let cameraView = CameraPreview(session: session) checkCameraAuthorizationStatus(cameraView) return cameraView } static func dismantleUIView(_ uiView: CameraPreview, coordinator: ()) { uiView.session.stopRunning() } private func checkCameraAuthorizationStatus(_ uiView: CameraPreview) { let cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video) if cameraAuthorizationStatus == .authorized { setupCamera(uiView) } else { AVCaptureDevice.requestAccess(for: .video) { granted in DispatchQueue.main.sync { if granted { self.setupCamera(uiView) } } } } } func updateUIView(_ uiView: CameraPreview, context: UIViewRepresentableContext<CameraView>) { uiView.setContentHuggingPriority(.defaultHigh, for: .vertical) uiView.setContentHuggingPriority(.defaultLow, for: .horizontal) } }
CameraPreview
import UIKit import AVFoundation class CameraPreview: UIView { private var label:UILabel? var previewLayer: AVCaptureVideoPreviewLayer? var session = AVCaptureSession() init(session: AVCaptureSession) { super.init(frame: .zero) self.session = session } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() previewLayer?.frame = self.bounds } }
问题原因
- 无效的TabView嵌套:ContentView中嵌套了两层TabView,外层TabView没有配置任何tabItem,会干扰内层TabView的选中状态管理,权限弹窗触发视图更新时,选中状态被意外重置。
- 主线程阻塞风险:CameraView中使用
DispatchQueue.main.sync处理权限回调,可能导致主线程阻塞,触发SwiftUI视图的意外重建,进而执行init中的代码重置选中标签。 - SceneStorage初始化逻辑错误:在ContentView的init中直接设置
selectedTab,会在视图重建时覆盖用户当前选中的持久化状态。 - Tag类型不匹配:TabView的tag使用了
Tab枚举实例,但selectedTab是Int?类型,类型不匹配可能导致选中状态识别异常。
修复方案及代码调整
1. 移除嵌套TabView,修正选中状态绑定
将外层无用的TabView移除,直接使用带selection绑定的TabView作为根视图,同时统一tag与selectedTab的类型:
import SwiftUI enum Tab: Int { case first = 1, second } struct ContentView: View { // 将selectedTab改为非可选Int类型,直接声明默认值,避免init覆盖持久化状态 @SceneStorage("selectedTab") var selectedTab: Int = Tab.first.rawValue var body: some View { TabView(selection: $selectedTab) { AnyView() .tabItem { Label("Calendar", systemImage: "calendar") } .tag(Tab.first.rawValue) // 使用枚举rawValue,与selectedTab类型匹配 CameraView() .tabItem { Label("Camera", systemImage: "person.fill") } .tag(Tab.second.rawValue) } .padding() } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
2. 修正权限回调的线程处理
将DispatchQueue.main.sync改为DispatchQueue.main.async,避免主线程阻塞:
private func checkCameraAuthorizationStatus(_ uiView: CameraPreview) { let cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video) if cameraAuthorizationStatus == .authorized { setupCamera(uiView) } else { AVCaptureDevice.requestAccess(for: .video) { granted in // 使用async避免主线程阻塞 DispatchQueue.main.async { if granted { self.setupCamera(uiView) } } } } }
修复说明
- 移除嵌套TabView后,TabView的选中状态管理变得单一且正确,不会因多余的视图层级导致状态混乱。
- 调整SceneStorage的初始化方式后,仅在首次启动时使用默认值,后续会保留用户的选中状态,不会在视图重建时重置。
- 替换
sync为async避免了主线程阻塞,防止因线程问题触发的视图意外重建。 - 统一tag与selectedTab的类型,确保选中状态的识别准确。
内容的提问来源于stack exchange,提问作者annakodarpa
相关产品推荐
相关产品推荐

