Skip to content

Commit

Permalink
Add a Quick Action for toggling Speak Readings
Browse files Browse the repository at this point in the history
Speak Readings setting can now be quickly accessed from home screen by long-pressing the app icon.
  • Loading branch information
stami committed May 1, 2022
1 parent f487db5 commit 84e5295
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 4 deletions.
20 changes: 20 additions & 0 deletions xdrip.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -745,6 +748,9 @@
D4E499AC277B4CE7000F8CBA /* DateOnly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateOnly.swift; sourceTree = "<group>"; };
D4FD899627772F9100689788 /* TreatmentEntryAccessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TreatmentEntryAccessor.swift; sourceTree = "<group>"; };
F51B9F7C24B216CD00FC0643 /* Libre1NonFixedSlopeCalibrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Libre1NonFixedSlopeCalibrator.swift; sourceTree = "<group>"; };
F64039AF281C3F9D0051EFFE /* QuickActionsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickActionsManager.swift; sourceTree = "<group>"; };
F64039B1281E90CF0051EFFE /* QuickActions.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = QuickActions.strings; sourceTree = "<group>"; };
F64039B4281E91500051EFFE /* TextsQuickActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextsQuickActions.swift; sourceTree = "<group>"; };
F8025C0921D94FD700ECF0C0 /* CBManagerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CBManagerState.swift; sourceTree = "<group>"; };
F8025C1221DA683400ECF0C0 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; };
F8025E4D21ED450300ECF0C0 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1620,6 +1626,14 @@
path = Treatments;
sourceTree = "<group>";
};
F64039AE281C3F8D0051EFFE /* QuickActions */ = {
isa = PBXGroup;
children = (
F64039AF281C3F9D0051EFFE /* QuickActionsManager.swift */,
);
path = QuickActions;
sourceTree = "<group>";
};
F8025C0B21D9513400ECF0C0 /* Extensions */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -2010,6 +2024,7 @@
F821CF4F229BF43A005C1E43 /* NightScout */,
F821CF9922AEF2DF005C1E43 /* Speak */,
F8E3A2A723D906B600E5E98A /* Watch */,
F64039AE281C3F8D0051EFFE /* QuickActions */,
);
path = Managers;
sourceTree = "<group>";
Expand Down Expand Up @@ -2041,6 +2056,7 @@
F81F370A25C1584A00520946 /* LibreStates.strings */,
F8E6C79324CEC2E3007C1199 /* Snooze.strings */,
4749EB9D25B36E010072DF8B /* LibreNFC.strings */,
F64039B1281E90CF0051EFFE /* QuickActions.strings */,
);
path = Storyboards;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 /* [email protected] in Resources */,
F82437C324CB7A9900BED341 /* Metallic.caf in Resources */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
11 changes: 11 additions & 0 deletions xdrip/Application Delegate/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}

91 changes: 91 additions & 0 deletions xdrip/Managers/QuickActions/QuickActionsManager.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
2 changes: 2 additions & 0 deletions xdrip/Storyboards/QuickActions.strings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"quickactions_speak_readings" = "Speak readings";
"quickactions_stop_speaking_readings" = "Stop speaking readings";
14 changes: 14 additions & 0 deletions xdrip/Texts/TextsQuickActions.swift
Original file line number Diff line number Diff line change
@@ -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")
}()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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
}
}
}

0 comments on commit 84e5295

Please sign in to comment.