mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-01-31 00:45:24 +01:00
* chore(ios): consolidate optional argument standard mark all optional iOS arguments as `var <name>: Type?`, following the pattern described in the documentation: https://v2.tauri.app/develop/plugins/develop-mobile/#ios * chore: add missing Info.plist to example
301 lines
8.2 KiB
Swift
301 lines
8.2 KiB
Swift
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
import AVFoundation
|
|
import Tauri
|
|
import UIKit
|
|
import WebKit
|
|
|
|
struct ScanOptions: Decodable {
|
|
var formats: [SupportedFormat]?
|
|
var windowed: Bool?
|
|
var cameraDirection: String?
|
|
}
|
|
|
|
enum SupportedFormat: String, CaseIterable, Decodable {
|
|
// UPC_A not supported
|
|
case UPC_E
|
|
case EAN_8
|
|
case EAN_13
|
|
case CODE_39
|
|
case CODE_93
|
|
case CODE_128
|
|
// CODABAR not supported
|
|
case ITF
|
|
case AZTEC
|
|
case DATA_MATRIX
|
|
case PDF_417
|
|
case QR_CODE
|
|
|
|
var value: AVMetadataObject.ObjectType {
|
|
switch self {
|
|
case .UPC_E: return AVMetadataObject.ObjectType.upce
|
|
case .EAN_8: return AVMetadataObject.ObjectType.ean8
|
|
case .EAN_13: return AVMetadataObject.ObjectType.ean13
|
|
case .CODE_39: return AVMetadataObject.ObjectType.code39
|
|
case .CODE_93: return AVMetadataObject.ObjectType.code93
|
|
case .CODE_128: return AVMetadataObject.ObjectType.code128
|
|
case .ITF: return AVMetadataObject.ObjectType.interleaved2of5
|
|
case .AZTEC: return AVMetadataObject.ObjectType.aztec
|
|
case .DATA_MATRIX: return AVMetadataObject.ObjectType.dataMatrix
|
|
case .PDF_417: return AVMetadataObject.ObjectType.pdf417
|
|
case .QR_CODE: return AVMetadataObject.ObjectType.qr
|
|
}
|
|
}
|
|
}
|
|
|
|
enum CaptureError: Error {
|
|
case backCameraUnavailable
|
|
case frontCameraUnavailable
|
|
case couldNotCaptureInput(error: NSError)
|
|
}
|
|
|
|
class BarcodeScannerPlugin: Plugin, AVCaptureMetadataOutputObjectsDelegate {
|
|
var webView: WKWebView!
|
|
var cameraView: CameraView!
|
|
var captureSession: AVCaptureSession?
|
|
var captureVideoPreviewLayer: AVCaptureVideoPreviewLayer?
|
|
var metaOutput: AVCaptureMetadataOutput?
|
|
|
|
var currentCamera = 0
|
|
var frontCamera: AVCaptureDevice?
|
|
var backCamera: AVCaptureDevice?
|
|
|
|
var isScanning = false
|
|
|
|
var windowed = false
|
|
var previousBackgroundColor: UIColor? = UIColor.white
|
|
|
|
var invoke: Invoke? = nil
|
|
|
|
var scanFormats = [AVMetadataObject.ObjectType]()
|
|
|
|
public override func load(webview: WKWebView) {
|
|
self.webView = webview
|
|
loadCamera()
|
|
}
|
|
|
|
private func loadCamera() {
|
|
cameraView = CameraView(
|
|
frame: CGRect(
|
|
x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
|
|
cameraView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
}
|
|
|
|
public func metadataOutput(
|
|
_ captureOutput: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject],
|
|
from connection: AVCaptureConnection
|
|
) {
|
|
if metadataObjects.count == 0 || !self.isScanning {
|
|
// while nothing is detected, or if scanning is false, do nothing.
|
|
return
|
|
}
|
|
|
|
let found = metadataObjects[0] as! AVMetadataMachineReadableCodeObject
|
|
if scanFormats.contains(found.type) {
|
|
var jsObject: JsonObject = [:]
|
|
|
|
jsObject["format"] = formatStringFromMetadata(found.type)
|
|
if found.stringValue != nil {
|
|
jsObject["content"] = found.stringValue
|
|
}
|
|
|
|
invoke?.resolve(jsObject)
|
|
destroy()
|
|
|
|
}
|
|
}
|
|
|
|
private func setupCamera(direction: String, windowed: Bool) {
|
|
do {
|
|
var cameraDirection = direction
|
|
cameraView.backgroundColor = UIColor.clear
|
|
if windowed {
|
|
webView.superview?.insertSubview(cameraView, belowSubview: webView)
|
|
} else {
|
|
webView.superview?.insertSubview(cameraView, aboveSubview: webView)
|
|
}
|
|
|
|
let availableVideoDevices = discoverCaptureDevices()
|
|
for device in availableVideoDevices {
|
|
if device.position == AVCaptureDevice.Position.back {
|
|
backCamera = device
|
|
} else if device.position == AVCaptureDevice.Position.front {
|
|
frontCamera = device
|
|
}
|
|
}
|
|
|
|
// older iPods have no back camera
|
|
if cameraDirection == "back" {
|
|
if backCamera == nil {
|
|
cameraDirection = "front"
|
|
}
|
|
} else {
|
|
if frontCamera == nil {
|
|
cameraDirection = "back"
|
|
}
|
|
}
|
|
|
|
let input: AVCaptureDeviceInput
|
|
input = try createCaptureDeviceInput(
|
|
cameraDirection: cameraDirection, backCamera: backCamera, frontCamera: frontCamera)
|
|
captureSession = AVCaptureSession()
|
|
captureSession!.addInput(input)
|
|
metaOutput = AVCaptureMetadataOutput()
|
|
captureSession!.addOutput(metaOutput!)
|
|
metaOutput!.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
|
|
captureVideoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession!)
|
|
cameraView.addPreviewLayer(captureVideoPreviewLayer)
|
|
|
|
self.windowed = windowed
|
|
if windowed {
|
|
self.previousBackgroundColor = self.webView.backgroundColor
|
|
self.webView.isOpaque = false
|
|
self.webView.backgroundColor = UIColor.clear
|
|
self.webView.scrollView.backgroundColor = UIColor.clear
|
|
}
|
|
} catch CaptureError.backCameraUnavailable {
|
|
//
|
|
} catch CaptureError.frontCameraUnavailable {
|
|
//
|
|
} catch CaptureError.couldNotCaptureInput {
|
|
//
|
|
} catch {
|
|
//
|
|
}
|
|
}
|
|
|
|
private func dismantleCamera() {
|
|
if self.captureSession != nil {
|
|
self.captureSession!.stopRunning()
|
|
self.cameraView.removePreviewLayer()
|
|
self.captureVideoPreviewLayer = nil
|
|
self.metaOutput = nil
|
|
self.captureSession = nil
|
|
self.frontCamera = nil
|
|
self.backCamera = nil
|
|
}
|
|
|
|
self.isScanning = false
|
|
}
|
|
|
|
private func destroy() {
|
|
dismantleCamera()
|
|
invoke = nil
|
|
if windowed {
|
|
let backgroundColor = previousBackgroundColor ?? UIColor.white
|
|
webView.isOpaque = true
|
|
webView.backgroundColor = backgroundColor
|
|
webView.scrollView.backgroundColor = backgroundColor
|
|
}
|
|
}
|
|
|
|
private func getPermissionState() -> String {
|
|
var permissionState: String
|
|
|
|
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
|
case .authorized:
|
|
permissionState = "granted"
|
|
case .denied:
|
|
permissionState = "denied"
|
|
default:
|
|
permissionState = "prompt"
|
|
}
|
|
|
|
return permissionState
|
|
}
|
|
|
|
@objc override func checkPermissions(_ invoke: Invoke) {
|
|
let permissionState = getPermissionState()
|
|
invoke.resolve(["camera": permissionState])
|
|
}
|
|
|
|
@objc override func requestPermissions(_ invoke: Invoke) {
|
|
let state = getPermissionState()
|
|
if state == "prompt" {
|
|
AVCaptureDevice.requestAccess(for: .video) { (authorized) in
|
|
invoke.resolve(["camera": authorized ? "granted" : "denied"])
|
|
}
|
|
} else {
|
|
invoke.resolve(["camera": state])
|
|
}
|
|
}
|
|
|
|
@objc func openAppSettings(_ invoke: Invoke) {
|
|
guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else {
|
|
return
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
if UIApplication.shared.canOpenURL(settingsUrl) {
|
|
UIApplication.shared.open(
|
|
settingsUrl,
|
|
completionHandler: { (success) in
|
|
invoke.resolve()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
private func runScanner(_ invoke: Invoke, args: ScanOptions) {
|
|
scanFormats = [AVMetadataObject.ObjectType]()
|
|
|
|
(args.formats ?? []).forEach { format in
|
|
scanFormats.append(format.value)
|
|
}
|
|
|
|
if scanFormats.count == 0 {
|
|
for supportedFormat in SupportedFormat.allCases {
|
|
scanFormats.append(supportedFormat.value)
|
|
}
|
|
}
|
|
|
|
self.metaOutput!.metadataObjectTypes = self.scanFormats
|
|
self.captureSession!.startRunning()
|
|
|
|
self.isScanning = true
|
|
}
|
|
|
|
@objc private func scan(_ invoke: Invoke) throws {
|
|
let args = try invoke.parseArgs(ScanOptions.self)
|
|
|
|
self.invoke = invoke
|
|
|
|
var iOS14min: Bool = false
|
|
if #available(iOS 14.0, *) { iOS14min = true }
|
|
if !iOS14min && self.getPermissionState() != "granted" {
|
|
var authorized = false
|
|
AVCaptureDevice.requestAccess(for: .video) { (isAuthorized) in
|
|
authorized = isAuthorized
|
|
}
|
|
if !authorized {
|
|
invoke.reject("denied by the user")
|
|
return
|
|
}
|
|
}
|
|
|
|
DispatchQueue.main.async { [self] in
|
|
self.loadCamera()
|
|
self.dismantleCamera()
|
|
self.setupCamera(
|
|
direction: args.cameraDirection ?? "back",
|
|
windowed: args.windowed ?? false
|
|
)
|
|
self.runScanner(invoke, args: args)
|
|
}
|
|
}
|
|
|
|
@objc private func cancel(_ invoke: Invoke) {
|
|
self.invoke?.reject("cancelled")
|
|
|
|
destroy()
|
|
invoke.resolve()
|
|
}
|
|
}
|
|
|
|
@_cdecl("init_plugin_barcode_scanner")
|
|
func initPlugin() -> Plugin {
|
|
return BarcodeScannerPlugin()
|
|
}
|