diff --git a/apps/ios/GuideDogs.xcodeproj/project.pbxproj b/apps/ios/GuideDogs.xcodeproj/project.pbxproj index c55f55c7..732e4960 100644 --- a/apps/ios/GuideDogs.xcodeproj/project.pbxproj +++ b/apps/ios/GuideDogs.xcodeproj/project.pbxproj @@ -662,6 +662,9 @@ 62F7A30C27B6080900C62390 /* InteractiveBeaconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F7A30B27B6080900C62390 /* InteractiveBeaconView.swift */; }; 62F7A30E27B6082A00C62390 /* InteractiveBeaconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F7A30D27B6082A00C62390 /* InteractiveBeaconViewModel.swift */; }; 6A4891BB2A5E66DE0002D146 /* ExternalNavigationApps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4891BA2A5E66DE0002D146 /* ExternalNavigationApps.swift */; }; + 7FB733C82CE555D500C99D46 /* GeneralSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB733C72CE555D500C99D46 /* GeneralSettingsViewController.swift */; }; + 7FB733CC2CE5570A00C99D46 /* CalloutSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB733CB2CE5570A00C99D46 /* CalloutSettingsViewController.swift */; }; + 7FB733D02CE5575400C99D46 /* TroubleshootingSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB733CF2CE5575400C99D46 /* TroubleshootingSettingsViewController.swift */; }; 91172A732AD8D56D00E6E8E9 /* CoreGPX in Frameworks */ = {isa = PBXBuildFile; productRef = 91172A722AD8D56D00E6E8E9 /* CoreGPX */; }; 914BAAF32AD745E400CB2171 /* DestinationManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914BAAF22AD745E400CB2171 /* DestinationManagerTest.swift */; }; 914BAAFD2AD7483300CB2171 /* AudioEngineTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914BAAFC2AD7483300CB2171 /* AudioEngineTest.swift */; }; @@ -1582,6 +1585,9 @@ 62F7A30B27B6080900C62390 /* InteractiveBeaconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveBeaconView.swift; sourceTree = ""; }; 62F7A30D27B6082A00C62390 /* InteractiveBeaconViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveBeaconViewModel.swift; sourceTree = ""; }; 6A4891BA2A5E66DE0002D146 /* ExternalNavigationApps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ExternalNavigationApps.swift; path = Code/App/ExternalNavigationApps.swift; sourceTree = ""; }; + 7FB733C72CE555D500C99D46 /* GeneralSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsViewController.swift; sourceTree = ""; }; + 7FB733CB2CE5570A00C99D46 /* CalloutSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalloutSettingsViewController.swift; sourceTree = ""; }; + 7FB733CF2CE5575400C99D46 /* TroubleshootingSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TroubleshootingSettingsViewController.swift; sourceTree = ""; }; 914BAAF22AD745E400CB2171 /* DestinationManagerTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DestinationManagerTest.swift; sourceTree = ""; }; 914BAAFC2AD7483300CB2171 /* AudioEngineTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioEngineTest.swift; sourceTree = ""; }; 914DEBCD2A3CE6B9007B161C /* UnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1902,6 +1908,9 @@ 287D3E8C22DE50340084B92B /* StatusTableViewController.swift */, C30EF61E2089475D00BEA785 /* VoiceSettingsTableViewController.swift */, B92EA05D27AC80E900127A9A /* SiriShortcutsTableViewController.swift */, + 7FB733C72CE555D500C99D46 /* GeneralSettingsViewController.swift */, + 7FB733CB2CE5570A00C99D46 /* CalloutSettingsViewController.swift */, + 7FB733CF2CE5575400C99D46 /* TroubleshootingSettingsViewController.swift */, ); name = Settings; sourceTree = ""; @@ -5872,6 +5881,7 @@ 6202A2A6284A85FD00CC51DF /* LeftAccessoryText.swift in Sources */, 28AA35F026C1E82800CBD680 /* MarkerLoader.swift in Sources */, B93629B01FCDE25000BAF3A6 /* UIViewController+Extensions.swift in Sources */, + 7FB733D02CE5575400C99D46 /* TroubleshootingSettingsViewController.swift in Sources */, 28246D1A21ED372A004C8F09 /* LinkedList.swift in Sources */, C38841D52268FCC400F6DFA5 /* POI+Similarity.swift in Sources */, C363227D22DFB6B700715374 /* Heading+Order.swift in Sources */, @@ -5961,6 +5971,7 @@ 62E8BB2D24AE6BCA00DDBCB4 /* LocationDetailViewController.swift in Sources */, 28F0F9281F86A43000B6A64F /* DestinationTutorialBeaconPage.swift in Sources */, 62791781248EBCA4001D72F3 /* IntersectionDecisionPoint.swift in Sources */, + 7FB733C82CE555D500C99D46 /* GeneralSettingsViewController.swift in Sources */, 6249500526FBE26C008D842B /* MarkerEditViewRepresentable.swift in Sources */, 62E2C04D267055BE00F7CBE1 /* WaypointDetail.swift in Sources */, C37E33B223610A370033D640 /* GeolocationManagerSnoozeDelegate.swift in Sources */, @@ -6176,6 +6187,7 @@ B9E63D0B26FE735600CCE4ED /* CloudKeyValueStore+Routes.swift in Sources */, 625F8524243BDAD10085AE05 /* UserActivityManager.swift in Sources */, 624DF68B27BED80C000A634C /* AuthorizationStatus.swift in Sources */, + 7FB733CC2CE5570A00C99D46 /* CalloutSettingsViewController.swift in Sources */, 2896378526D70CE7001694C0 /* TableHeaderCell.swift in Sources */, 3153765521FF1DFB008445AD /* CodeableDirection.swift in Sources */, 620BC33F25F2CC98007DBA29 /* HeadphoneMotionManagerStatus.swift in Sources */, 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 c99c624e..be0e47bb 100644 --- a/apps/ios/GuideDogs/Assets/Localization/en-US.lproj/Localizable.strings +++ b/apps/ios/GuideDogs/Assets/Localization/en-US.lproj/Localizable.strings @@ -380,21 +380,9 @@ /* Settings title, About Soundscape {NumberedPlaceholder="Soundscape"} */ "settings.about_app" = "About Soundscape"; -/* Settings title,General Settings */ -"settings.section.general" = "General Settings"; - /* Settings title,Units of Measure */ "settings.section.units" = "Units of Measure"; -/* Settings title, Troubleshooting */ -"settings.section.troubleshooting" = "Troubleshooting"; - -/* Settings title, About */ -"settings.section.about" = "About"; - -/* Settings title, Telemetry */ -"settings.section.telemetry" = "Telemetry"; - /* Settings title, Share Usage Data */ "settings.section.share_usage_data" = "Share Usage Data"; @@ -416,12 +404,47 @@ /* Language display name. %1$@ is a language name, %2$@ is the country name. e.g. "English (United Kingdom)" {NumberedPlaceholder="%1$@", "%2$@"} */ "settings.language.language_name" = "%1$@ (%2$@)"; +"menu.manage_callouts.all" = "Allow Callouts"; + +"menu.manage_callouts.poi" = "Places and Landmarks"; + +"menu.manage_callouts.mobility" = "Mobility"; + +"menu.manage_callouts.beacon" = "Distance to the Audio Beacon"; + +"menu.manage_callouts.shake" = "Repeat Callouts"; + + //------------------------------------------------------------------------------ -// MARK: Settings (Audio) +// MARK: Settings (Sections) //------------------------------------------------------------------------------ +/* Settings title, General settings for the app */ +"settings.section.general" = "General settings for the app."; + /* The title for the screen which configures media controls (play/pause/next/etc) related settings */ -"settings.audio.media_controls" = "Media Controls"; +"settings.audio.media_controls" = "Control how audio interacts with other media."; + +/* Settings title, Manage the callouts that help navigate */ +"menu.manage_callouts" = "Manage the callouts that help navigate."; + +/* Settings title, Settings for including unnamed roads */ +"settings.section.street_preview" = "Settings for including unnamed roads."; + +/* Settings title, Options for troubleshooting the app */ +"settings.section.troubleshooting" = "Options for troubleshooting the app."; + +/* Settings title, Information about the app */ +"settings.section.about" = "Information about the app."; + +/* Settings title, Manage data collection and privacy */ +"settings.section.telemetry" = "Manage data collection and privacy."; + + + +//------------------------------------------------------------------------------ +// MARK: Settings (Audio) +//------------------------------------------------------------------------------ /* A title for a toggle for an audio setting which determines if the current app's audio will be mixed with others. If ON, this app will output audio which could be played at the same time as other apps that play audio, such as another music player app. */ "settings.audio.mix_with_others.title" = "Enable Media Controls"; @@ -720,7 +743,7 @@ //------------------------------------------------------------------------------ /* Street Preview, View title, Street Preview is functionality that allows the user to select any location in the world to preview the area at street level in order to familiarise and build a mental map of the space. {NumberedPlaceholder="Soundscape Street Preview"} */ -"preview.title" = "Soundscape Street Preview"; +"preview.title" = "Street Preview"; /* Text displayed when the current location is not known while previewing */ "preview.current_intersection_unknown.label" = "Current Location Unknown"; @@ -2150,9 +2173,6 @@ /* Title, Head Tracking Headphones, "head tracking headphones" is a term that refers to headphones which include sensors that track a person's head movements */ "menu.devices" = "Head Tracking Headphones"; -/* Title, Manage Callouts */ -"menu.manage_callouts" = "Manage Callouts"; - /* Title, Help and Tutorials */ "menu.help_and_tutorials" = "Help & Tutorials"; diff --git a/apps/ios/GuideDogs/CalloutSettingsViewController.swift b/apps/ios/GuideDogs/CalloutSettingsViewController.swift new file mode 100644 index 00000000..3c8c6925 --- /dev/null +++ b/apps/ios/GuideDogs/CalloutSettingsViewController.swift @@ -0,0 +1,127 @@ +import UIKit + +class CalloutSettingsViewController: UITableViewController, CalloutSettingsCellViewDelegate { + + // MARK: - CalloutSettingsCellViewDelegate + func onCalloutSettingChanged(_ type: CalloutSettingCellType) { + // Handle callout setting changes here + print("Callout setting changed for type: \(type)") + } + + private enum CalloutsRow: Int, CaseIterable { + case all = 0 + case poi = 1 + case mobility = 2 + case beacon = 3 + case shake = 4 + } + + private static let cellIdentifiers: [CalloutsRow: String] = [ + .all: "allCallouts", + .poi: "poiCallouts", + .mobility: "mobilityCallouts", + .beacon: "beaconCallouts", + .shake: "shakeCallouts" + ] + + // Properties to store the states of each callout setting + private var automaticCalloutsEnabled = SettingsContext.shared.automaticCalloutsEnabled + private var poiCalloutsEnabled = true + private var mobilityCalloutsEnabled = true + private var beaconCalloutsEnabled = true + private var shakeCalloutsEnabled = false + + override func viewDidLoad() { + super.viewDidLoad() + self.title = GDLocalizedString("menu.manage_callouts") + + // Register all cell identifiers + for identifier in CalloutSettingsViewController.cellIdentifiers.values { + self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: identifier) + } + } + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + // Only show additional rows when "Allow Callouts" is enabled + return automaticCalloutsEnabled ? CalloutsRow.allCases.count : 1 + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + // Determine the row type + guard let rowType = CalloutsRow(rawValue: indexPath.row) else { + return UITableViewCell() + } + + // Get the identifier for the row type + let identifier = CalloutSettingsViewController.cellIdentifiers[rowType] ?? "default" + let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) + cell.selectionStyle = .none // Disable selection + + // Configure the toggle for each row + let switchView = UISwitch(frame: .zero) + switchView.addTarget(self, action: #selector(toggleCalloutSetting(_:)), for: .valueChanged) + switchView.tag = rowType.rawValue + cell.accessoryView = switchView + + // Configure the row content dynamically + switch rowType { + case .all: + cell.textLabel?.text = GDLocalizedString("menu.manage_callouts.all") + switchView.setOn(automaticCalloutsEnabled, animated: true) + case .poi: + cell.textLabel?.text = GDLocalizedString("menu.manage_callouts.poi") + switchView.setOn(poiCalloutsEnabled, animated: true) + case .mobility: + cell.textLabel?.text = GDLocalizedString("menu.manage_callouts.mobility") + switchView.setOn(mobilityCalloutsEnabled, animated: true) + case .beacon: + cell.textLabel?.text = GDLocalizedString("menu.manage_callouts.beacon") + switchView.setOn(beaconCalloutsEnabled, animated: true) + case .shake: + cell.textLabel?.text = GDLocalizedString("menu.manage_callouts.shake") + switchView.setOn(shakeCalloutsEnabled, animated: true) + } + + return cell + } + + // MARK: - Toggle Actions + + @objc private func toggleCalloutSetting(_ sender: UISwitch) { + // Determine which toggle was changed based on its tag + switch CalloutsRow(rawValue: sender.tag) { + case .all: + automaticCalloutsEnabled = sender.isOn + SettingsContext.shared.automaticCalloutsEnabled = sender.isOn + logCalloutToggle(for: "all", state: sender.isOn) + tableView.reloadData() // Refresh table to show/hide rows + case .poi: + poiCalloutsEnabled = sender.isOn + logCalloutToggle(for: "poi", state: sender.isOn) + case .mobility: + mobilityCalloutsEnabled = sender.isOn + logCalloutToggle(for: "mobility", state: sender.isOn) + case .beacon: + beaconCalloutsEnabled = sender.isOn + logCalloutToggle(for: "beacon", state: sender.isOn) + case .shake: + shakeCalloutsEnabled = sender.isOn + logCalloutToggle(for: "shake", state: sender.isOn) + case .none: + break + } + } + + // MARK: - Logging Function + + private func logCalloutToggle(for type: String, state: Bool) { + let logMessage = "Toggled \(type) callouts to: \(state)" + GDLogActionInfo(logMessage) // Use GDLog for logging + GDATelemetry.track("callout_toggle", with: ["type": type, "state": "\(state)"]) + } +} + diff --git a/apps/ios/GuideDogs/Code/App/Settings/SettingsContext.swift b/apps/ios/GuideDogs/Code/App/Settings/SettingsContext.swift index 8f4e54ae..d2c1f09c 100644 --- a/apps/ios/GuideDogs/Code/App/Settings/SettingsContext.swift +++ b/apps/ios/GuideDogs/Code/App/Settings/SettingsContext.swift @@ -487,3 +487,4 @@ extension SettingsContext: AutoCalloutSettingsProvider { } } } + diff --git a/apps/ios/GuideDogs/Code/Visual UI/View Controllers/Settings/SettingsViewController.swift b/apps/ios/GuideDogs/Code/Visual UI/View Controllers/Settings/SettingsViewController.swift index ffd31035..7a631fc7 100644 --- a/apps/ios/GuideDogs/Code/Visual UI/View Controllers/Settings/SettingsViewController.swift +++ b/apps/ios/GuideDogs/Code/Visual UI/View Controllers/Settings/SettingsViewController.swift @@ -1,4 +1,3 @@ -// // SettingsViewController.swift // Soundscape // @@ -7,19 +6,19 @@ // import UIKit - import AppCenterAnalytics class SettingsViewController: BaseTableViewController { - + private enum Section: Int, CaseIterable { case general = 0 case audio = 1 - case callouts = 2 - case streetPreview = 3 - case troubleshooting = 4 - case about = 5 - case telemetry = 6 + case beacon = 2 + case callouts = 3 + case streetPreview = 4 + case troubleshooting = 5 + case about = 6 + case telemetry = 7 } private enum CalloutsRow: Int, CaseIterable { @@ -33,19 +32,21 @@ class SettingsViewController: BaseTableViewController { private static let cellIdentifiers: [IndexPath: String] = [ IndexPath(row: 0, section: Section.general.rawValue): "languageAndRegion", IndexPath(row: 1, section: Section.general.rawValue): "voice", - IndexPath(row: 2, section: Section.general.rawValue): "beaconSettings", - IndexPath(row: 3, section: Section.general.rawValue): "volumeSettings", - IndexPath(row: 4, section: Section.general.rawValue): "manageDevices", - IndexPath(row: 5, section: Section.general.rawValue): "siriShortcuts", - + IndexPath(row: 2, section: Section.general.rawValue): "volumeSettings", + IndexPath(row: 3, section: Section.general.rawValue): "manageDevices", + IndexPath(row: 4, section: Section.general.rawValue): "siriShortcuts", + IndexPath(row: 0, section: Section.audio.rawValue): "mixAudio", + IndexPath(row: 0, section: Section.beacon.rawValue): "beaconSettings", + + IndexPath(row: CalloutsRow.all.rawValue, section: Section.callouts.rawValue): "allCallouts", IndexPath(row: CalloutsRow.poi.rawValue, section: Section.callouts.rawValue): "poiCallouts", IndexPath(row: CalloutsRow.mobility.rawValue, section: Section.callouts.rawValue): "mobilityCallouts", IndexPath(row: CalloutsRow.beacon.rawValue, section: Section.callouts.rawValue): "beaconCallouts", IndexPath(row: CalloutsRow.shake.rawValue, section: Section.callouts.rawValue): "shakeCallouts", - + IndexPath(row: 0, section: Section.streetPreview.rawValue): "streetPreview", IndexPath(row: 0, section: Section.troubleshooting.rawValue): "troubleshooting", IndexPath(row: 0, section: Section.about.rawValue): "about", @@ -62,6 +63,20 @@ class SettingsViewController: BaseTableViewController { // MARK: Properties @IBOutlet weak var largeBannerContainerView: UIView! + + private var expandedSections: Set = [] + + // Section Descriptions + private static let sectionDescriptions: [Section: String] = [ + .general: "General settings for the app.", + .audio: "Control how audio interacts with other media.", + .beacon: "Settings for beacon management.", + .callouts: "Manage the callouts that help navigate.", + .streetPreview: "Settings for including unnamed roads.", + .troubleshooting: "Options for troubleshooting the app.", + .about: "Information about the app.", + .telemetry: "Manage data collection and privacy." + ] // MARK: View Life Cycle @@ -73,6 +88,7 @@ class SettingsViewController: BaseTableViewController { GDATelemetry.trackScreenView("settings") self.title = GDLocalizedString("settings.screen_title") + expandedSections = [] } override func numberOfSections(in tableView: UITableView) -> Int { @@ -81,60 +97,62 @@ class SettingsViewController: BaseTableViewController { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { guard let sectionType = Section(rawValue: section) else { return 0 } - + + switch sectionType { - case .general: return 6 - case .audio: return 1 - case .callouts: return SettingsContext.shared.automaticCalloutsEnabled ? 5 : 1 - case .streetPreview: return 1 - case .troubleshooting: return 1 - case .about: return 1 - case .telemetry: return 1 + case .general: return expandedSections.contains(section) ? 5 : 0 + case .audio: return expandedSections.contains(section) ? 1 : 0 + case .beacon: return expandedSections.contains(section) ? 1 : 0 + case .callouts: + return SettingsContext.shared.automaticCalloutsEnabled && expandedSections.contains(section) ? 5 : 0 + case .streetPreview, .troubleshooting, .about, .telemetry: + return expandedSections.contains(section) ? 1 : 0 } } + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return expandedSections.contains(indexPath.section) ? UITableView.automaticDimension : 0 + } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard expandedSections.contains(indexPath.section) else { + return UITableViewCell() + } + let identifier = SettingsViewController.cellIdentifiers[indexPath] + let cell = tableView.dequeueReusableCell(withIdentifier: identifier ?? "default", for: indexPath) - guard let sectionType = Section(rawValue: indexPath.section) else { - return tableView.dequeueReusableCell(withIdentifier: identifier ?? "default", for: indexPath) - } - - switch sectionType { + switch Section(rawValue: indexPath.section) { case .callouts: - let cell = tableView.dequeueReusableCell(withIdentifier: identifier ?? "default", for: indexPath) as! CalloutSettingsCellView - cell.delegate = self - - if let rowType = CalloutsRow(rawValue: indexPath.row) { - switch rowType { - case .all: cell.type = .all - case .poi: cell.type = .poi - case .mobility: cell.type = .mobility - case .beacon: cell.type = .beacon - case .shake: cell.type = .shake - } - } - - return cell - + configureCalloutCell(cell as! CalloutSettingsCellView, at: indexPath) case .telemetry: - let cell = tableView.dequeueReusableCell(withIdentifier: identifier ?? "default", for: indexPath) as! TelemetrySettingsTableViewCell - cell.parent = self - - return cell - + (cell as! TelemetrySettingsTableViewCell).parent = self case .audio: - let cell = tableView.dequeueReusableCell(withIdentifier: identifier ?? "default", for: indexPath) as! MixAudioSettingCell - cell.delegate = self - return cell - + (cell as! MixAudioSettingCell).delegate = self default: - return tableView.dequeueReusableCell(withIdentifier: identifier ?? "default", for: indexPath) + break } + return cell } - // MARK: UITableViewDataSource + private func configureCalloutCell(_ cell: CalloutSettingsCellView, at indexPath: IndexPath) { + cell.delegate = self + if let rowType = CalloutsRow(rawValue: indexPath.row) { + switch rowType { + case .all: + cell.type = .all + case .poi: + cell.type = .poi + case .mobility: + cell.type = .mobility + case .beacon: + cell.type = .beacon + case .shake: + cell.type = .shake + } + } + } override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { guard let sectionType = Section(rawValue: section) else { return nil } @@ -142,6 +160,7 @@ class SettingsViewController: BaseTableViewController { switch sectionType { case .general: return GDLocalizedString("settings.section.general") case .audio: return GDLocalizedString("settings.audio.media_controls") + case .beacon: return GDLocalizedString("Beacon") case .callouts: return GDLocalizedString("menu.manage_callouts") case .about: return GDLocalizedString("settings.section.about") case .streetPreview: return GDLocalizedString("preview.title") @@ -153,6 +172,10 @@ class SettingsViewController: BaseTableViewController { override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { guard let sectionType = Section(rawValue: section) else { return nil } + if expandedSections.contains(section) { + return SettingsViewController.sectionDescriptions[sectionType] + } + switch sectionType { case .audio: return GDLocalizedString("settings.audio.mix_with_others.description") case .streetPreview: return GDLocalizedString("preview.include_unnamed_roads.subtitle") @@ -160,30 +183,72 @@ class SettingsViewController: BaseTableViewController { default: return nil } } + + override func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { + guard let header = view as? UITableViewHeaderFooterView else { return } + + header.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleHeaderTap(_:)))) + header.tag = section + + header.textLabel?.textColor = .white + header.textLabel?.font = UIFont.boldSystemFont(ofSize: 18) + header.contentView.backgroundColor = UIColor(named: "HeaderBackgroundColor") + + // Accessibility + header.accessibilityTraits = .header + header.accessibilityLabel = SettingsViewController.sectionDescriptions[Section(rawValue: section)!] + header.accessibilityHint = "Double tap to expand or collapse this section" + + let chevronImageView = UIImageView(image: UIImage(systemName: "chevron.right")) + chevronImageView.tintColor = .white + chevronImageView.translatesAutoresizingMaskIntoConstraints = false + header.contentView.addSubview(chevronImageView) + + NSLayoutConstraint.activate([ + chevronImageView.trailingAnchor.constraint(equalTo: header.contentView.trailingAnchor, constant: -15), + chevronImageView.centerYAnchor.constraint(equalTo: header.contentView.centerYAnchor), + chevronImageView.widthAnchor.constraint(equalToConstant: 20), + chevronImageView.heightAnchor.constraint(equalToConstant: 20) + ]) + } + + @objc private func handleHeaderTap(_ gesture: UITapGestureRecognizer) { + guard let header = gesture.view as? UITableViewHeaderFooterView else { return } + let section = header.tag + + if section == Section.callouts.rawValue { + // Navigate to CalloutsSettingsViewController + let calloutsSettingsVC = CalloutSettingsViewController() + self.navigationController?.pushViewController(calloutsSettingsVC, animated: true) + } else { + tableView.beginUpdates() + if expandedSections.contains(section) { + expandedSections.remove(section) + tableView.reloadSections(IndexSet(integer: section), with: .automatic) + UIAccessibility.post(notification: .announcement, argument: "Section collapsed") + } else { + expandedSections.insert(section) + tableView.reloadSections(IndexSet(integer: section), with: .automatic) + UIAccessibility.post(notification: .announcement, argument: "Section expanded") + } + tableView.endUpdates() + } + } + } extension SettingsViewController: MixAudioSettingCellDelegate { func onSettingValueChanged(_ cell: MixAudioSettingCell, settingSwitch: UISwitch) { - // Note: The UI for this setting is "Enable Media Controls" but the setting is stored as - // "Mixes with Others" (the inverse of "Enable Media Controls") - guard settingSwitch.isOn else { - // If the setting switch is now off, the user disabled media controls. This doesn't - // require a warning alert, so just set mixesWithOthers to true and return. updateSetting(true) return } - - // Otherwise, the user is turning on media controls, so we need to show a warning to make sure - // they understand what this change means in terms of how other audio apps will stop Soundscape - // from playing. This warning was added based on bug bash feedback on 12/3/20. - // Show an alert indicating that the user can download an enhanced version of the voice in Settings + let alert = UIAlertController(title: GDLocalizedString("general.alert.confirmation_title"), message: GDLocalizedString("setting.audio.mix_with_others.confirmation"), preferredStyle: .alert) let mixAction = UIAlertAction(title: GDLocalizedString("settings.audio.mix_with_others.title"), style: .default) { [weak self] (_) in - // Make the setting switch - turn off mixesWithOthers self?.updateSetting(false) self?.focusOnCell(cell) } @@ -191,12 +256,8 @@ extension SettingsViewController: MixAudioSettingCellDelegate { alert.preferredAction = mixAction alert.addAction(UIAlertAction(title: GDLocalizedString("general.alert.cancel"), style: .cancel, handler: { [weak self] (_) in - // Toggle the setting back off settingSwitch.isOn = false - - // Track that the user decided not to enable media controls GDATelemetry.track("settings.mix_audio.cancel", with: ["context": "app_settings"]) - self?.focusOnCell(cell) })) @@ -241,5 +302,4 @@ extension SettingsViewController: LargeBannerContainerView { largeBannerContainerView.setHeight(height) tableView.reloadData() } - } diff --git a/apps/ios/GuideDogs/GeneralSettingsViewController.swift b/apps/ios/GuideDogs/GeneralSettingsViewController.swift new file mode 100644 index 00000000..1b797ae8 --- /dev/null +++ b/apps/ios/GuideDogs/GeneralSettingsViewController.swift @@ -0,0 +1,132 @@ +// +// GeneralSettingsViewController.swift +// Soundscape +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// + +import UIKit + +class GeneralSettingsViewController: UITableViewController { + + private enum GeneralRow: Int, CaseIterable { + case languageAndRegion = 0 + case voice = 1 + case beaconSettings = 2 + case volumeSettings = 3 + case manageDevices = 4 + case siriShortcuts = 5 + } + + private static let cellIdentifiers: [GeneralRow: String] = [ + .languageAndRegion: "languageAndRegion", + .voice: "voice", + .beaconSettings: "beaconSettings", + .volumeSettings: "volumeSettings", + .manageDevices: "manageDevices", + .siriShortcuts: "siriShortcuts" + ] + + + private static let rowDescriptions: [GeneralRow: String] = [ + .languageAndRegion: GDLocalizedString("settings.general.language_and_region"), + .voice: GDLocalizedString("settings.general.voice"), + .beaconSettings: GDLocalizedString("settings.general.beacon_settings"), + .volumeSettings: GDLocalizedString("settings.general.volume_settings"), + .manageDevices: GDLocalizedString("settings.general.manage_devices"), + .siriShortcuts: GDLocalizedString("settings.general.siri_shortcuts") + ] + + override func viewDidLoad() { + super.viewDidLoad() + self.title = GDLocalizedString("settings.section.general") + + // Register the default UITableViewCell class for reuse + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "languageAndRegion") + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "voice") + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "beaconSettings") + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "volumeSettings") + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "manageDevices") + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "siriShortcuts") + } + + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 // Single section for General Settings + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return GeneralRow.allCases.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let row = GeneralRow(rawValue: indexPath.row) else { + fatalError("Unexpected row in General Settings") + } + + let cellIdentifier = GeneralSettingsViewController.cellIdentifiers[row] ?? "default" + let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) + cell.textLabel?.text = GeneralSettingsViewController.rowDescriptions[row] + cell.accessoryType = .disclosureIndicator + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + guard let row = GeneralRow(rawValue: indexPath.row) else { return } + navigateToDetail(for: row) + } + + private func navigateToDetail(for row: GeneralRow) { + switch row { + case .languageAndRegion: + navigateToLanguageAndRegion() + case .voice: + navigateToVoiceSettings() + case .beaconSettings: + navigateToBeaconSettings() + case .volumeSettings: + navigateToVolumeSettings() + case .manageDevices: + navigateToManageDevices() + case .siriShortcuts: + navigateToSiriShortcuts() + } + } + + private func navigateToLanguageAndRegion() { + // Push detailed Language and Region screen + GDLogActionInfo("Opened 'Language and Region Settings'") + } + + private func navigateToVoiceSettings() { + // Push detailed Voice Settings screen + GDLogActionInfo("Opened 'Voice Settings'") + } + + private func navigateToBeaconSettings() { + // Push detailed Beacon Settings screen + GDLogActionInfo("Opened 'Beacon Settings'") + } + + private func navigateToVolumeSettings() { + // Push detailed Volume Settings screen + GDLogActionInfo("Opened 'Volume Settings'") + } + + private func navigateToManageDevices() { + // Push detailed Manage Devices screen + GDLogActionInfo("Opened 'Manage Devices'") + } + + private func navigateToSiriShortcuts() { + // Push detailed Siri Shortcuts screen + GDLogActionInfo("Opened 'Siri Shortcuts'") + } + + override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + return GDLocalizedString("settings.section.general.description") + } +} diff --git a/apps/ios/GuideDogs/TroubleshootingSettingsViewController.swift b/apps/ios/GuideDogs/TroubleshootingSettingsViewController.swift new file mode 100644 index 00000000..f0b39035 --- /dev/null +++ b/apps/ios/GuideDogs/TroubleshootingSettingsViewController.swift @@ -0,0 +1,109 @@ +import UIKit + +class TroubleshootingViewController: UITableViewController { + + private enum TroubleshootingRow: Int, CaseIterable { + case checkAudio = 0 + case clearMapData = 1 + } + + private static let cellIdentifiers: [TroubleshootingRow: String] = [ + .checkAudio: "checkAudio", + .clearMapData: "clearMapData" + ] + + override func viewDidLoad() { + super.viewDidLoad() + self.title = GDLocalizedString("settings.section.troubleshooting") + + // Register the specific cell identifiers + self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "checkAudio") + self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "clearMapData") + } + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return TroubleshootingRow.allCases.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let rowType = TroubleshootingRow(rawValue: indexPath.row) else { + return UITableViewCell() + } + + let identifier = TroubleshootingViewController.cellIdentifiers[rowType] ?? "default" + let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) + cell.selectionStyle = .default + + switch rowType { + case .checkAudio: + cell.textLabel?.text = GDLocalizedString("troubleshooting.check_audio") + cell.accessoryType = .disclosureIndicator + case .clearMapData: + cell.textLabel?.text = GDLocalizedString("settings.clear_data") + cell.accessoryType = .disclosureIndicator + } + + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let rowType = TroubleshootingRow(rawValue: indexPath.row) else { + return + } + + switch rowType { + case .checkAudio: + checkAudioStatus() + case .clearMapData: + clearMapDataConfirmation() + } + + tableView.deselectRow(at: indexPath, animated: true) + } + + // MARK: - Actions + + private func checkAudioStatus() { + // Here, implement the functionality to check audio status. + // This could involve presenting an alert or a new screen with audio information. + let alert = UIAlertController( + title: GDLocalizedString("troubleshooting.check_audio"), + message: GDLocalizedString("troubleshooting.check_audio.explanation"), + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: GDLocalizedString("general.alert.ok"), style: .default)) + present(alert, animated: true) + } + + private func clearMapDataConfirmation() { + // Display a confirmation alert before clearing the map data + let alert = UIAlertController( + title: GDLocalizedString("settings.clear_cache.alert_title"), + message: GDLocalizedString("settings.clear_cache.alert_message"), + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: GDLocalizedString("general.alert.cancel"), style: .cancel)) + alert.addAction(UIAlertAction(title: GDLocalizedString("general.alert.confirm"), style: .destructive) { _ in + self.clearMapData() + }) + present(alert, animated: true) + } + + private func clearMapData() { + // Logic to clear stored map data goes here + // For example, removing cached files or data from UserDefaults if relevant. + // Notify the user after clearing data. + let confirmationAlert = UIAlertController( + title: GDLocalizedString("settings.clear_cache.alert_title"), + message: GDLocalizedString("settings.clear_cache.no_service.message"), + preferredStyle: .alert + ) + confirmationAlert.addAction(UIAlertAction(title: GDLocalizedString("general.alert.ok"), style: .default)) + present(confirmationAlert, animated: true) + } +} +