From 6a6012e5d3bbde28af2f66b0ea2b73d796ef48d6 Mon Sep 17 00:00:00 2001 From: "Daniel W. Steinbrook" Date: Thu, 12 Sep 2024 18:20:53 -0400 Subject: [PATCH] "Enter Vicinity Distance" setting (#102) * "Enter Vicinity Distance" setting Adds a setting to configure the distance threshold for stopping beacon audio. Fixes #78 --- apps/ios/GuideDogs.xcodeproj/project.pbxproj | 4 + .../en-GB.lproj/Localizable.strings | 2 + .../en-US.lproj/Localizable.strings | 4 + .../Code/App/Settings/SettingsContext.swift | 28 +++++- .../Default/BeaconCalloutGenerator.swift | 2 +- .../Default/Callouts/DestinationCallout.swift | 2 +- .../Behaviors/Preview/PreviewGenerator.swift | 2 +- .../DestinationManager.swift | 7 +- .../Settings/Beacon/BeaconSelectionView.swift | 17 +++- .../Settings/Beacon/SettingStepper.swift | 91 +++++++++++++++++++ 10 files changed, 147 insertions(+), 12 deletions(-) create mode 100644 apps/ios/GuideDogs/Code/Visual UI/Views/Settings/Beacon/SettingStepper.swift diff --git a/apps/ios/GuideDogs.xcodeproj/project.pbxproj b/apps/ios/GuideDogs.xcodeproj/project.pbxproj index 0308756c..b9058c75 100644 --- a/apps/ios/GuideDogs.xcodeproj/project.pbxproj +++ b/apps/ios/GuideDogs.xcodeproj/project.pbxproj @@ -720,6 +720,7 @@ B9EE98081E3656B7007ADBED /* UIDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9EE98071E3656B7007ADBED /* UIDeviceManager.swift */; }; B9F0F96B1E10A40700F32F70 /* BaseTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9F0F96A1E10A40700F32F70 /* BaseTableViewController.swift */; }; B9F749A21F16153900DC10C6 /* CoreLocation+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9F749A11F16153900DC10C6 /* CoreLocation+Extensions.swift */; }; + BDA963AE2C69483300261EF2 /* SettingStepper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA963AD2C69483300261EF2 /* SettingStepper.swift */; }; C3060705205C544E00C39489 /* AddressGeocoderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3060704205C544E00C39489 /* AddressGeocoderProtocol.swift */; }; C306DF8522F0D71300248E9F /* CompoundFilterPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C306DF8422F0D71300248E9F /* CompoundFilterPredicate.swift */; }; C30DBD2023A2C19400082B27 /* SearchResultsUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = C30DBD1F23A2C19400082B27 /* SearchResultsUpdater.swift */; }; @@ -1648,6 +1649,7 @@ B9EE98071E3656B7007ADBED /* UIDeviceManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UIDeviceManager.swift; path = Code/Devices/UIDeviceManager.swift; sourceTree = ""; }; B9F0F96A1E10A40700F32F70 /* BaseTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BaseTableViewController.swift; path = "Code/Visual UI/View Controllers/BaseTableViewController.swift"; sourceTree = ""; }; B9F749A11F16153900DC10C6 /* CoreLocation+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "CoreLocation+Extensions.swift"; path = "Code/App/Framework Extensions/Geo Extensions/CoreLocation+Extensions.swift"; sourceTree = ""; }; + BDA963AD2C69483300261EF2 /* SettingStepper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingStepper.swift; sourceTree = ""; }; C3060704205C544E00C39489 /* AddressGeocoderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AddressGeocoderProtocol.swift; path = Code/Generators/Geocoding/Protocols/AddressGeocoderProtocol.swift; sourceTree = ""; }; C306DF8422F0D71300248E9F /* CompoundFilterPredicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CompoundFilterPredicate.swift; path = Code/Data/Models/Helpers/Filter/CompoundFilterPredicate.swift; sourceTree = ""; }; C30DBD1F23A2C19400082B27 /* SearchResultsUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsUpdater.swift; sourceTree = ""; }; @@ -2605,6 +2607,7 @@ 2896378326D7099B001694C0 /* Beacon */ = { isa = PBXGroup; children = ( + BDA963AD2C69483300261EF2 /* SettingStepper.swift */, 2832D36526D54DE60052EE47 /* BeaconSelectionView.swift */, 2832D36726D54E0B0052EE47 /* BeaconSelectionHostViewController.swift */, 2896378826D70D3F001694C0 /* BeaconOptionCell.swift */, @@ -5722,6 +5725,7 @@ 62CE14CF25C0DEC3001CBC0B /* HeadphoneCalibration.swift in Sources */, C37E33D52368DBA60033D640 /* NotificationManager.swift in Sources */, C317F26523722ECF000579BA /* NotificationObserver.swift in Sources */, + BDA963AE2C69483300261EF2 /* SettingStepper.swift in Sources */, C317F26923722F1A000579BA /* NotificationObserverDelegate.swift in Sources */, 62B11A3727ACAAC50094FE66 /* RoundedBackground.swift in Sources */, C37E33C423677EC00033D640 /* AlertType.swift in Sources */, diff --git a/apps/ios/GuideDogs/Assets/Localization/en-GB.lproj/Localizable.strings b/apps/ios/GuideDogs/Assets/Localization/en-GB.lproj/Localizable.strings index 9c257c50..7628c488 100644 --- a/apps/ios/GuideDogs/Assets/Localization/en-GB.lproj/Localizable.strings +++ b/apps/ios/GuideDogs/Assets/Localization/en-GB.lproj/Localizable.strings @@ -577,6 +577,8 @@ /* */ "beacon.settings_title" = "Beacon Settings"; +/* */ +"beacon.settings.vicinity" = "Enter Vicinity Distance"; /* */ "beacon.settings.style" = "Audio Styles"; diff --git a/apps/ios/GuideDogs/Assets/Localization/en-US.lproj/Localizable.strings b/apps/ios/GuideDogs/Assets/Localization/en-US.lproj/Localizable.strings index fb7e6a50..4bc110c9 100644 --- a/apps/ios/GuideDogs/Assets/Localization/en-US.lproj/Localizable.strings +++ b/apps/ios/GuideDogs/Assets/Localization/en-US.lproj/Localizable.strings @@ -511,6 +511,10 @@ /* Title of the settings screen for beacons. This screen allows users to configure the way the audio beacon sounds. See Terms for the definition of "beacon". */ "beacon.settings_title" = "Beacon Settings"; + +/* Title of a section in the beacon settings page where the user can select the distance threshold when a beacon automatically stops playing. */ +"beacon.settings.vicinity" = "Enter Vicinity Distance"; + /* Title of a section in the beacon settings page where the user can select a style for their beacon. The various styles mainly differ in the sounds that are played for the beacon, though there are two styles that include some haptics as well. */ "beacon.settings.style" = "Audio Styles"; diff --git a/apps/ios/GuideDogs/Code/App/Settings/SettingsContext.swift b/apps/ios/GuideDogs/Code/App/Settings/SettingsContext.swift index 89066076..9aa4b925 100644 --- a/apps/ios/GuideDogs/Code/App/Settings/SettingsContext.swift +++ b/apps/ios/GuideDogs/Code/App/Settings/SettingsContext.swift @@ -8,6 +8,7 @@ import Foundation import AVFoundation +import CoreLocation extension Notification.Name { static let automaticCalloutsEnabledChanged = Notification.Name("GDAAutomaticCalloutsChanged") @@ -53,6 +54,8 @@ class SettingsContext { fileprivate static let previewIntersectionsIncludeUnnamedRoads = "GDASettingsPreviewIntersectionsIncludeUnnamedRoads" fileprivate static let audioSessionMixesWithOthers = "GDAAudioSessionMixesWithOthers" fileprivate static let markerSortStyle = "GDAMarkerSortStyle" + fileprivate static let leaveImmediateVicinityDistance = "GDALeaveImmediateVicinityDistance" + fileprivate static let enterImmediateVicinityDistance = "GDAEnterImmediateVicinityDistance" fileprivate static let ttsGain = "GDATTSAudioGain" fileprivate static let beaconGain = "GDABeaconAudioGain" @@ -102,7 +105,9 @@ class SettingsContext { Keys.senseDestination: true, Keys.previewIntersectionsIncludeUnnamedRoads: false, Keys.audioSessionMixesWithOthers: true, - Keys.markerSortStyle: SortStyle.distance.rawValue + Keys.markerSortStyle: SortStyle.distance.rawValue, + Keys.leaveImmediateVicinityDistance: 30.0, + Keys.enterImmediateVicinityDistance: 15.0 ]) resetLocaleIfNeeded() @@ -117,7 +122,7 @@ class SettingsContext { } // MARK: Properties - + var appUseCount: Int { get { return userDefaults.integer(forKey: Keys.appUseCount) @@ -315,7 +320,7 @@ class SettingsContext { } // MARK: Push Notifications - + var apnsDeviceToken: Data? { get { return userDefaults.data(forKey: Keys.apnsDeviceToken) @@ -366,6 +371,23 @@ class SettingsContext { } } + var leaveImmediateVicinityDistance: CLLocationDistance { + get { + return userDefaults.double(forKey: Keys.leaveImmediateVicinityDistance) as CLLocationDistance + } + set { + userDefaults.set(newValue, forKey: Keys.leaveImmediateVicinityDistance) + } + } + + var enterImmediateVicinityDistance: CLLocationDistance { + get { + return userDefaults.double(forKey: Keys.enterImmediateVicinityDistance) as CLLocationDistance + } + set { + userDefaults.set(newValue, forKey: Keys.enterImmediateVicinityDistance) + } + } } extension SettingsContext: AutoCalloutSettingsProvider { diff --git a/apps/ios/GuideDogs/Code/Behaviors/Default/BeaconCalloutGenerator.swift b/apps/ios/GuideDogs/Code/Behaviors/Default/BeaconCalloutGenerator.swift index 8f78b964..2d16cac9 100644 --- a/apps/ios/GuideDogs/Code/Behaviors/Default/BeaconCalloutGenerator.swift +++ b/apps/ios/GuideDogs/Code/Behaviors/Default/BeaconCalloutGenerator.swift @@ -281,7 +281,7 @@ class BeaconCalloutGenerator: AutomaticGenerator, ManualGenerator { } // Don't do a location update for the destination if we have already entered the immediate vicinity - guard destination.distanceToClosestLocation(from: location) > DestinationManager.EnterImmediateVicinityDistance else { + guard destination.distanceToClosestLocation(from: location) > SettingsContext.shared.enterImmediateVicinityDistance else { return nil } } diff --git a/apps/ios/GuideDogs/Code/Behaviors/Default/Callouts/DestinationCallout.swift b/apps/ios/GuideDogs/Code/Behaviors/Default/Callouts/DestinationCallout.swift index 57c3fbd5..00180996 100644 --- a/apps/ios/GuideDogs/Code/Behaviors/Default/Callouts/DestinationCallout.swift +++ b/apps/ios/GuideDogs/Code/Behaviors/Default/Callouts/DestinationCallout.swift @@ -86,7 +86,7 @@ struct DestinationCallout: POICalloutProtocol { return Sounds(sounds) case .beaconGeofence: - let formattedDistance = LanguageFormatter.formattedDistance(from: DestinationManager.EnterImmediateVicinityDistance) + let formattedDistance = LanguageFormatter.formattedDistance(from: SettingsContext.shared.enterImmediateVicinityDistance) // Inform the user why the audio beacon has stopped if causedAudioDisabled { diff --git a/apps/ios/GuideDogs/Code/Behaviors/Preview/PreviewGenerator.swift b/apps/ios/GuideDogs/Code/Behaviors/Preview/PreviewGenerator.swift index a3565674..63478ee6 100644 --- a/apps/ios/GuideDogs/Code/Behaviors/Preview/PreviewGenerator.swift +++ b/apps/ios/GuideDogs/Code/Behaviors/Preview/PreviewGenerator.swift @@ -103,7 +103,7 @@ struct PreviewGenerator: ManualGenerator { var callouts: [CalloutProtocol] = [] if event.arrived { - let formattedDistance = LanguageFormatter.formattedDistance(from: DestinationManager.EnterImmediateVicinityDistance) + let formattedDistance = LanguageFormatter.formattedDistance(from: SettingsContext.shared.enterImmediateVicinityDistance) callouts.append(GenericCallout(.preview, description: "arrived at beacon (in preview)") { (_, _, _) -> [Sound] in let earcon = GlyphSound(.beaconFound) diff --git a/apps/ios/GuideDogs/Code/Data/Destination Manager/DestinationManager.swift b/apps/ios/GuideDogs/Code/Data/Destination Manager/DestinationManager.swift index ba599904..89bc159d 100644 --- a/apps/ios/GuideDogs/Code/Data/Destination Manager/DestinationManager.swift +++ b/apps/ios/GuideDogs/Code/Data/Destination Manager/DestinationManager.swift @@ -25,9 +25,6 @@ enum DestinationManagerError: Error { class DestinationManager: DestinationManagerProtocol { - static let LeaveImmediateVicinityDistance: CLLocationDistance = 30.0 - static let EnterImmediateVicinityDistance: CLLocationDistance = 15.0 - // MARK: Notification Keys struct Keys { @@ -614,10 +611,10 @@ class DestinationManager: DestinationManagerProtocol { let distance = origin.distanceToClosestLocation(from: location) - if isWithinGeofence && distance >= DestinationManager.LeaveImmediateVicinityDistance { + if isWithinGeofence && distance >= SettingsContext.shared.leaveImmediateVicinityDistance { // Left immediate vicinity return false - } else if !isWithinGeofence && distance <= DestinationManager.EnterImmediateVicinityDistance { + } else if !isWithinGeofence && distance <= SettingsContext.shared.enterImmediateVicinityDistance { // Entered immediate vicinity return true } diff --git a/apps/ios/GuideDogs/Code/Visual UI/Views/Settings/Beacon/BeaconSelectionView.swift b/apps/ios/GuideDogs/Code/Visual UI/Views/Settings/Beacon/BeaconSelectionView.swift index 81aa9905..eefcce2b 100644 --- a/apps/ios/GuideDogs/Code/Visual UI/Views/Settings/Beacon/BeaconSelectionView.swift +++ b/apps/ios/GuideDogs/Code/Visual UI/Views/Settings/Beacon/BeaconSelectionView.swift @@ -15,6 +15,7 @@ struct BeaconSelectionView: View { @State var isPresented: Bool = false @State var selectedBeaconKey: String @State var areMelodiesEnabled: Bool + @State var enterImmediateVicinityDistance: Double let initialBeacon: String let initialMelodies: Bool @@ -22,6 +23,7 @@ struct BeaconSelectionView: View { init() { _selectedBeaconKey = State(initialValue: SettingsContext.shared.selectedBeacon) _areMelodiesEnabled = State(initialValue: SettingsContext.shared.playBeaconStartAndEndMelodies) + _enterImmediateVicinityDistance = State(initialValue: SettingsContext.shared.enterImmediateVicinityDistance) initialBeacon = SettingsContext.shared.selectedBeacon initialMelodies = SettingsContext.shared.playBeaconStartAndEndMelodies } @@ -52,7 +54,20 @@ struct BeaconSelectionView: View { SettingsContext.shared.playBeaconStartAndEndMelodies = areMelodiesEnabled beaconDemo.play(styleChanged: true) }) - + + TableHeaderCell(text: GDLocalizedString("beacon.settings.vicinity")) + + SettingStepper( + value: $enterImmediateVicinityDistance, + unitsLocalization: "distance.format.meters", + stepSize: 5.0, + minValue: 0.0, + maxValue: 50.0 + ) + .onChange(of: enterImmediateVicinityDistance, perform: { _ in + SettingsContext.shared.enterImmediateVicinityDistance = enterImmediateVicinityDistance + }) + TableHeaderCell(text: GDLocalizedString("beacon.settings.style")) ForEach(BeaconOption.allAvailableCases(for: .standard)) { details in diff --git a/apps/ios/GuideDogs/Code/Visual UI/Views/Settings/Beacon/SettingStepper.swift b/apps/ios/GuideDogs/Code/Visual UI/Views/Settings/Beacon/SettingStepper.swift new file mode 100644 index 00000000..797a2b7d --- /dev/null +++ b/apps/ios/GuideDogs/Code/Visual UI/Views/Settings/Beacon/SettingStepper.swift @@ -0,0 +1,91 @@ +// +// SettingStepper.swift +// Soundscape +// +// Created by Daniel W. Steinbrook on 8/11/24. +// Copyright © 2024 Soundscape community. All rights reserved. +// + +import SwiftUI + +/// Defines a stepper (increment/decrement buttons) that can be used for settings like Enter Vicinity Distance. +/// Takes a step size, min, max, and localization key for printing the value with units.. +/// `unitsLocalization` should be a localization key like "distance.format.meters". +struct SettingStepper: View { + @Binding var value: Double + private let unitsLocalization: String + private let stepSize: Double + private let minValue: Double + private let maxValue: Double + + init(value: Binding, unitsLocalization: String, stepSize: Double, minValue: Double, maxValue: Double) { + self._value = value + self.unitsLocalization = unitsLocalization + self.stepSize = stepSize + self.minValue = minValue + self.maxValue = maxValue + } + + // Increment and Decrement actions + private func increment() { + let newValue = value + stepSize + value = min(max(newValue, minValue), maxValue) + } + + private func decrement() { + let newValue = value - stepSize + value = min(max(newValue, minValue), maxValue) + } + + var body: some View { + VStack { + /// We don't use the native `Stepper` because the increment/decrement + /// controls can't be styled, and the defaults are low contrast. + HStack { + // truncate `value` at the decimal point + Text(GDLocalizedString(unitsLocalization, String(format: "%.0f", value))) + .foregroundColor(.primaryForeground) + .font(.body) + .lineLimit(nil) + + Spacer() + + Button(action: decrement) { + Text("-") + .font(.title) + .frame(width: 44, height: 30) + .background(Color.gray.opacity(0.3)) + .foregroundColor(.white) + .cornerRadius(8) + } + .accessibilityLabel(Text("Decrease")) + .disabled(value <= self.minValue) + + Button(action: increment) { + Text("+") + .font(.title) + .frame(width: 44, height: 30) + .background(Color.gray.opacity(0.3)) + .foregroundColor(.white) + .cornerRadius(8) + } + .accessibilityLabel(Text("Increase")) + .disabled(value >= self.maxValue) + } + .padding() + .background(Color.primaryBackground) + .accessibilityElement(children: .ignore) + .accessibilityValue(GDLocalizedString(unitsLocalization, String(format: "%.0f", value))) + .accessibilityAdjustableAction { direction in + switch direction { + case .increment: + increment() + case .decrement: + decrement() + @unknown default: + break + } + } + } + } +}