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 all 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
91 changes: 91 additions & 0 deletions Sources/ArcGISToolkit/Common/CameraRequester.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// 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 AVFoundation
import SwiftUI

/// Performs camera authorization request handling.
///
/// Ensures that access is granted before launching the system camera.
@MainActor final class CameraRequester: ObservableObject {
@Published var alertIsPresented = false

var onAccessDenied: (() -> Void)?

func request(onAccessGranted: @escaping () -> Void, onAccessDenied: @escaping () -> Void) {
self.onAccessDenied = onAccessDenied
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
onAccessGranted()
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { granted in
if granted {
onAccessGranted()
} else {
Task { @MainActor in
self.alertIsPresented = true
}
}
}
default:
alertIsPresented = true
}
}
}

private struct CameraRequesterModifier: ViewModifier {
@ObservedObject var requester: CameraRequester

func body(content: Content) -> some View {
content
.alert(cameraAccessAlertTitle, isPresented: $requester.alertIsPresented) {
#if !targetEnvironment(macCatalyst)
Button(String.settings) {
Task { await UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) }
}
#endif
Button(String.cancel, role: .cancel) {
requester.onAccessDenied?()
}
} message: {
Text(cameraAccessAlertMessage)
}
}
}

extension View {
func cameraRequester(_ requester: CameraRequester) -> some View {
modifier(CameraRequesterModifier(requester: requester))
}
}

private extension CameraRequesterModifier {
/// A message for an alert requesting camera access.
var cameraAccessAlertMessage: String {
.init(
localized: "Please enable camera access in settings.",
bundle: .toolkitModule,
comment: "A message for an alert requesting camera access."
)
}

/// A title for an alert that camera access is disabled.
var cameraAccessAlertTitle: String {
.init(
localized: "Camera access is disabled",
bundle: .toolkitModule,
comment: "A title for an alert that camera access is disabled."
)
}
}
61 changes: 61 additions & 0 deletions Sources/ArcGISToolkit/Common/FlashlightButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// 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 AVFoundation
import SwiftUI

struct FlashlightButton: View {
@State private var flashIsOn = false

var device: AVCaptureDevice? {
.default(for: .video)
}

var hasTorch: Bool {
device?.hasTorch ?? false
}

var body: some View {
Button {
flashIsOn.toggle()
} label: {
Group {
if !hasTorch {
Image(systemName: "flashlight.slash")
} else if #available(iOS 17, *) {
Image(systemName: flashIsOn ? "flashlight.on.fill" : "flashlight.off.fill")
.contentTransition(.symbolEffect(.replace))
} else {
Image(systemName: flashIsOn ? "flashlight.on.fill" : "flashlight.off.fill")
}
}
.padding()
.background(.regularMaterial)
.clipShape(Circle())
}
.disabled(!hasTorch)
.onDisappear {
flashIsOn = false
}
.onChange(of: flashIsOn) { isOn in
try? device?.lockForConfiguration()
device?.torchMode = isOn ? .on : .off
device?.unlockForConfiguration()
}
}
}

#Preview {
FlashlightButton()
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@ struct AttachmentImportMenu: View {
self.onAdd = onAdd
}

/// A Boolean value indicating whether the camera access alert is presented.
@State private var cameraAccessAlertIsPresented = false

/// A Boolean value indicating whether the attachment camera controller is presented.
@State private var cameraIsShowing = false

Expand All @@ -51,6 +48,9 @@ struct AttachmentImportMenu: View {
/// A Boolean value indicating whether the attachment photo picker is presented.
@State private var photoPickerIsPresented = false

/// Performs camera authorization request handling.
@StateObject private var cameraRequester = CameraRequester()

/// The maximum attachment size limit.
let attachmentUploadSizeLimit = Measurement(
value: 50,
Expand All @@ -73,18 +73,9 @@ struct AttachmentImportMenu: View {

private func takePhotoOrVideoButton() -> Button<some View> {
Button {
if AVCaptureDevice.authorizationStatus(for: .video) == .authorized {
cameraRequester.request {
cameraIsShowing = true
} else {
Task {
let granted = await AVCaptureDevice.requestAccess(for: .video)
if granted {
cameraIsShowing = true
} else {
cameraAccessAlertIsPresented = true
}
}
}
} onAccessDenied: { }
} label: {
Text(cameraButtonLabel)
Image(systemName: "camera")
Expand Down Expand Up @@ -127,14 +118,7 @@ struct AttachmentImportMenu: View {
.padding(5)
}
.disabled(importState.importInProgress)
.alert(cameraAccessAlertTitle, isPresented: $cameraAccessAlertIsPresented) {
#if !targetEnvironment(macCatalyst)
appSettingsButton
#endif
Button(String.cancel, role: .cancel) { }
} message: {
Text(cameraAccessAlertMessage)
}
.cameraRequester(cameraRequester)
.alert(importFailureAlertTitle, isPresented: errorIsPresented) { } message: {
Text(importFailureAlertMessage)
}
Expand Down Expand Up @@ -232,24 +216,6 @@ private extension AttachmentImportMenu {
}
}

/// A message for an alert requesting camera access.
var cameraAccessAlertMessage: String {
.init(
localized: "Please enable camera access in settings.",
bundle: .toolkitModule,
comment: "A message for an alert requesting camera access."
)
}

/// A title for an alert that camera access is disabled.
var cameraAccessAlertTitle: String {
.init(
localized: "Camera access is disabled",
bundle: .toolkitModule,
comment: "A title for an alert that camera access is disabled."
)
}

/// A label for a button to capture a new photo or video.
var cameraButtonLabel: String {
.init(
Expand Down
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
Loading