react-native-camera-kit-no-.../ios/ReactNativeCameraKit/SimulatorCamera.swift
Seph Soliman a73b84ef78 Removed iOsSleepBeforeStarting
No longer needed with proper begin/commit handling
2026-01-08 16:41:43 -08:00

222 lines
7.3 KiB
Swift

//
// SimulatorCamera.swift
// ReactNativeCameraKit
//
import AVFoundation
import React
import UIKit
/*
* Fake camera implementation to be used on simulator
*/
class SimulatorCamera: CameraProtocol {
private var onOrientationChange: RCTDirectEventBlock?
private var onZoom: RCTDirectEventBlock?
private var videoDeviceZoomFactor: Double = 1.0
private var videoDeviceMaxAvailableVideoZoomFactor: Double = 150.0
private var wideAngleZoomFactor: Double = 2.0
private var zoom: Double?
private var maxZoom: Double?
private var resizeMode: ResizeMode = .contain
private var barcodeFrameSize: CGSize?
var previewView: UIView { mockPreview }
private var fakeFocusFinishedTimer: Timer?
// Create mock camera layer. When a photo is taken, we capture this layer and save it in place of a hardware input.
private let mockPreview = SimulatorPreviewView(frame: .zero)
// MARK: - Public
func setup(cameraType: CameraType, supportedBarcodeType: [CodeFormat]) {
DispatchQueue.main.async {
self.mockPreview.cameraTypeLabel.text = "Camera type: \(cameraType)"
}
// Listen to orientation changes
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
NotificationCenter.default.addObserver(
forName: UIDevice.orientationDidChangeNotification,
object: UIDevice.current,
queue: nil,
using: { [weak self] notification in
self?.orientationChanged(notification: notification)
})
}
private func orientationChanged(notification: Notification) {
guard let device = notification.object as? UIDevice,
let orientation = Orientation(from: device.orientation)
else {
return
}
self.onOrientationChange?(["orientation": orientation.rawValue])
}
func cameraRemovedFromSuperview() {
NotificationCenter.default.removeObserver(
self, name: UIDevice.orientationDidChangeNotification, object: UIDevice.current)
}
func update(onOrientationChange: RCTDirectEventBlock?) {
self.onOrientationChange = onOrientationChange
}
func update(onZoom: RCTDirectEventBlock?) {
self.onZoom = onZoom
}
func update(iOsDeferredStartEnabled: Bool?) {
// Not applicable on simulator; deferred start only matters for real capture outputs.
}
func setVideoDevice(zoomFactor: Double) {
self.videoDeviceZoomFactor = zoomFactor
self.mockPreview.zoomLabel.text = "Zoom: \(zoomFactor)"
}
private var zoomStartedAt: Double = 1.0
func zoomPinchStart() {
DispatchQueue.main.async {
self.zoomStartedAt = self.videoDeviceZoomFactor
self.mockPreview.zoomLabel.text = "Zoom start"
}
}
func zoomPinchChange(pinchScale: CGFloat) {
guard !pinchScale.isNaN else { return }
DispatchQueue.main.async {
let desiredZoomFactor = self.zoomStartedAt * pinchScale
var maxZoomFactor = self.videoDeviceMaxAvailableVideoZoomFactor
if let maxZoom = self.maxZoom {
maxZoomFactor = min(maxZoom, maxZoomFactor)
}
let zoomForDevice = max(1.0, min(desiredZoomFactor, maxZoomFactor))
if zoomForDevice != self.videoDeviceZoomFactor {
// Only trigger zoom changes if it's an uncontrolled component (zoom isn't manually set)
// otherwise it's likely to cause issues inf. loops
if self.zoom == nil {
self.setVideoDevice(zoomFactor: zoomForDevice)
}
self.onZoom?(["zoom": zoomForDevice])
}
}
}
func focus(at: CGPoint, focusBehavior: FocusBehavior) {
DispatchQueue.main.async {
self.mockPreview.focusAtLabel.text =
"Focus at: (\(Int(at.x)), \(Int(at.y))), focusMode: \(focusBehavior.avFocusMode)"
}
// Fake focus finish after a second
fakeFocusFinishedTimer?.invalidate()
if case .customFocus(_, _, let focusFinished) = focusBehavior {
fakeFocusFinishedTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) {
_ in
focusFinished()
}
}
}
func update(torchMode: TorchMode) {
DispatchQueue.main.async {
self.mockPreview.torchModeLabel.text = "Torch mode: \(torchMode)"
}
}
func update(maxPhotoQualityPrioritization: MaxPhotoQualityPrioritization?) {
}
func update(flashMode: FlashMode) {
DispatchQueue.main.async {
self.mockPreview.flashModeLabel.text = "Flash mode: \(flashMode)"
}
}
func update(cameraType: CameraType) {
DispatchQueue.main.async {
self.mockPreview.cameraTypeLabel.text = "Camera type: \(cameraType)"
self.mockPreview.randomize()
}
}
func update(maxZoom: Double?) {
self.maxZoom = maxZoom
}
func update(zoom: Double?) {
self.zoom = zoom
DispatchQueue.main.async {
var zoomOrDefault = zoom ?? 0
// -1 will reset to zoom default (which is not 1 on modern cameras)
if zoomOrDefault == 0 {
zoomOrDefault = self.wideAngleZoomFactor
}
var maxZoomFactor = self.videoDeviceMaxAvailableVideoZoomFactor
if let maxZoom = self.maxZoom {
maxZoomFactor = min(maxZoom, maxZoomFactor)
}
let zoomForDevice = max(1.0, min(zoomOrDefault, maxZoomFactor))
self.setVideoDevice(zoomFactor: zoomForDevice)
// If they wanted to reset, tell them what the default zoom turned out to be
// regardless if it's controlled
if self.zoom == nil || zoom == 0 {
self.onZoom?(["zoom": zoomForDevice])
}
}
}
func update(resizeMode: ResizeMode) {
DispatchQueue.main.async {
self.mockPreview.resizeModeLabel.text = "Resize mode: \(resizeMode)"
self.mockPreview.randomize()
}
}
func isBarcodeScannerEnabled(
_ isEnabled: Bool,
supportedBarcodeTypes: [CodeFormat],
onBarcodeRead: ((_ barcode: String, _ codeFormat: CodeFormat) -> Void)?
) {}
func update(scannerFrameSize: CGRect?) {}
func capturePicture(
onWillCapture: @escaping () -> Void,
onSuccess:
@escaping (_ imageData: Data, _ thumbnailData: Data?, _ dimensions: CMVideoDimensions)
-> Void,
onError: @escaping (_ message: String) -> Void
) {
onWillCapture()
DispatchQueue.main.async {
// Generate snapshot from main UI thread
let previewSnapshot = self.mockPreview.snapshot(withTimestamp: true)
// Then switch to background thread
DispatchQueue.global(qos: .default).async {
if let imageData = previewSnapshot?.jpegData(compressionQuality: 0.85) {
onSuccess(imageData, nil, CMVideoDimensions(width: 480, height: 640))
} else {
onError("Failed to convert snapshot to JPEG data")
}
}
}
}
func update(barcodeFrameSize: CGSize?) {
self.barcodeFrameSize = barcodeFrameSize
}
}