diff --git a/xdrip.xcodeproj/project.pbxproj b/xdrip.xcodeproj/project.pbxproj index c20437e34..96b53fbe3 100644 --- a/xdrip.xcodeproj/project.pbxproj +++ b/xdrip.xcodeproj/project.pbxproj @@ -44,6 +44,9 @@ D4E499AD277B4CE7000F8CBA /* DateOnly.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4E499AC277B4CE7000F8CBA /* DateOnly.swift */; }; D4FD899727772F9100689788 /* TreatmentEntryAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4FD899627772F9100689788 /* TreatmentEntryAccessor.swift */; }; F51B9F7D24B216CD00FC0643 /* Libre1NonFixedSlopeCalibrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F51B9F7C24B216CD00FC0643 /* Libre1NonFixedSlopeCalibrator.swift */; }; + F64039B0281C3F9D0051EFFE /* QuickActionsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F64039AF281C3F9D0051EFFE /* QuickActionsManager.swift */; }; + F64039B2281E90CF0051EFFE /* QuickActions.strings in Resources */ = {isa = PBXBuildFile; fileRef = F64039B1281E90CF0051EFFE /* QuickActions.strings */; }; + F64039B5281E91500051EFFE /* TextsQuickActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F64039B4281E91500051EFFE /* TextsQuickActions.swift */; }; F8025C0A21D94FD700ECF0C0 /* CBManagerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8025C0921D94FD700ECF0C0 /* CBManagerState.swift */; }; F8025C1321DA683400ECF0C0 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8025C1221DA683400ECF0C0 /* Data.swift */; }; F8025E4E21ED450300ECF0C0 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8025E4D21ED450300ECF0C0 /* Double.swift */; }; @@ -745,6 +748,9 @@ D4E499AC277B4CE7000F8CBA /* DateOnly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateOnly.swift; sourceTree = ""; }; D4FD899627772F9100689788 /* TreatmentEntryAccessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TreatmentEntryAccessor.swift; sourceTree = ""; }; F51B9F7C24B216CD00FC0643 /* Libre1NonFixedSlopeCalibrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Libre1NonFixedSlopeCalibrator.swift; sourceTree = ""; }; + F64039AF281C3F9D0051EFFE /* QuickActionsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickActionsManager.swift; sourceTree = ""; }; + F64039B1281E90CF0051EFFE /* QuickActions.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = QuickActions.strings; sourceTree = ""; }; + F64039B4281E91500051EFFE /* TextsQuickActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextsQuickActions.swift; sourceTree = ""; }; F8025C0921D94FD700ECF0C0 /* CBManagerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CBManagerState.swift; sourceTree = ""; }; F8025C1221DA683400ECF0C0 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; F8025E4D21ED450300ECF0C0 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; @@ -1620,6 +1626,14 @@ path = Treatments; sourceTree = ""; }; + F64039AE281C3F8D0051EFFE /* QuickActions */ = { + isa = PBXGroup; + children = ( + F64039AF281C3F9D0051EFFE /* QuickActionsManager.swift */, + ); + path = QuickActions; + sourceTree = ""; + }; F8025C0B21D9513400ECF0C0 /* Extensions */ = { isa = PBXGroup; children = ( @@ -2010,6 +2024,7 @@ F821CF4F229BF43A005C1E43 /* NightScout */, F821CF9922AEF2DF005C1E43 /* Speak */, F8E3A2A723D906B600E5E98A /* Watch */, + F64039AE281C3F8D0051EFFE /* QuickActions */, ); path = Managers; sourceTree = ""; @@ -2041,6 +2056,7 @@ F81F370A25C1584A00520946 /* LibreStates.strings */, F8E6C79324CEC2E3007C1199 /* Snooze.strings */, 4749EB9D25B36E010072DF8B /* LibreNFC.strings */, + F64039B1281E90CF0051EFFE /* QuickActions.strings */, ); path = Storyboards; sourceTree = ""; @@ -2521,6 +2537,7 @@ F82436FB24BE014000BED341 /* TextsLibreStates.swift */, F869188B23A044340065B607 /* TextsM5StackView.swift */, F84DDF4A279DF03400F7B5A4 /* TextsNightScout.swift */, + F64039B4281E91500051EFFE /* TextsQuickActions.swift */, F8BDD451221DEAB1006EAB84 /* TextsSettingsView.swift */, F8E6C78F24CEC22A007C1199 /* TextsSnooze.swift */, F8B48A9322B2A705009BCC01 /* TextsSpeakReading.swift */, @@ -3278,6 +3295,7 @@ F8AC426A21ADEBD70078C348 /* LaunchScreen.storyboard in Resources */, F824378524CB7A9900BED341 /* Siri_Low_Glucose.caf in Resources */, F824378124CB7A9800BED341 /* Cartoon_Uh_Oh.caf in Resources */, + F64039B2281E90CF0051EFFE /* QuickActions.strings in Resources */, F824377F24CB7A9800BED341 /* Sci-Fi_Alarm.caf in Resources */, F8E3A2A323D4E7E200E5E98A /* Default-568h@2x.png in Resources */, F82437C324CB7A9900BED341 /* Metallic.caf in Resources */, @@ -3560,6 +3578,7 @@ F821CF8122A5C814005C1E43 /* RepeatingTimer.swift in Sources */, F80D915C24F06A40006840B5 /* PreLibre2.swift in Sources */, 470F021326DD515300C5D626 /* SettingsViewSensorCountdownSettingsViewModel.swift in Sources */, + F64039B5281E91500051EFFE /* TextsQuickActions.swift in Sources */, F8F9722223A5915900C3F17D /* CRC.swift in Sources */, F8CB59C02734976D00BA199E /* DexcomTransmitterTimeTxMessage.swift in Sources */, F821CF6F229FC280005C1E43 /* Endpoint+NightScout.swift in Sources */, @@ -3795,6 +3814,7 @@ F8B3A820227DEC92004BA588 /* AlertTypesAccessor.swift in Sources */, F8F9720623A5915900C3F17D /* AuthRequestTxMessage.swift in Sources */, F8F9721123A5915900C3F17D /* KeepAliveTxMessage.swift in Sources */, + F64039B0281C3F9D0051EFFE /* QuickActionsManager.swift in Sources */, F8B3A81E227DEC92004BA588 /* BgReadingsAccessor.swift in Sources */, F8F1671327274557001AA3D8 /* DexcomCalibrationRxMessage.swift in Sources */, F8E51D63244B3386001C9E5A /* MiaoMiaoResponseType.swift in Sources */, diff --git a/xdrip/Application Delegate/AppDelegate.swift b/xdrip/Application Delegate/AppDelegate.swift index aed705629..b9043dedd 100644 --- a/xdrip/Application Delegate/AppDelegate.swift +++ b/xdrip/Application Delegate/AppDelegate.swift @@ -8,6 +8,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: - Properties var window: UIWindow? + + private let quickActionsManager = QuickActionsManager() /// allow the orientation to be changed as per the settings for each individual view controller var restrictRotation:UIInterfaceOrientationMask = .all @@ -53,5 +55,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. // Saves changes in the application's managed object context before the application terminates. } + + // Handle Quick Actions + func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { + if let quickActionType = QuickActionType(rawValue: shortcutItem.type) { + quickActionsManager.handleQuickAction(quickActionType) + } + + completionHandler(true) + } } diff --git a/xdrip/Managers/QuickActions/QuickActionsManager.swift b/xdrip/Managers/QuickActions/QuickActionsManager.swift new file mode 100644 index 000000000..598eab79f --- /dev/null +++ b/xdrip/Managers/QuickActions/QuickActionsManager.swift @@ -0,0 +1,91 @@ +// +// QuickActionsManager.swift +// xdrip +// +// Created by Samuli Tamminen on 29.4.2022. +// Copyright © 2022 Johan Degraeve. All rights reserved. +// + +import UIKit + +/// This enum defines actions that can be available at app icon's quick actions on iOS home screen +enum QuickActionType: String { + case speakReadings = "speakReadings" + case stopSpeakingReadings = "stopSpeakingReadings" + + /// Title is displayed in the long-press menu on the iOS home screen + private var localizedTitle: String { + switch self { + case .speakReadings: return Texts_QuickActions.speakReadings + case .stopSpeakingReadings: return Texts_QuickActions.stopSpeakingReadings + } + } + + /// Icon is displayed nex to the tile in the long-press menu on the iOS home screen + private var icon: UIApplicationShortcutIcon { + switch self { + case .speakReadings: return .init(systemImageName: "speaker.wave.2") + case .stopSpeakingReadings: return .init(systemImageName: "speaker.slash") + } + } + + /// Make a UIApplicationShortcutItem from the action + var shortcutItem: UIApplicationShortcutItem { + return UIApplicationShortcutItem(type: rawValue, localizedTitle: localizedTitle, localizedSubtitle: nil, icon: icon) + } +} + +class QuickActionsManager: NSObject { + override init() { + super.init() + + // add observer for speakReadings to update available quick actions when the setting is changed + UserDefaults.standard.addObserver(self, forKeyPath: UserDefaults.Key.speakReadings.rawValue, options: .new, context: nil) + + // Refresh initial state + updateAvailableQuickActions() + } + + /// Refresh available quick actions + func updateAvailableQuickActions() { + var shortcutItems = [UIApplicationShortcutItem]() + + if UserDefaults.standard.speakReadings { + shortcutItems.append(QuickActionType.stopSpeakingReadings.shortcutItem) + } else { + shortcutItems.append(QuickActionType.speakReadings.shortcutItem) + } + + UIApplication.shared.shortcutItems = shortcutItems + } + + /// Perform the necessary action when user selects a quick action + func handleQuickAction(_ actionType: QuickActionType) { + switch actionType { + case .speakReadings: + UserDefaults.standard.speakReadings = true + case .stopSpeakingReadings: + UserDefaults.standard.speakReadings = false + } + + // Refresh actions to represent current state + updateAvailableQuickActions() + } + + // MARK: - observe function + + // update available quick actions when the related setting is changed from elsewhere + override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + guard let keyPath = keyPath, + let keyPathEnum = UserDefaults.Key(rawValue: keyPath) + else { return } + + switch keyPathEnum { + case UserDefaults.Key.speakReadings: + updateAvailableQuickActions() + + default: + break + } + } +} diff --git a/xdrip/Storyboards/QuickActions.strings b/xdrip/Storyboards/QuickActions.strings new file mode 100644 index 000000000..1d1a43380 --- /dev/null +++ b/xdrip/Storyboards/QuickActions.strings @@ -0,0 +1,2 @@ +"quickactions_speak_readings" = "Speak readings"; +"quickactions_stop_speaking_readings" = "Stop speaking readings"; diff --git a/xdrip/Texts/TextsQuickActions.swift b/xdrip/Texts/TextsQuickActions.swift new file mode 100644 index 000000000..615a9f4a7 --- /dev/null +++ b/xdrip/Texts/TextsQuickActions.swift @@ -0,0 +1,14 @@ +import Foundation + +/// all texts for Quick Actions +class Texts_QuickActions { + static private let filename = "QuickActions" + + static let speakReadings: String = { + return NSLocalizedString("quickactions_speak_readings", tableName: filename, bundle: Bundle.main, value: "Speak readings", comment: "Home screen quick action, turns speaking on, available when speaking is off") + }() + + static let stopSpeakingReadings: String = { + return NSLocalizedString("quickactions_stop_speaking_readings", tableName: filename, bundle: Bundle.main, value: "Stop speaking readings", comment: "Home screen quick action, turns speaking off, available when speaking is on") + }() +} diff --git a/xdrip/View Controllers/Helpers/SettingsViewModelProtocol.swift b/xdrip/View Controllers/Helpers/SettingsViewModelProtocol.swift index ff8f86c95..282d1bd82 100644 --- a/xdrip/View Controllers/Helpers/SettingsViewModelProtocol.swift +++ b/xdrip/View Controllers/Helpers/SettingsViewModelProtocol.swift @@ -72,6 +72,13 @@ protocol SettingsViewModelProtocol { /// just an additional method to force row reloads, (there's also the method completeSettingsViewRefreshNeeded which may return true or false depending on row number and which will be called from within the SettingsViewController. The rowReloadClosure is useful when the reload needs to be handled asynchronously func storeRowReloadClosure(rowReloadClosure: @escaping ((Int) -> Void)) + /// closure to call to reload the current section that the viewmodel is implementing + func storeSectionReloadClosure(sectionReloadClosure: @escaping (() -> Void)) +} + +// Add default implementations here so that ViewModels don't need to implement empty methods +extension SettingsViewModelProtocol { + func storeSectionReloadClosure(sectionReloadClosure: @escaping (() -> Void)) {} } /// to make the coding a bit easier, just one function defined for now, which is to get the viewModel for a specific setting diff --git a/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewController.swift b/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewController.swift index f5bff7f2a..6808c71db 100644 --- a/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewController.swift +++ b/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewController.swift @@ -169,11 +169,14 @@ final class SettingsViewController: UIViewController { // store self as uiViewController in the viewModel viewModel.storeUIViewController(uIViewController: self) - // store reload closure in the viewModel + // store row reload closure in the viewModel viewModel.storeRowReloadClosure(rowReloadClosure: {row in - self.tableView.reloadRows(at: [IndexPath(row: row, section: section.rawValue)], with: .none) - + }) + + // store section reload closure in the viewModel + viewModel.storeSectionReloadClosure(sectionReloadClosure: { [weak self] in + self?.tableView.reloadSections([section.rawValue], with: .none) }) // store the viewModel diff --git a/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewSpeakSettingsViewModel.swift b/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewSpeakSettingsViewModel.swift index 3775ba78e..9956c7031 100644 --- a/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewSpeakSettingsViewModel.swift +++ b/xdrip/View Controllers/SettingsNavigationController/SettingsViewController/SettingsViewModels/SettingsViewSpeakSettingsViewModel.swift @@ -20,10 +20,21 @@ fileprivate enum Setting:Int, CaseIterable { } /// conforms to SettingsViewModelProtocol for all speak settings in the first sections screen -class SettingsViewSpeakSettingsViewModel:SettingsViewModelProtocol { +class SettingsViewSpeakSettingsViewModel: NSObject, SettingsViewModelProtocol { + override init() { + super.init() + addObservers() + } + + var sectionReloadClosure: (() -> Void)? + func storeRowReloadClosure(rowReloadClosure: ((Int) -> Void)) {} + func storeSectionReloadClosure(sectionReloadClosure: @escaping (() -> Void)) { + self.sectionReloadClosure = sectionReloadClosure + } + func storeUIViewController(uIViewController: UIViewController) {} func storeMessageHandler(messageHandler: ((String, String) -> Void)) { @@ -155,4 +166,26 @@ class SettingsViewSpeakSettingsViewModel:SettingsViewModelProtocol { return nil } } + + // MARK: - observe functions + + private func addObservers() { + // Listen for changes in the Speak Readings setting as it may be changed with a Quick Action + UserDefaults.standard.addObserver(self, forKeyPath: UserDefaults.Key.speakReadings.rawValue, options: .new, context: nil) + } + + override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + guard let keyPath = keyPath, + let keyPathEnum = UserDefaults.Key(rawValue: keyPath) + else { return } + + switch keyPathEnum { + case UserDefaults.Key.speakReadings: + // Speak readings setting has been changed from other model, likely by a Quick Action. Update UI to reflect current state. + sectionReloadClosure?() + + default: + break + } + } }