Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FeatureFormView - BarcodeScannerFormInput support #893

Open
wants to merge 37 commits into
base: v.next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6068189
Stop excluding barcode
dfeinzimer Sep 20, 2024
7f72da3
Add barcode switch case
dfeinzimer Sep 20, 2024
64d503e
Create BarcodeScannerInput.swift
dfeinzimer Sep 20, 2024
b3449e3
Merge branch 'v.next' into df/apollo/866
dfeinzimer Sep 25, 2024
4d6bd0d
Update BarcodeScannerInput.swift
dfeinzimer Sep 25, 2024
14c2b91
Update BarcodeScannerInput.swift
dfeinzimer Sep 25, 2024
f888e75
Update BarcodeScannerInput.swift
dfeinzimer Sep 25, 2024
b42c736
Update BarcodeScannerInput.swift
dfeinzimer Sep 25, 2024
5bd34a4
Use convertAndUpdateValue
dfeinzimer Sep 25, 2024
7bb1b7a
Update BarcodeScannerInput.swift
dfeinzimer Sep 26, 2024
e2f23ce
Update BarcodeScannerInput.swift
dfeinzimer Sep 26, 2024
492fa1c
Truncate barcode input to a single line
dfeinzimer Sep 26, 2024
03e2dba
Alphabetize `ScannerView` members
dfeinzimer Sep 26, 2024
9a5089d
Update BarcodeScannerInput.swift
dfeinzimer Sep 26, 2024
55f3d68
Add new static localized string vars
dfeinzimer Sep 26, 2024
7bfa606
Update BarcodeScannerInput.swift
dfeinzimer Sep 26, 2024
dbab4b2
Add `ScannerViewController.SessionSetupResult`
dfeinzimer Sep 26, 2024
07374d0
Add `ScannerViewController.setupResult`, camera auth check
dfeinzimer Sep 26, 2024
693ebb6
Remove `AttachmentImportMenu` strings
dfeinzimer Sep 26, 2024
00cae0e
Update BarcodeScannerInput.swift
dfeinzimer Sep 26, 2024
7ce704c
Rework `viewWillAppear` for setup status
dfeinzimer Sep 26, 2024
313d81a
Add `CameraRequester`
dfeinzimer Sep 27, 2024
77be638
Use CameraRequester where relevant
dfeinzimer Sep 27, 2024
65679c5
Update BarcodeScannerInput.swift
dfeinzimer Sep 27, 2024
eff7e49
Update String.swift
dfeinzimer Sep 27, 2024
6d8d2da
Cleanup, resolve runtime warnings
dfeinzimer Sep 27, 2024
1a1d641
Update CameraRequester.swift
dfeinzimer Sep 27, 2024
f76e76c
Update CameraRequester.swift
dfeinzimer Sep 27, 2024
7954299
Implement `TextInput` fallback for unavailable camera
dfeinzimer Sep 27, 2024
e375f71
Create FlashlightButton.swift
dfeinzimer Sep 27, 2024
972fe3d
Record focus
dfeinzimer Sep 27, 2024
b0483a2
Update FlashlightButton.swift
dfeinzimer Sep 27, 2024
12cf47a
Add cancel and torch control
dfeinzimer Sep 27, 2024
4a4d20a
Support barcode selection
dfeinzimer Sep 27, 2024
f2f4d10
Update Localizable.strings
dfeinzimer Sep 27, 2024
ddbb864
Disable scanning on simulators
dfeinzimer Sep 27, 2024
9f5c401
Use .secondary for scanner icon
dfeinzimer Sep 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,7 @@ extension FeatureFormView {
/// Makes UI for a field form element including a divider beneath it.
/// - Parameter element: The element to generate UI for.
@ViewBuilder func makeFieldElement(_ element: FieldFormElement) -> some View {
if !(element.input is UnsupportedFormInput ||
element.input is BarcodeScannerFormInput) {
if !(element.input is UnsupportedFormInput) {
InputWrapper(element: element)
Divider()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// Copyright 2024 Esri
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import ArcGIS
@preconcurrency import AVFoundation
import SwiftUI

struct BarcodeScannerInput: View {
/// The element the input belongs to.
private let element: FieldFormElement

/// The input configuration of the field.
private let input: BarcodeScannerFormInput

/// A Boolean value indicating whether the code scanner is presented.
@State private var scannerIsPresented = false

/// The current barcode value.
@State private var value = ""

/// Creates a view for a barcode scanner input.
/// - Parameters:
/// - element: The input's parent element.
init(element: FieldFormElement) {
precondition(
element.input is BarcodeScannerFormInput,
"\(Self.self).\(#function) element's input must be \(BarcodeScannerFormInput.self)."
)
self.element = element
self.input = element.input as! BarcodeScannerFormInput
}

var body: some View {
HStack {
Text(value.isEmpty ? String.noValue : value)
Spacer()
if !value.isEmpty {
ClearButton {
value.removeAll()
}
}
Image(systemName: "barcode.viewfinder")
.foregroundStyle(.tint)
dfeinzimer marked this conversation as resolved.
Show resolved Hide resolved
}
.frame(maxWidth: .infinity, alignment: .leading)
.formInputStyle()
.onTapGesture {
scannerIsPresented = true
}
.sheet(isPresented: $scannerIsPresented) {
ScannerView(scanOutput: $value, scannerIsPresented: $scannerIsPresented)
}
}
}

struct ScannerView: UIViewControllerRepresentable {
@Binding var scanOutput: String
@Binding var scannerIsPresented: Bool

func makeUIViewController(context: Context) -> ScannerViewController {
let scannerViewController = ScannerViewController()
scannerViewController.delegate = context.coordinator
return scannerViewController
}

func updateUIViewController(_ uiViewController: ScannerViewController, context: Context) {}

func makeCoordinator() -> Coordinator {
Coordinator(scannedCode: $scanOutput, isShowingScanner: $scannerIsPresented)
}

class Coordinator: NSObject, ScannerViewControllerDelegate {
@Binding var scanOutput: String
@Binding var scannerIsPresented: Bool

init(scannedCode: Binding<String>, isShowingScanner: Binding<Bool>) {
_scanOutput = scannedCode
_scannerIsPresented = isShowingScanner
}

func didScanCode(_ code: String) {
scanOutput = code
scannerIsPresented = false
}
}
}

protocol ScannerViewControllerDelegate: AnyObject {
func didScanCode(_ code: String)
}

class ScannerViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
weak var delegate: ScannerViewControllerDelegate?
private let captureSession = AVCaptureSession()
private var previewLayer: AVCaptureVideoPreviewLayer!

private let sessionQueue = DispatchQueue(label: "ScannerViewController")

override func viewDidLoad() {
super.viewDidLoad()

guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { return }
let videoInput: AVCaptureDeviceInput

do {
videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
} catch {
return
}

if captureSession.canAddInput(videoInput) {
captureSession.addInput(videoInput)
} else {
return
}

let metadataOutput = AVCaptureMetadataOutput()

if captureSession.canAddOutput(metadataOutput) {
captureSession.addOutput(metadataOutput)

metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
metadataOutput.metadataObjectTypes = [
// Barcodes
.codabar,
.code39,
.code39Mod43,
.code93,
.code128,
.ean8,
.ean13,
.gs1DataBar,
.gs1DataBarExpanded,
.gs1DataBarLimited,
.interleaved2of5,
.itf14,
.upce,

// 2D Codes
.aztec,
.dataMatrix,
.microPDF417,
.microQR,
.pdf417,
.qr,
]
} else {
return
}

previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer.frame = view.layer.bounds
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

if captureSession.isRunning == false {
sessionQueue.async { [captureSession] in
captureSession.startRunning()
}
}
}

func finish(stringValue: String) {
captureSession.stopRunning()
delegate?.didScanCode(stringValue)
}

nonisolated func metadataOutput(
_ output: AVCaptureMetadataOutput,
didOutput metadataObjects: [AVMetadataObject],
from connection: AVCaptureConnection
) {
if let metadataObject = metadataObjects.first {
guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
guard let stringValue = readableObject.stringValue else { return }
Task {
await finish(stringValue: stringValue)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ struct InputWrapper: View {
InputHeader(element: element)
if isEditable {
switch element.input {
case is BarcodeScannerFormInput:
BarcodeScannerInput(element: element)
case is ComboBoxFormInput:
ComboBoxInput(element: element)
case is DateTimePickerFormInput:
Expand Down