diff --git a/.github/workflows/validate-ios.yml b/.github/workflows/validate-ios.yml
index ec341df..392675d 100644
--- a/.github/workflows/validate-ios.yml
+++ b/.github/workflows/validate-ios.yml
@@ -13,33 +13,30 @@ on:
- 'ios/**'
jobs:
- lint:
- name: SwiftLint
+ SwiftLint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run SwiftLint GitHub Action (--strict)
- uses: norio-nomura/action-swiftlint@3.2.1
+ uses: norio-nomura/action-swiftlint@master
with:
args: --strict
env:
# DIFF_BASE: ${{ github.base_ref }}
WORKING_DIRECTORY: ios
- # TODO: Figure out how to run SwiftFormat in a GitHub action
- # SwiftFormat:
- # name: SwiftFormat
- # description: 'https://github.com/nicklockwood/SwiftFormat'
- # runs-on: ubuntu-latest
- # defaults:
- # run:
- # working-directory: ./ios
- # steps:
- # - uses: actions/checkout@v2
+ SwiftFormat:
+ runs-on: macOS-latest
+ defaults:
+ run:
+ working-directory: ./ios
+ steps:
+ - uses: actions/checkout@v2
- # - name: Format Swift code
- # run: swiftformat --verbose .
- # working-directory: ${{env.working-directory}}
+ - name: Install SwiftFormat
+ run: brew install swiftformat
- # - name: Verify formatted code is unchanged
- # run: git diff --exit-code HEAD
- # working-directory: ${{env.working-directory}}
+ - name: Format Swift code
+ run: swiftformat --verbose .
+
+ - name: Verify formatted code is unchanged
+ run: git diff --exit-code HEAD
diff --git a/ios/.swift-version b/ios/.swift-version
new file mode 100644
index 0000000..ef425ca
--- /dev/null
+++ b/ios/.swift-version
@@ -0,0 +1 @@
+5.2
diff --git a/ios/.swiftformat b/ios/.swiftformat
index c8823b1..e3989bc 100644
--- a/ios/.swiftformat
+++ b/ios/.swiftformat
@@ -1,3 +1,14 @@
--allman false
--indent 2
--exclude Pods,Generated
+
+--disable andOperator
+--disable redundantReturn
+--disable wrapMultilineStatementBraces
+
+--enable organizeDeclarations
+--lifecycle didSetProps,requiresMainQueueSetup,view,methodQueue,getCameraView
+
+--enable markTypes
+
+--enable isEmpty
diff --git a/ios/.swiftlint.yml b/ios/.swiftlint.yml
index 06191e0..9ed53b6 100644
--- a/ios/.swiftlint.yml
+++ b/ios/.swiftlint.yml
@@ -18,7 +18,6 @@ opt_in_rules:
- last_where
- reduce_boolean
- reduce_into
- - sorted_first_last
- yoda_condition
- vertical_whitespace_opening_braces
- vertical_whitespace_closing_braces
diff --git a/ios/CameraError.swift b/ios/CameraError.swift
index b8652e6..99deae4 100644
--- a/ios/CameraError.swift
+++ b/ios/CameraError.swift
@@ -8,10 +8,14 @@
import Foundation
+// MARK: - PermissionError
+
enum PermissionError: String {
case microphone = "microphone-permission-denied"
case camera = "camera-permission-denied"
+ // MARK: Internal
+
var code: String {
return rawValue
}
@@ -26,6 +30,8 @@ enum PermissionError: String {
}
}
+// MARK: - ParameterError
+
enum ParameterError {
case invalid(unionName: String, receivedValue: String)
case unsupportedOS(unionName: String, receivedValue: String, supportedOnOs: String)
@@ -33,6 +39,8 @@ enum ParameterError {
case unsupportedInput(inputDescriptor: String)
case invalidCombination(provided: String, missing: String)
+ // MARK: Internal
+
var code: String {
switch self {
case .invalid:
@@ -64,6 +72,8 @@ enum ParameterError {
}
}
+// MARK: - DeviceError
+
enum DeviceError: String {
case configureError = "configuration-error"
case noDevice = "no-device"
@@ -74,6 +84,8 @@ enum DeviceError: String {
case focusNotSupported = "focus-not-supported"
case notAvailableOnSimulator = "camera-not-available-on-simulator"
+ // MARK: Internal
+
var code: String {
return rawValue
}
@@ -100,12 +112,16 @@ enum DeviceError: String {
}
}
+// MARK: - FormatError
+
enum FormatError {
case invalidFps(fps: Int)
case invalidHdr
case invalidFormat
case invalidPreset(preset: String)
+ // MARK: Internal
+
var code: String {
switch self {
case .invalidFormat:
@@ -133,10 +149,14 @@ enum FormatError {
}
}
+// MARK: - SessionError
+
enum SessionError {
case cameraNotReady
case audioSessionSetupFailed(reason: String)
+ // MARK: Internal
+
var code: String {
switch self {
case .cameraNotReady:
@@ -156,6 +176,8 @@ enum SessionError {
}
}
+// MARK: - CaptureError
+
enum CaptureError {
case invalidPhotoFormat
case recordingInProgress
@@ -165,6 +187,8 @@ enum CaptureError {
case invalidPhotoCodec
case unknown(message: String? = nil)
+ // MARK: Internal
+
var code: String {
switch self {
case .invalidPhotoFormat:
@@ -204,9 +228,13 @@ enum CaptureError {
}
}
+// MARK: - SystemError
+
enum SystemError: String {
case noManager = "no-camera-manager"
+ // MARK: Internal
+
var code: String {
return rawValue
}
@@ -219,6 +247,8 @@ enum SystemError: String {
}
}
+// MARK: - CameraError
+
enum CameraError: Error {
case permission(_ id: PermissionError)
case parameter(_ id: ParameterError)
@@ -229,6 +259,8 @@ enum CameraError: Error {
case system(_ id: SystemError)
case unknown(message: String? = nil)
+ // MARK: Internal
+
var code: String {
switch self {
case let .permission(id: id):
diff --git a/ios/CameraView+TakePhoto.swift b/ios/CameraView+TakePhoto.swift
index fbd06d3..b0f4387 100644
--- a/ios/CameraView+TakePhoto.swift
+++ b/ios/CameraView+TakePhoto.swift
@@ -8,9 +8,10 @@
import AVFoundation
+// MARK: - TakePhotoOptions
+
struct TakePhotoOptions {
- var videoCodec: AVVideoCodecType?
- var qualityPrioritization: String?
+ // MARK: Lifecycle
init(fromDictionary dictionary: NSDictionary) {
if let videoCodec = dictionary.value(forKey: "videoCodec") as? String {
@@ -18,6 +19,11 @@ struct TakePhotoOptions {
}
qualityPrioritization = dictionary.value(forKey: "qualityPrioritization") as? String
}
+
+ // MARK: Internal
+
+ var videoCodec: AVVideoCodecType?
+ var qualityPrioritization: String?
}
extension CameraView {
diff --git a/ios/CameraView.swift b/ios/CameraView.swift
index b08bd73..a27329a 100644
--- a/ios/CameraView.swift
+++ b/ios/CameraView.swift
@@ -29,72 +29,10 @@ import UIKit
private let propsThatRequireReconfiguration = ["cameraId", "enableDepthData", "enableHighResolutionCapture", "enablePortraitEffectsMatteDelivery", "preset", "onCodeScanned", "scannableCodes"]
private let propsThatRequireDeviceReconfiguration = ["fps", "hdr", "lowLightBoost", "colorSpace"]
+// MARK: - CameraView
+
final class CameraView: UIView {
- // pragma MARK: Exported Properties
- // props that require reconfiguring
- @objc var cameraId: NSString?
- @objc var enableDepthData = false
- @objc var enableHighResolutionCapture: NSNumber? // nullable bool
- @objc var enablePortraitEffectsMatteDelivery = false
- @objc var preset: String?
- @objc var scannableCodes: [String]?
- // props that require format reconfiguring
- @objc var format: NSDictionary?
- @objc var fps: NSNumber?
- @objc var hdr: NSNumber? // nullable bool
- @objc var lowLightBoost: NSNumber? // nullable bool
- @objc var colorSpace: NSString?
- // other props
- @objc var isActive = false
- @objc var torch = "off"
- @objc var zoom: NSNumber = 0.0 // in percent
- // events
- @objc var onInitialized: RCTDirectEventBlock?
- @objc var onError: RCTDirectEventBlock?
- @objc var onCodeScanned: RCTBubblingEventBlock?
- @objc var enableZoomGesture: Bool = false {
- didSet {
- if enableZoomGesture {
- addPinchGestureRecognizer()
- } else {
- removePinchGestureRecognizer()
- }
- }
- }
-
- var isReady: Bool = false
- var isRunning: Bool {
- return captureSession.isRunning
- }
-
- // pragma MARK: Private Properties
- /// The serial execution queue for the camera preview layer (input stream) as well as output processing (take photo, record video, process metadata/barcodes)
- internal let queue = DispatchQueue(label: "com.mrousavy.camera-queue", qos: .userInteractive, attributes: [], autoreleaseFrequency: .inherit, target: nil)
- private let captureSession = AVCaptureSession()
- internal var videoDeviceInput: AVCaptureDeviceInput?
- internal var audioDeviceInput: AVCaptureDeviceInput?
- internal var photoOutput: AVCapturePhotoOutput?
- internal var movieOutput: AVCaptureMovieFileOutput?
- internal var metadataOutput: AVCaptureMetadataOutput?
- // CameraView+TakePhoto
- internal var photoCaptureDelegates: [PhotoCaptureDelegate] = []
- // CameraView+RecordVideo
- internal var recordingDelegateResolver: RCTPromiseResolveBlock?
- internal var recordingDelegateRejecter: RCTPromiseRejectBlock?
- // CameraView+Zoom
- internal var pinchGestureRecognizer: UIPinchGestureRecognizer?
- internal var pinchScaleOffset: CGFloat = 1.0
-
- // pragma MARK: Setup
- override class var layerClass: AnyClass {
- return AVCaptureVideoPreviewLayer.self
- }
-
- /// Convenience wrapper to get layer as its statically known type.
- var videoPreviewLayer: AVCaptureVideoPreviewLayer {
- // swiftlint:disable force_cast
- return layer as! AVCaptureVideoPreviewLayer
- }
+ // MARK: Lifecycle
override init(frame: CGRect) {
super.init(frame: frame)
@@ -114,26 +52,6 @@ final class CameraView: UIView {
object: captureSession)
}
- override func removeFromSuperview() {
- captureSession.stopRunning()
- super.removeFromSuperview()
- }
-
- @objc
- func sessionRuntimeError(notification: Notification) {
- guard let error = notification.userInfo?[AVCaptureSessionErrorKey] as? AVError else {
- return
- }
-
- if isActive {
- // restart capture session after an error occured
- queue.async {
- self.captureSession.startRunning()
- }
- }
- invokeOnError(.unknown(message: error.localizedDescription), cause: error as NSError)
- }
-
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) is not implemented.")
@@ -188,6 +106,159 @@ final class CameraView: UIView {
}
}
+ // MARK: Internal
+
+ // pragma MARK: Setup
+ override class var layerClass: AnyClass {
+ return AVCaptureVideoPreviewLayer.self
+ }
+
+ // pragma MARK: Exported Properties
+ // props that require reconfiguring
+ @objc var cameraId: NSString?
+ @objc var enableDepthData = false
+ @objc var enableHighResolutionCapture: NSNumber? // nullable bool
+ @objc var enablePortraitEffectsMatteDelivery = false
+ @objc var preset: String?
+ @objc var scannableCodes: [String]?
+ // props that require format reconfiguring
+ @objc var format: NSDictionary?
+ @objc var fps: NSNumber?
+ @objc var hdr: NSNumber? // nullable bool
+ @objc var lowLightBoost: NSNumber? // nullable bool
+ @objc var colorSpace: NSString?
+ // other props
+ @objc var isActive = false
+ @objc var torch = "off"
+ @objc var zoom: NSNumber = 0.0 // in percent
+ // events
+ @objc var onInitialized: RCTDirectEventBlock?
+ @objc var onError: RCTDirectEventBlock?
+ @objc var onCodeScanned: RCTBubblingEventBlock?
+ var isReady = false
+ // pragma MARK: Private Properties
+ /// The serial execution queue for the camera preview layer (input stream) as well as output processing (take photo, record video, process metadata/barcodes)
+ internal let queue = DispatchQueue(label: "com.mrousavy.camera-queue", qos: .userInteractive, attributes: [], autoreleaseFrequency: .inherit, target: nil)
+ internal var videoDeviceInput: AVCaptureDeviceInput?
+ internal var audioDeviceInput: AVCaptureDeviceInput?
+ internal var photoOutput: AVCapturePhotoOutput?
+ internal var movieOutput: AVCaptureMovieFileOutput?
+ internal var metadataOutput: AVCaptureMetadataOutput?
+ // CameraView+TakePhoto
+ internal var photoCaptureDelegates: [PhotoCaptureDelegate] = []
+ // CameraView+RecordVideo
+ internal var recordingDelegateResolver: RCTPromiseResolveBlock?
+ internal var recordingDelegateRejecter: RCTPromiseRejectBlock?
+ // CameraView+Zoom
+ internal var pinchGestureRecognizer: UIPinchGestureRecognizer?
+ internal var pinchScaleOffset: CGFloat = 1.0
+
+ @objc var enableZoomGesture = false {
+ didSet {
+ if enableZoomGesture {
+ addPinchGestureRecognizer()
+ } else {
+ removePinchGestureRecognizer()
+ }
+ }
+ }
+
+ var isRunning: Bool {
+ return captureSession.isRunning
+ }
+
+ /// Convenience wrapper to get layer as its statically known type.
+ var videoPreviewLayer: AVCaptureVideoPreviewLayer {
+ // swiftlint:disable force_cast
+ return layer as! AVCaptureVideoPreviewLayer
+ }
+
+ override func removeFromSuperview() {
+ captureSession.stopRunning()
+ super.removeFromSuperview()
+ }
+
+ @objc
+ func sessionRuntimeError(notification: Notification) {
+ guard let error = notification.userInfo?[AVCaptureSessionErrorKey] as? AVError else {
+ return
+ }
+
+ if isActive {
+ // restart capture session after an error occured
+ queue.async {
+ self.captureSession.startRunning()
+ }
+ }
+ invokeOnError(.unknown(message: error.localizedDescription), cause: error as NSError)
+ }
+
+ internal final func setTorchMode(_ torchMode: String) {
+ guard let device = videoDeviceInput?.device else {
+ return invokeOnError(.session(.cameraNotReady))
+ }
+ guard var torchMode = AVCaptureDevice.TorchMode(withString: torchMode) else {
+ return invokeOnError(.parameter(.invalid(unionName: "TorchMode", receivedValue: torch)))
+ }
+ if !captureSession.isRunning {
+ torchMode = .off
+ }
+ if device.torchMode == torchMode {
+ // no need to run the whole lock/unlock bs
+ return
+ }
+ if !device.hasTorch || !device.isTorchAvailable {
+ if torchMode == .off {
+ // ignore it, when it's off and not supported, it's off.
+ return
+ } else {
+ // torch mode is .auto or .on, but no torch is available.
+ return invokeOnError(.device(.torchUnavailable))
+ }
+ }
+ do {
+ try device.lockForConfiguration()
+ device.torchMode = torchMode
+ if torchMode == .on {
+ try device.setTorchModeOn(level: 1.0)
+ }
+ device.unlockForConfiguration()
+ } catch let error as NSError {
+ return invokeOnError(.device(.configureError), cause: error)
+ }
+ }
+
+ // pragma MARK: Event Invokers
+ internal final func invokeOnError(_ error: CameraError, cause: NSError? = nil) {
+ ReactLogger.log(level: .error, message: error.localizedDescription, alsoLogToJS: true)
+ guard let onError = self.onError else { return }
+
+ var causeDictionary: [String: Any]?
+ if let cause = cause {
+ causeDictionary = [
+ "code": cause.code,
+ "domain": cause.domain,
+ "message": cause.localizedDescription,
+ "details": cause.userInfo,
+ ]
+ }
+ onError([
+ "code": error.code,
+ "message": error.message,
+ "cause": causeDictionary ?? NSNull(),
+ ])
+ }
+
+ internal final func invokeOnInitialized() {
+ ReactLogger.log(level: .info, message: "Camera onInitialized()", alsoLogToJS: true)
+ guard let onInitialized = self.onInitialized else { return }
+ onInitialized([String: Any]())
+ }
+
+ // MARK: Private
+
+ private let captureSession = AVCaptureSession()
+
// pragma MARK: Session, Device and Format Configuration
/**
Configures the Capture Session.
@@ -196,7 +267,7 @@ final class CameraView: UIView {
isReady = false
#if targetEnvironment(simulator)
- return invokeOnError(.device(.notAvailableOnSimulator))
+ return invokeOnError(.device(.notAvailableOnSimulator))
#endif
guard cameraId != nil else {
@@ -295,7 +366,7 @@ final class CameraView: UIView {
}
captureSession.addOutput(photoOutput!)
if videoDeviceInput!.device.position == .front {
- photoOutput!.mirror()
+ photoOutput!.mirror()
}
// Video Output
@@ -308,7 +379,7 @@ final class CameraView: UIView {
}
captureSession.addOutput(movieOutput!)
if videoDeviceInput!.device.position == .front {
- movieOutput!.mirror()
+ movieOutput!.mirror()
}
// Barcode Scanning
@@ -422,66 +493,4 @@ final class CameraView: UIView {
return invokeOnError(.device(.configureError), cause: error)
}
}
-
- internal final func setTorchMode(_ torchMode: String) {
- guard let device = videoDeviceInput?.device else {
- return invokeOnError(.session(.cameraNotReady))
- }
- guard var torchMode = AVCaptureDevice.TorchMode(withString: torchMode) else {
- return invokeOnError(.parameter(.invalid(unionName: "TorchMode", receivedValue: torch)))
- }
- if !captureSession.isRunning {
- torchMode = .off
- }
- if device.torchMode == torchMode {
- // no need to run the whole lock/unlock bs
- return
- }
- if !device.hasTorch || !device.isTorchAvailable {
- if torchMode == .off {
- // ignore it, when it's off and not supported, it's off.
- return
- } else {
- // torch mode is .auto or .on, but no torch is available.
- return invokeOnError(.device(.torchUnavailable))
- }
- }
- do {
- try device.lockForConfiguration()
- device.torchMode = torchMode
- if torchMode == .on {
- try device.setTorchModeOn(level: 1.0)
- }
- device.unlockForConfiguration()
- } catch let error as NSError {
- return invokeOnError(.device(.configureError), cause: error)
- }
- }
-
- // pragma MARK: Event Invokers
- internal final func invokeOnError(_ error: CameraError, cause: NSError? = nil) {
- ReactLogger.log(level: .error, message: error.localizedDescription, alsoLogToJS: true)
- guard let onError = self.onError else { return }
-
- var causeDictionary: [String: Any]?
- if let cause = cause {
- causeDictionary = [
- "code": cause.code,
- "domain": cause.domain,
- "message": cause.localizedDescription,
- "details": cause.userInfo
- ]
- }
- onError([
- "code": error.code,
- "message": error.message,
- "cause": causeDictionary ?? NSNull(),
- ])
- }
-
- internal final func invokeOnInitialized() {
- ReactLogger.log(level: .info, message: "Camera onInitialized()", alsoLogToJS: true)
- guard let onInitialized = self.onInitialized else { return }
- onInitialized([String: Any]())
- }
}
diff --git a/ios/CameraViewManager.swift b/ios/CameraViewManager.swift
index 55def09..f46e2c9 100644
--- a/ios/CameraViewManager.swift
+++ b/ios/CameraViewManager.swift
@@ -11,17 +11,15 @@ import Foundation
@objc(CameraViewManager)
final class CameraViewManager: RCTViewManager {
- // pragma MARK: Setup
- override final func view() -> UIView! {
- return CameraView()
- }
+ // MARK: Lifecycle
override static func requiresMainQueueSetup() -> Bool {
return true
}
- override var methodQueue: DispatchQueue! {
- return DispatchQueue.main
+ // pragma MARK: Setup
+ override final func view() -> UIView! {
+ return CameraView()
}
private func getCameraView(withTag tag: NSNumber) -> CameraView {
@@ -29,6 +27,12 @@ final class CameraViewManager: RCTViewManager {
return bridge.uiManager.view(forReactTag: tag) as! CameraView
}
+ // MARK: Internal
+
+ override var methodQueue: DispatchQueue! {
+ return DispatchQueue.main
+ }
+
// pragma MARK: Exported Functions
@objc
final func startRecording(_ node: NSNumber, options: NSDictionary, onRecordCallback: @escaping RCTResponseSenderBlock) {
@@ -65,7 +69,7 @@ final class CameraViewManager: RCTViewManager {
guard let movieOutput = component.movieOutput else {
throw CameraError.session(SessionError.cameraNotReady)
}
- return movieOutput.availableVideoCodecTypes.map { $0.descriptor }
+ return movieOutput.availableVideoCodecTypes.map(\.descriptor)
}
}
@@ -76,26 +80,10 @@ final class CameraViewManager: RCTViewManager {
guard let photoOutput = component.photoOutput else {
throw CameraError.session(SessionError.cameraNotReady)
}
- return photoOutput.availablePhotoCodecTypes.map { $0.descriptor }
+ return photoOutput.availablePhotoCodecTypes.map(\.descriptor)
}
}
- private final func getAllDeviceTypes() -> [AVCaptureDevice.DeviceType] {
- var deviceTypes: [AVCaptureDevice.DeviceType] = []
- if #available(iOS 13.0, *) {
- deviceTypes.append(.builtInTripleCamera)
- deviceTypes.append(.builtInDualWideCamera)
- deviceTypes.append(.builtInUltraWideCamera)
- }
- if #available(iOS 11.1, *) {
- deviceTypes.append(.builtInTrueDepthCamera)
- }
- deviceTypes.append(.builtInDualCamera)
- deviceTypes.append(.builtInWideAngleCamera)
- deviceTypes.append(.builtInTelephotoCamera)
- return deviceTypes
- }
-
// pragma MARK: View Manager funcs
@objc
final func getAvailableCameraDevices(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
@@ -104,7 +92,7 @@ final class CameraViewManager: RCTViewManager {
return discoverySession.devices.map {
return [
"id": $0.uniqueID,
- "devices": $0.physicalDevices.map { $0.deviceType.descriptor },
+ "devices": $0.physicalDevices.map(\.deviceType.descriptor),
"position": $0.position.descriptor,
"name": $0.localizedName,
"hasFlash": $0.hasFlash,
@@ -155,4 +143,22 @@ final class CameraViewManager: RCTViewManager {
resolve(result.descriptor)
}
}
+
+ // MARK: Private
+
+ private final func getAllDeviceTypes() -> [AVCaptureDevice.DeviceType] {
+ var deviceTypes: [AVCaptureDevice.DeviceType] = []
+ if #available(iOS 13.0, *) {
+ deviceTypes.append(.builtInTripleCamera)
+ deviceTypes.append(.builtInDualWideCamera)
+ deviceTypes.append(.builtInUltraWideCamera)
+ }
+ if #available(iOS 11.1, *) {
+ deviceTypes.append(.builtInTrueDepthCamera)
+ }
+ deviceTypes.append(.builtInDualCamera)
+ deviceTypes.append(.builtInWideAngleCamera)
+ deviceTypes.append(.builtInTelephotoCamera)
+ return deviceTypes
+ }
}
diff --git a/ios/Extensions/AVCaptureDevice.Format+toDictionary.swift b/ios/Extensions/AVCaptureDevice.Format+toDictionary.swift
index ff7b3e6..1a2c101 100644
--- a/ios/Extensions/AVCaptureDevice.Format+toDictionary.swift
+++ b/ios/Extensions/AVCaptureDevice.Format+toDictionary.swift
@@ -23,7 +23,7 @@ extension AVCaptureDevice.Format {
func toDictionary() -> [String: Any] {
var dict: [String: Any] = [
- "videoStabilizationModes": videoStabilizationModes.map { $0.descriptor },
+ "videoStabilizationModes": videoStabilizationModes.map(\.descriptor),
"autoFocusSystem": autoFocusSystem.descriptor,
"photoHeight": highResolutionStillImageDimensions.height,
"photoWidth": highResolutionStillImageDimensions.width,
@@ -31,7 +31,7 @@ extension AVCaptureDevice.Format {
"minISO": minISO,
"fieldOfView": videoFieldOfView,
"maxZoom": videoMaxZoomFactor,
- "colorSpaces": supportedColorSpaces.map { $0.descriptor },
+ "colorSpaces": supportedColorSpaces.map(\.descriptor),
"supportsVideoHDR": isVideoHDRSupported,
"supportsPhotoHDR": false,
"frameRateRanges": videoSupportedFrameRateRanges.map {
diff --git a/ios/Extensions/AVCaptureMovieFileOutput+mirror.swift b/ios/Extensions/AVCaptureMovieFileOutput+mirror.swift
index 068c5be..fae61a3 100644
--- a/ios/Extensions/AVCaptureMovieFileOutput+mirror.swift
+++ b/ios/Extensions/AVCaptureMovieFileOutput+mirror.swift
@@ -9,11 +9,11 @@
import AVFoundation
extension AVCaptureMovieFileOutput {
- func mirror() {
- connections.forEach { (connection) in
- if connection.isVideoMirroringSupported {
- connection.isVideoMirrored = true
- }
- }
+ func mirror() {
+ connections.forEach { connection in
+ if connection.isVideoMirroringSupported {
+ connection.isVideoMirrored = true
+ }
}
+ }
}
diff --git a/ios/Extensions/AVCapturePhotoOutput+mirror.swift b/ios/Extensions/AVCapturePhotoOutput+mirror.swift
index 728e71b..0884ae0 100644
--- a/ios/Extensions/AVCapturePhotoOutput+mirror.swift
+++ b/ios/Extensions/AVCapturePhotoOutput+mirror.swift
@@ -9,11 +9,11 @@
import AVFoundation
extension AVCapturePhotoOutput {
- func mirror() {
- connections.forEach { (connection) in
- if connection.isVideoMirroringSupported {
- connection.isVideoMirrored = true
- }
- }
+ func mirror() {
+ connections.forEach { connection in
+ if connection.isVideoMirroringSupported {
+ connection.isVideoMirrored = true
+ }
}
+ }
}
diff --git a/ios/PhotoCaptureDelegate.swift b/ios/PhotoCaptureDelegate.swift
index 72b585c..b0c79a5 100644
--- a/ios/PhotoCaptureDelegate.swift
+++ b/ios/PhotoCaptureDelegate.swift
@@ -10,8 +10,10 @@ import AVFoundation
private var delegatesReferences: [NSObject] = []
+// MARK: - PhotoCaptureDelegate
+
class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate {
- private let promise: Promise
+ // MARK: Lifecycle
required init(promise: Promise) {
self.promise = promise
@@ -19,6 +21,8 @@ class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate {
delegatesReferences.append(self)
}
+ // MARK: Internal
+
func photoOutput(_: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
defer {
delegatesReferences.removeAll(where: { $0 == self })
@@ -66,4 +70,8 @@ class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate {
return promise.reject(error: .capture(.unknown(message: error.localizedDescription)), cause: error as NSError)
}
}
+
+ // MARK: Private
+
+ private let promise: Promise
}
diff --git a/ios/README.md b/ios/README.md
index 1908197..9a9fe45 100644
--- a/ios/README.md
+++ b/ios/README.md
@@ -17,4 +17,4 @@ This folder contains the iOS-platform-specific code for react-native-vision-came
It is recommended that you work on the code using the Example project (`example/ios/VisionCameraExample.xcworkspace`), since that always includes the React Native header files, plus you can easily test changes that way.
-You can however still edit the library project here by opening `VisionCamera.xcodeproj`.
+You can however still edit the library project here by opening `VisionCamera.xcodeproj`, this has the advantage of **automatically formatting your Code** (swiftformat) and **showing you Linter errors** (swiftlint) when trying to build (⌘+B).
diff --git a/ios/React/Promise.swift b/ios/React/Promise.swift
index 43817b5..48d4d47 100644
--- a/ios/React/Promise.swift
+++ b/ios/React/Promise.swift
@@ -8,18 +8,21 @@
import Foundation
+// MARK: - Promise
+
/**
* Represents a JavaScript Promise instance. `reject()` and `resolve()` should only be called once.
*/
class Promise {
- private let resolver: RCTPromiseResolveBlock
- private let rejecter: RCTPromiseRejectBlock
+ // MARK: Lifecycle
init(resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
self.resolver = resolver
self.rejecter = rejecter
}
+ // MARK: Internal
+
func reject(error: CameraError, cause: NSError?) {
rejecter(error.code, error.message, cause)
}
@@ -35,6 +38,11 @@ class Promise {
func resolve() {
resolve(nil)
}
+
+ // MARK: Private
+
+ private let resolver: RCTPromiseResolveBlock
+ private let rejecter: RCTPromiseRejectBlock
}
/**
diff --git a/ios/React/ReactLogger.swift b/ios/React/ReactLogger.swift
index 849f25e..46a1cc8 100644
--- a/ios/React/ReactLogger.swift
+++ b/ios/React/ReactLogger.swift
@@ -10,6 +10,8 @@ import Foundation
let context = "Camera"
+// MARK: - ReactLogger
+
enum ReactLogger {
static func log(level: RCTLogLevel, message: String, alsoLogToJS: Bool = false, file: String = #file, lineNumber: Int = #line) {
RCTDefaultLogFunction(level, RCTLogSource.native, file, lineNumber as NSNumber, "\(context): \(message)")
diff --git a/ios/VideoCaptureDelegate.swift b/ios/VideoCaptureDelegate.swift
index ef4a6be..dca742e 100644
--- a/ios/VideoCaptureDelegate.swift
+++ b/ios/VideoCaptureDelegate.swift
@@ -14,9 +14,10 @@ import AVFoundation
// once the delegate has been triggered once.
private var delegateReferences: [NSObject] = []
+// MARK: - RecordingDelegateWithCallback
+
class RecordingDelegateWithCallback: NSObject, AVCaptureFileOutputRecordingDelegate {
- private let callback: RCTResponseSenderBlock // (video?, error?) => void
- private let resetTorchMode: () -> Void
+ // MARK: Lifecycle
init(callback: @escaping RCTResponseSenderBlock, resetTorchMode: @escaping () -> Void) {
self.callback = callback
@@ -25,6 +26,8 @@ class RecordingDelegateWithCallback: NSObject, AVCaptureFileOutputRecordingDeleg
delegateReferences.append(self)
}
+ // MARK: Internal
+
func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from _: [AVCaptureConnection], error: Error?) {
defer {
self.resetTorchMode()
@@ -37,4 +40,9 @@ class RecordingDelegateWithCallback: NSObject, AVCaptureFileOutputRecordingDeleg
let seconds = CMTimeGetSeconds(output.recordedDuration)
return callback([["path": outputFileURL.absoluteString, "duration": seconds, "size": output.recordedFileSize], NSNull()])
}
+
+ // MARK: Private
+
+ private let callback: RCTResponseSenderBlock // (video?, error?) => void
+ private let resetTorchMode: () -> Void
}
diff --git a/ios/VisionCamera.xcodeproj/project.pbxproj b/ios/VisionCamera.xcodeproj/project.pbxproj
index 665b9e2..ba7c230 100644
--- a/ios/VisionCamera.xcodeproj/project.pbxproj
+++ b/ios/VisionCamera.xcodeproj/project.pbxproj
@@ -196,6 +196,7 @@
buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "VisionCamera" */;
buildPhases = (
B81F6C7625E515810008974A /* Run SwiftLint */,
+ B80D6CAB25F770FE006F2CB7 /* Run SwiftFormat */,
58B511D71A9E6C8500147676 /* Sources */,
58B511D81A9E6C8500147676 /* Frameworks */,
58B511D91A9E6C8500147676 /* CopyFiles */,
@@ -242,6 +243,24 @@
/* End PBXProject section */
/* Begin PBXShellScriptBuildPhase section */
+ B80D6CAB25F770FE006F2CB7 /* Run SwiftFormat */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ name = "Run SwiftFormat";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "if which swiftformat >/dev/null; then\n swiftformat .\nelse\n echo \"warning: SwiftFormat not installed, download from https://github.com/nicklockwood/SwiftFormat\"\nfi\n";
+ };
B81F6C7625E515810008974A /* Run SwiftLint */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -258,7 +277,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "if which swiftlint >/dev/null; then\n swiftlint autocorrect && swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
+ shellScript = "if which swiftlint >/dev/null; then\n swiftlint --fix && swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
};
/* End PBXShellScriptBuildPhase section */
diff --git a/package.json b/package.json
index f095223..62d9336 100644
--- a/package.json
+++ b/package.json
@@ -35,10 +35,14 @@
"example": "yarn --cwd example",
"pods": "cd example && pod-install --quiet",
"bootstrap": "yarn example && yarn && yarn pods",
- "ktlint-fix": "ktlint -F android/**/*.kt*",
- "swiftlint-fix": "cd ios && swiftlint autocorrect",
+ "ktlint": "scripts/ktlint.sh",
+ "swiftlint": "scripts/swiftlint.sh",
+ "swiftformat": "scripts/swiftformat.sh",
"docs": "cd docs && yarn build"
},
+ "pre-commit": [
+ "swiftformat"
+ ],
"keywords": [
"react-native",
"ios",
@@ -81,6 +85,7 @@
"eslint-plugin-react-native": "^3.10.0",
"jest": "^26.0.1",
"pod-install": "^0.1.0",
+ "pre-commit": "^1.2.2",
"prettier": "^2.2.1",
"react": "17.0.1",
"react-native": "0.63.4",
diff --git a/scripts/ktlint.sh b/scripts/ktlint.sh
new file mode 100755
index 0000000..1504641
--- /dev/null
+++ b/scripts/ktlint.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+if which ktlint >/dev/null; then
+ cd android && ktlint -F ./**/*.kt*
+else
+ echo "warning: KTLint not installed, download from https://github.com/pinterest/ktlint"
+fi
diff --git a/scripts/swiftformat.sh b/scripts/swiftformat.sh
new file mode 100755
index 0000000..42c29b1
--- /dev/null
+++ b/scripts/swiftformat.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+if which swiftformat >/dev/null; then
+ cd ios && swiftformat .
+else
+ echo "warning: SwiftFormat not installed, download from https://github.com/nicklockwood/SwiftFormat"
+fi
diff --git a/scripts/swiftlint.sh b/scripts/swiftlint.sh
new file mode 100755
index 0000000..1206992
--- /dev/null
+++ b/scripts/swiftlint.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+if which swiftlint >/dev/null; then
+ cd ios && swiftlint --fix && swiftlint
+else
+ echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
+fi
diff --git a/yarn.lock b/yarn.lock
index 602f828..b118d49 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,11 +2,6 @@
# yarn lockfile v1
-"@actions/core@^1.2.0":
- version "1.2.6"
- resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.2.6.tgz#a78d49f41a4def18e88ce47c2cac615d5694bf09"
- integrity sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA==
-
"@babel/code-frame@7.12.11":
version "7.12.11"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
@@ -1043,13 +1038,6 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
-"@firmnav/eslint-github-actions-formatter@^1.0.1":
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/@firmnav/eslint-github-actions-formatter/-/eslint-github-actions-formatter-1.0.1.tgz#dbedcc4d8a799faf9b709417981039819980aab0"
- integrity sha512-KbhZwNPFuwoRWspUfoJISOeGZHGSm7tvdOC+uOUlbcY9LNmusRHHmBcq3KaorvW9WmmiOS/2EOo0nafFZ0gpEQ==
- dependencies:
- "@actions/core" "^1.2.0"
-
"@hapi/address@2.x.x":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
@@ -3035,7 +3023,7 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
-concat-stream@^1.6.0:
+concat-stream@^1.4.7, concat-stream@^1.6.0:
version "1.6.2"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
@@ -3289,7 +3277,7 @@ cosmiconfig@^5.0.5, cosmiconfig@^5.1.0:
js-yaml "^3.13.1"
parse-json "^4.0.0"
-cross-spawn@^5.1.0:
+cross-spawn@^5.0.1, cross-spawn@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=
@@ -7112,6 +7100,11 @@ os-name@4.0.0:
macos-release "^2.2.0"
windows-release "^4.0.0"
+os-shim@^0.1.2:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917"
+ integrity sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc=
+
os-tmpdir@^1.0.0, os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
@@ -7436,6 +7429,15 @@ posix-character-classes@^0.1.0:
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
+pre-commit@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/pre-commit/-/pre-commit-1.2.2.tgz#dbcee0ee9de7235e57f79c56d7ce94641a69eec6"
+ integrity sha1-287g7p3nI15X95xW186UZBpp7sY=
+ dependencies:
+ cross-spawn "^5.0.1"
+ spawn-sync "^1.0.15"
+ which "1.2.x"
+
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@@ -8480,6 +8482,14 @@ source-map@^0.7.3:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
+spawn-sync@^1.0.15:
+ version "1.0.15"
+ resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476"
+ integrity sha1-sAeZVX63+wyDdsKdROih6mfldHY=
+ dependencies:
+ concat-stream "^1.4.7"
+ os-shim "^0.1.2"
+
spdx-correct@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
@@ -9381,6 +9391,13 @@ which-module@^2.0.0:
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
+which@1.2.x:
+ version "1.2.14"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5"
+ integrity sha1-mofEN48D6CfOyvGs31bHNsAcFOU=
+ dependencies:
+ isexe "^2.0.0"
+
which@^1.2.9:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"