Search code examples
swiftuiswift5ios16avcapturedeviceswiftui-tabview

Requesting access with AVCaptureDevice causes a switch in selected tab in tab view


I am trying to add a QR scanner to my app that has a Tab View at the root. It sort of works except for one thing; When the dialog appears to ask the user for permission to use the camera, it also automatically switches the selected tab from the second tab to the first one that appears in the code. If I switch places in the code between CameraView and AnyView it all works as expected, but I don't want the tab with the CameraView to be the first tab.

I've reproduced this in the following example:

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
    }
}

Any ideas on why this may be happening or how I can fix this or work around it?


Solution

  • After reading a bit about SceneStorage I started to wonder if that was what was creating the issue. I switched @SceneStorage("selectedTab") var selectedTab: Int? to @State var selectedTab: Int? but that did not work.

    After looking at Apple's own example for how to create a tab view I realized that maybe the syntax was off just a little bit.

    After switching to:

    enum Tab {
        case first   <-- this is different
        case second
    }
    
    struct ContentView: View {
        @State var selectedTab: Tab = .first <-- this is different
    
        var body: some View {
            TabView {
                TabView(selection: $selectedTab) {
                    AnyView()
                        .tabItem {
                            Label("Calendar", systemImage: "calendar")
                        }
                        .tag(Tab.first)
                    AddFriend()
                        .tabItem {
                            Label("Camera", systemImage: "person.fill")
                        }
                        .tag(Tab.second)
                            
                }
                .padding()
            }
        }
    }
    

    the problem no longer persisted. I guess the way I was initializing the default tab made it behave in unexpected ways.