diff --git a/apps/ios/GuideDogs.xcodeproj/project.pbxproj b/apps/ios/GuideDogs.xcodeproj/project.pbxproj index a0f47a49..72fddf30 100644 --- a/apps/ios/GuideDogs.xcodeproj/project.pbxproj +++ b/apps/ios/GuideDogs.xcodeproj/project.pbxproj @@ -719,6 +719,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 */; }; @@ -1646,6 +1647,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 = ""; }; @@ -2603,6 +2605,7 @@ 2896378326D7099B001694C0 /* Beacon */ = { isa = PBXGroup; children = ( + BDA963AD2C69483300261EF2 /* SettingStepper.swift */, 2832D36526D54DE60052EE47 /* BeaconSelectionView.swift */, 2832D36726D54E0B0052EE47 /* BeaconSelectionHostViewController.swift */, 2896378826D70D3F001694C0 /* BeaconOptionCell.swift */, @@ -5718,6 +5721,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 eaaa9b43..3602436d 100644 Binary files a/apps/ios/GuideDogs/Assets/Localization/en-GB.lproj/Localizable.strings and b/apps/ios/GuideDogs/Assets/Localization/en-GB.lproj/Localizable.strings differ 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 a95174bc..4a3aceb5 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/Data/Destination Manager/DestinationManager.swift b/apps/ios/GuideDogs/Code/Data/Destination Manager/DestinationManager.swift index ba599904..15f72de3 100644 --- a/apps/ios/GuideDogs/Code/Data/Destination Manager/DestinationManager.swift +++ b/apps/ios/GuideDogs/Code/Data/Destination Manager/DestinationManager.swift @@ -25,8 +25,8 @@ enum DestinationManagerError: Error { class DestinationManager: DestinationManagerProtocol { - static let LeaveImmediateVicinityDistance: CLLocationDistance = 30.0 - static let EnterImmediateVicinityDistance: CLLocationDistance = 15.0 + static let LeaveImmediateVicinityDistance: CLLocationDistance = SettingsContext.shared.leaveImmediateVicinityDistance + static let EnterImmediateVicinityDistance: CLLocationDistance = SettingsContext.shared.enterImmediateVicinityDistance // MARK: Notification Keys 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..aa1c03b3 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: 5.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..653ac75b --- /dev/null +++ b/apps/ios/GuideDogs/Code/Visual UI/Views/Settings/Beacon/SettingStepper.swift @@ -0,0 +1,56 @@ +// +// 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 { + Stepper( + onIncrement: increment, + onDecrement: decrement + ) { + // truncate `value` at the decimal point + Text(GDLocalizedString(unitsLocalization, String(format: "%.0f", value))) + .foregroundColor(.primaryForeground) + .font(.body) + .lineLimit(nil) + } + .padding() + .background(Color.primaryBackground) + } + } +}