Option to define loopDelay dependent on the time of day
loopDelay means that readings shared with Loop will be shifted forward with a predefined amount of minutes. Example : a reading that is for instance created at 15:00, to which a delay is applied of 5 minutes, then Loop will receive that reading at 15:05, while at 15:00, Loop will receive the reading that was created at 14:55. This is only useful when used in combination with smoothing. Advantage is that readings that Loop will receive are more efficiently smoothed - new setting is available in the Developer settings - used SwiftUI to define a new View that allows to create loopDelays - colors used in SwiftUI code are defined in Constants/ConstantsUI.swift - deployment target increased from 12.0 to 13.0 - String extension getSchedule renamed to splitToInt
This commit is contained in:
parent
d764760e52
commit
3f2a021a51
|
@ -278,6 +278,8 @@
|
|||
F85FF3CD252F9FD7004E6FF1 /* SnoozeParameters+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85FF3CC252F9FD7004E6FF1 /* SnoozeParameters+CoreDataProperties.swift */; };
|
||||
F85FF3D1252F9FF9004E6FF1 /* SnoozeParameters+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85FF3D0252F9FF9004E6FF1 /* SnoozeParameters+CoreDataClass.swift */; };
|
||||
F85FF3D7252FB1C0004E6FF1 /* SnoozeParametersAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85FF3D6252FB1C0004E6FF1 /* SnoozeParametersAccessor.swift */; };
|
||||
F866974C28679A0100025441 /* LoopDelayScheduleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866974B28679A0100025441 /* LoopDelayScheduleViewController.swift */; };
|
||||
F86697502867AA4A00025441 /* LoopDelayScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866974F2867AA4A00025441 /* LoopDelayScheduleView.swift */; };
|
||||
F867E2612252ADAB000FD265 /* Calibration+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F867E25D2252ADAB000FD265 /* Calibration+CoreDataProperties.swift */; };
|
||||
F8691888239CEEFA0065B607 /* BluetoothPeripheralViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8691887239CEEFA0065B607 /* BluetoothPeripheralViewModel.swift */; };
|
||||
F869188C23A044340065B607 /* TextsM5StackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F869188B23A044340065B607 /* TextsM5StackView.swift */; };
|
||||
|
@ -1142,6 +1144,8 @@
|
|||
F85FF3CC252F9FD7004E6FF1 /* SnoozeParameters+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SnoozeParameters+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
F85FF3D0252F9FF9004E6FF1 /* SnoozeParameters+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SnoozeParameters+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
F85FF3D6252FB1C0004E6FF1 /* SnoozeParametersAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozeParametersAccessor.swift; sourceTree = "<group>"; };
|
||||
F866974B28679A0100025441 /* LoopDelayScheduleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopDelayScheduleViewController.swift; sourceTree = "<group>"; };
|
||||
F866974F2867AA4A00025441 /* LoopDelayScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopDelayScheduleView.swift; sourceTree = "<group>"; };
|
||||
F867E25D2252ADAB000FD265 /* Calibration+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Calibration+CoreDataProperties.swift"; path = "xdrip/Core Data/extensions/Calibration+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
|
||||
F8691887239CEEFA0065B607 /* BluetoothPeripheralViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothPeripheralViewModel.swift; sourceTree = "<group>"; };
|
||||
F869188B23A044340065B607 /* TextsM5StackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextsM5StackView.swift; sourceTree = "<group>"; };
|
||||
|
@ -2118,6 +2122,22 @@
|
|||
path = HouseKeeping;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F866974D2867A94D00025441 /* SwiftUIViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F866974E2867A97000025441 /* Settings */,
|
||||
);
|
||||
path = SwiftUIViews;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F866974E2867A97000025441 /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F866974F2867AA4A00025441 /* LoopDelayScheduleView.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F870D3D425126A49008967B0 /* xDrip4iOS Widget */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2242,8 +2262,6 @@
|
|||
F8AC425C21ADEBD60078C348 /* xdrip */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D4E499A9277B4363000F8CBA /* Treatments */,
|
||||
F8A5EEC5257EDC910085E660 /* xdripDebug.entitlements */,
|
||||
F85DC2FB21D2CD7000B9F74A /* Application Delegate */,
|
||||
F8F971AD23A5914C00C3F17D /* BluetoothPeripheral */,
|
||||
F8F971B923A5915900C3F17D /* BluetoothTransmitter */,
|
||||
|
@ -2255,12 +2273,15 @@
|
|||
F8025E5A21F7AFA100ECF0C0 /* Resources */,
|
||||
F85DC2F921D2CCC000B9F74A /* Storyboards */,
|
||||
F8025E5B21F7AFC600ECF0C0 /* Supporting Files */,
|
||||
F866974D2867A94D00025441 /* SwiftUIViews */,
|
||||
F8BDD44C221CAA26006EAB84 /* Texts */,
|
||||
D4E499A9277B4363000F8CBA /* Treatments */,
|
||||
F8EA6C8021B723A80082976B /* Utilities */,
|
||||
F85DC2FA21D2CD3000B9F74A /* View Controllers */,
|
||||
F8BECB00235CE3E20060DAE1 /* Views */,
|
||||
F8A54B0A22D9215500934E7A /* xdrip-Bridging-Header.h */,
|
||||
F821CF9822AE589E005C1E43 /* xdrip.entitlements */,
|
||||
F8A5EEC5257EDC910085E660 /* xdripDebug.entitlements */,
|
||||
);
|
||||
path = xdrip;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2487,6 +2508,7 @@
|
|||
F8B3A837227F090D004BA588 /* SettingsViewModels */,
|
||||
F8A389E6232ECE7E0010F405 /* SettingsViewUtilities.swift */,
|
||||
F830990423B94ED7005741DF /* TimeScheduleViewController.swift */,
|
||||
F866974B28679A0100025441 /* LoopDelayScheduleViewController.swift */,
|
||||
);
|
||||
path = SettingsViewController;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3575,6 +3597,7 @@
|
|||
F8C97853242AA70D00A09483 /* MiaoMiao+CoreDataClass.swift in Sources */,
|
||||
F8F9720D23A5915900C3F17D /* ResetMessage.swift in Sources */,
|
||||
F85FF3D7252FB1C0004E6FF1 /* SnoozeParametersAccessor.swift in Sources */,
|
||||
F866974C28679A0100025441 /* LoopDelayScheduleViewController.swift in Sources */,
|
||||
F8C97856242AA86B00A09483 /* CGMMiaoMiaoTransmitterDelegate.swift in Sources */,
|
||||
F898EDF6234A8A5700BFB79B /* UInt32.swift in Sources */,
|
||||
F8B955B1258BEE9D00C06016 /* ConstantsSpeakReading.swift in Sources */,
|
||||
|
@ -3771,6 +3794,7 @@
|
|||
F816E0F02433C31B009EE65B /* Blucon+CoreDataProperties.swift in Sources */,
|
||||
F8A2BC3925DB0D6D001D1E78 /* BluetoothPeripheralManager+CGMLibre2TransmitterDelegate.swift in Sources */,
|
||||
F8A5EEAE25791F370085E660 /* Libre2BLEUtilities.swift in Sources */,
|
||||
F86697502867AA4A00025441 /* LoopDelayScheduleView.swift in Sources */,
|
||||
F8E3C3AB21FE17B700907A04 /* StringProtocol.swift in Sources */,
|
||||
F8F9720723A5915900C3F17D /* AuthChallengeTxMessage.swift in Sources */,
|
||||
F8B3A78E22622954004BA588 /* AlertType+CoreDataClass.swift in Sources */,
|
||||
|
@ -4743,7 +4767,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
|
@ -4802,7 +4826,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -4824,7 +4848,7 @@
|
|||
CODE_SIGN_STYLE = "$(XDRIP_CODE_SIGN_STYLE)";
|
||||
DEVELOPMENT_TEAM = "$(XDRIP_DEVELOPMENT_TEAM)";
|
||||
INFOPLIST_FILE = "$(SRCROOT)/xdrip/Supporting Files/Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -4852,7 +4876,7 @@
|
|||
CODE_SIGN_STYLE = "$(XDRIP_CODE_SIGN_STYLE)";
|
||||
DEVELOPMENT_TEAM = "$(XDRIP_DEVELOPMENT_TEAM)";
|
||||
INFOPLIST_FILE = "$(SRCROOT)/xdrip/Supporting Files/Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
|
|
@ -1,8 +1,29 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
enum ConstantsUI {
|
||||
|
||||
// MARK: - SwiftUI Colors
|
||||
|
||||
/// color for section headers
|
||||
static let sectionHeaderColor = Color(UIColor.lightGray)
|
||||
|
||||
/// color for section footer
|
||||
static let sectionFooterColor = Color(UIColor.lightGray)
|
||||
|
||||
/// List background color
|
||||
static let listBackGroundUIColor = UIColor.black
|
||||
/// List background color
|
||||
static let listBackGroundColor = Color(listBackGroundUIColor)
|
||||
|
||||
/// color for cancel or dismiss button
|
||||
static let dismissOrCancelColor = Color(UIColor.red)
|
||||
|
||||
static let plusButtonColor = Color(UIColor.yellow)
|
||||
|
||||
// MARK: - Swift UIKit Colors
|
||||
|
||||
/// color for section titles in grouped table views, example in settings view
|
||||
static let tableViewHeaderTextColor = UIColor.lightGray
|
||||
|
||||
|
|
|
@ -172,7 +172,7 @@ extension String {
|
|||
/// - Example :
|
||||
/// - string = 5-480-660-1080
|
||||
/// - will return [5, 480, 660, 1080]
|
||||
func getSchedule() -> [Int] {
|
||||
func splitToInt() -> [Int] {
|
||||
|
||||
var schedule = [Int]()
|
||||
|
||||
|
|
|
@ -322,7 +322,9 @@ extension UserDefaults {
|
|||
/// to create artificial delay in readings stored in sharedUserDefaults for loop. Minutes - so that Loop receives more smoothed values.
|
||||
///
|
||||
/// Default value 0, if used then recommended value is multiple of 5 (eg 5 ot 10)
|
||||
case loopDelay = "loopDelay"
|
||||
case loopDelaySchedule = "loopDelaySchedule"
|
||||
|
||||
case loopDelayValueInMinutes = "loopDelayValueInMinutes"
|
||||
|
||||
/// used for Libre data parsing - only for Libre 1 or Libre 2 read via transmitter, ie full NFC block
|
||||
case previousRawLibreValues = "previousRawLibreValues"
|
||||
|
@ -436,12 +438,24 @@ extension UserDefaults {
|
|||
/// to create artificial delay in readings stored in sharedUserDefaults for loop. Minutes - so that Loop receives more smoothed values.
|
||||
///
|
||||
/// Default value 0, if used then recommended value is multiple of 5 (eg 5 ot 10)
|
||||
@objc dynamic var loopDelay: Int {
|
||||
@objc dynamic var loopDelaySchedule: String? {
|
||||
get {
|
||||
return integer(forKey: Key.loopDelay.rawValue)
|
||||
return string(forKey: Key.loopDelaySchedule.rawValue)
|
||||
}
|
||||
set {
|
||||
set(newValue, forKey: Key.loopDelay.rawValue)
|
||||
set(newValue, forKey: Key.loopDelaySchedule.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
/// to create artificial delay in readings stored in sharedUserDefaults for loop. Minutes - so that Loop receives more smoothed values.
|
||||
///
|
||||
/// Default value 0, if used then recommended value is multiple of 5 (eg 5 ot 10)
|
||||
@objc dynamic var loopDelayValueInMinutes: String? {
|
||||
get {
|
||||
return string(forKey: Key.loopDelayValueInMinutes.rawValue)
|
||||
}
|
||||
set {
|
||||
set(newValue, forKey: Key.loopDelayValueInMinutes.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -62,17 +62,16 @@ public class LoopManager:NSObject {
|
|||
|
||||
}
|
||||
|
||||
// if needed, remove readings less than loopDelay minutes old
|
||||
if UserDefaults.standard.loopDelay > 0 {
|
||||
|
||||
trace(" loopDelay > 0. Deleting readings",log: log, category: ConstantsLog.categoryLoopManager, type: .info)
|
||||
|
||||
while lastReadings.count > 0 && lastReadings[0].timeStamp.addingTimeInterval(TimeInterval(minutes: Double(UserDefaults.standard.loopDelay))) > Date() {
|
||||
// calculate loopDelay, to avoid having to do it multiple times
|
||||
let loopDelay = loopDelay()
|
||||
|
||||
// if needed, remove readings less than loopDelay minutes old
|
||||
if loopDelay > 0 {
|
||||
|
||||
trace(" loopDelay= %{public}@. Deleting readings.",log: log, category: ConstantsLog.categoryLoopManager, type: .info, loopDelay.description)
|
||||
|
||||
while lastReadings.count > 0 && lastReadings[0].timeStamp.addingTimeInterval(loopDelay) > Date() {
|
||||
|
||||
trace(" removing reading with timestamp %{public}@", log: log, category: ConstantsLog.categoryLoopManager, type: .info, lastReadings[0].timeStamp.toString(timeStyle: .long, dateStyle: .long))
|
||||
trace(" value %{public}@", log: log, category: ConstantsLog.categoryLoopManager, type: .info, lastReadings[0].calculatedValue.description)
|
||||
trace("", log: log, category: ConstantsLog.categoryLoopManager, type: .info)
|
||||
|
||||
lastReadings.remove(at: 0)
|
||||
|
||||
}
|
||||
|
@ -94,7 +93,7 @@ public class LoopManager:NSObject {
|
|||
}
|
||||
|
||||
// now, if needed, increase the timestamp for each reading
|
||||
if UserDefaults.standard.loopDelay > 0 {
|
||||
if loopDelay > 0 {
|
||||
|
||||
// create new dictionary that will have the readings with timestamp increased
|
||||
var newDictionary = [Dictionary<String, Any>]()
|
||||
|
@ -116,7 +115,7 @@ public class LoopManager:NSObject {
|
|||
if let readingTimeStamp = readingTimeStamp, let slopeOrdinal = reading["Trend"] as? Int, let value = reading["Value"] as? Double {
|
||||
|
||||
// create new date : original date + loopDelay
|
||||
let newReadingTimeStamp = readingTimeStamp.addingTimeInterval(TimeInterval(minutes: Double(UserDefaults.standard.loopDelay)))
|
||||
let newReadingTimeStamp = readingTimeStamp.addingTimeInterval(loopDelay)
|
||||
|
||||
// ignore the reading if newReadingTimeStamp > now
|
||||
if newReadingTimeStamp < Date() {
|
||||
|
@ -135,10 +134,6 @@ public class LoopManager:NSObject {
|
|||
]
|
||||
|
||||
newDictionary.append(newReading)
|
||||
|
||||
trace(" adding reading with timestamp %{public}@", log: log, category: ConstantsLog.categoryLoopManager, type: .info, newReadingTimeStamp.toString(timeStyle: .long, dateStyle: .long))
|
||||
trace(" value %{public}@", log: log, category: ConstantsLog.categoryLoopManager, type: .info, value.description)
|
||||
trace("", log: log, category: ConstantsLog.categoryLoopManager, type: .info)
|
||||
|
||||
}
|
||||
|
||||
|
@ -185,7 +180,7 @@ public class LoopManager:NSObject {
|
|||
UserDefaults.standard.timeStampLatestLoopSharedBgReading = lastReadings.first!.timeStamp.addingTimeInterval(5.0)
|
||||
|
||||
// in case loopdelay is used, then update UserDefaults.standard.timeStampLatestLoopSharedBgReading with value of timestamp of first element in the dictionary
|
||||
if let element = dictionary.first, UserDefaults.standard.loopDelay > 0 {
|
||||
if let element = dictionary.first, loopDelay > 0 {
|
||||
|
||||
if let elementDateAsString = element["DT"] as? String {
|
||||
|
||||
|
@ -202,6 +197,8 @@ public class LoopManager:NSObject {
|
|||
|
||||
}
|
||||
|
||||
// MARK: - private functions
|
||||
|
||||
private func parseTimestamp(_ timestamp: String) throws -> Date? {
|
||||
let regex = try NSRegularExpression(pattern: "\\((.*)\\)")
|
||||
if let match = regex.firstMatch(in: timestamp, range: NSMakeRange(0, timestamp.count)) {
|
||||
|
@ -211,4 +208,54 @@ public class LoopManager:NSObject {
|
|||
return nil
|
||||
}
|
||||
|
||||
/// calculate loop delay to use dependent on the time of the day, based on UserDefaults loopDelaySchedule and loopDelayValueInMinutes
|
||||
///
|
||||
/// finds element in loopDelaySchedule with value > actual minutes and uses previous element in loopDelayValueInMinutes as value to use as loopDelay
|
||||
private func loopDelay() -> TimeInterval {
|
||||
|
||||
// loopDelaySchedule is array of ints, giving minutes starting at 00:00 as of which new value for loopDelay should be used
|
||||
// if nil then user didn't set yet any value
|
||||
guard let loopDelaySchedule = UserDefaults.standard.loopDelaySchedule else {return TimeInterval(0)}
|
||||
|
||||
// split in array of Int
|
||||
let loopDelayScheduleArray = loopDelaySchedule.splitToInt()
|
||||
|
||||
// array size should be > 0
|
||||
guard loopDelaySchedule.count > 0 else {return TimeInterval(0)}
|
||||
|
||||
// loopDelayValueInMinutes is array of ints, giving values to be applied as loopdelay, for matching minutes values in loopDelaySchedule
|
||||
guard let loopDelayValueInMinutes = UserDefaults.standard.loopDelayValueInMinutes else {return TimeInterval(0)}
|
||||
|
||||
// splity in array of int
|
||||
let loopDelayValueInMinutesArray = loopDelayValueInMinutes.splitToInt()
|
||||
|
||||
// array size should be > 0, and size should be equal to size of loopDelayScheduleArray
|
||||
guard loopDelayValueInMinutesArray.count > 0, loopDelayScheduleArray.count == loopDelayValueInMinutesArray.count else {return TimeInterval(0)}
|
||||
|
||||
// minutes since midnight
|
||||
let minutes = Int16(Date().minutesSinceMidNightLocalTime())
|
||||
|
||||
// index in loopDelaySchedule and loopDelayValueInMinutes, start with first value
|
||||
var indexInLoopDelayScheduleArray = 0
|
||||
|
||||
// loop through Ints in loopDelayScheduleArray, until value > current minutes
|
||||
for (index, schedule) in loopDelayScheduleArray.enumerated() {
|
||||
|
||||
if schedule > minutes {
|
||||
break
|
||||
}
|
||||
|
||||
if index < loopDelayScheduleArray.count - 1 {
|
||||
if loopDelayScheduleArray[index + 1] > minutes {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
indexInLoopDelayScheduleArray = indexInLoopDelayScheduleArray + 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return TimeInterval(minutes: Double(loopDelayValueInMinutesArray[indexInLoopDelayScheduleArray]))
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1197,6 +1197,7 @@
|
|||
<segue destination="eUt-Xg-tDZ" kind="show" identifier="settingsToAlertTypeSettings" id="dX4-1s-1fK"/>
|
||||
<segue destination="d2Q-Tq-3zC" kind="show" identifier="settingsToM5StackSettings" id="wOc-Vw-59X"/>
|
||||
<segue destination="Yrn-pr-SQ1" kind="show" identifier="settingsToSchedule" id="GkN-Bx-7f5"/>
|
||||
<segue destination="bzY-sy-ivl" kind="show" identifier="settingsToLoopDelaySchedule" id="Yl3-hC-05i"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="4Nw-L8-lE0" sceneMemberID="firstResponder"/>
|
||||
|
@ -1805,6 +1806,22 @@
|
|||
</objects>
|
||||
<point key="canvasLocation" x="3971" y="1500"/>
|
||||
</scene>
|
||||
<!--Loop Delay Schedule View Controller-->
|
||||
<scene sceneID="I0j-hn-Oz3">
|
||||
<objects>
|
||||
<viewController id="bzY-sy-ivl" userLabel="Loop Delay Schedule View Controller" customClass="LoopDelayScheduleViewController" customModule="xdrip" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="rPP-0H-ZN9">
|
||||
<rect key="frame" x="0.0" y="0.0" width="390" height="844"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<viewLayoutGuide key="safeArea" id="rTK-eV-Clg"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
</view>
|
||||
<navigationItem key="navigationItem" title="Title" id="D3W-pP-Yk9"/>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="cdK-b9-772" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="4794" y="1500"/>
|
||||
</scene>
|
||||
<!--Tab Bar Controller-->
|
||||
<scene sceneID="yl2-sM-qoP">
|
||||
<objects>
|
||||
|
@ -2197,6 +2214,9 @@
|
|||
<image name="questionmark.circle" catalog="system" width="128" height="121"/>
|
||||
<image name="scope" catalog="system" width="128" height="122"/>
|
||||
<image name="sensor14_14_alt" width="390" height="14"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemBlueColor">
|
||||
<color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
|
|
|
@ -123,3 +123,8 @@
|
|||
"settingsviews_housekeeperRetentionPeriodMessage" = "For how many days should data be stored? (Min 90, Max 365)\n\n(Recommended: 90 days)";
|
||||
"suppressUnLockPayLoad" = "Suppress Unlock Payload";
|
||||
"suppressLoopShare" = "Suppress Loop Share";
|
||||
"Select Time" = "Select Time";
|
||||
"Select Value" = "Select Value";
|
||||
"expanatoryTextSelectTime" = "As of what time should the value apply";
|
||||
"expanatoryTextSelectValue" = "Delay in minutes, applied to readings shared with Loop";
|
||||
"warningLoopDelayAlreadyExists" = "There is already a loopDelay for this time.";
|
||||
|
|
|
@ -0,0 +1,317 @@
|
|||
import SwiftUI
|
||||
|
||||
struct LoopDelayScheduleView: View {
|
||||
|
||||
/// maximum amount of values, if 5, then possible delay values to choose form will be 0, 5, 10, 15, 20
|
||||
private static let maximumAmountOfValues:Int = 5
|
||||
|
||||
/// will store two arrays, one with loopdelayschedules (timestamps between 00:00 at 23:59 in minutes), one with loopdelayvalues in minutes
|
||||
@State private var loopDelays:[(Int, Int)] = [(Int, Int)]()
|
||||
|
||||
/// state variable, if true then view is shown to add a new row or updating an existing row
|
||||
@State private var addMode = false
|
||||
|
||||
/// index in loopDelays, points to loopDelay being updated. Used to update a loopDelay. If nil then user is adding a new loopDelay, if not nil then user is updating a loopDelay
|
||||
///
|
||||
/// add @State property wrapper because the value is changed
|
||||
@State private var loopDelayToUpdate:Int?
|
||||
|
||||
/// used in sheet that allows to add or update a loop delay row : delay selected
|
||||
@State private var selectedDelay:Int = 0
|
||||
|
||||
/// used in DatePicker to add or update a loop delay row - this is the timestamp
|
||||
@State private var selectedDate:Date = Date()
|
||||
|
||||
/// state variable to control display of alert
|
||||
@State private var duplicateLoopDelayAlertIsPresented = false
|
||||
|
||||
/// used in conjunction with duplicateLoopDelayAlertIsPresented
|
||||
///
|
||||
/// setting duplicateLoopDelayAlertIsPresented to true while the add sheet is being presented, doesn't show the alert. Seems solution is as described here https://stackoverflow.com/questions/63968344/swiftui-how-to-show-an-alert-after-a-sheet-is-closed, which is to show the alert when the sheet is dismissed
|
||||
@State private var showLoopDelayAlertOnDismiss = false
|
||||
|
||||
init() {
|
||||
|
||||
// setup colors etc.
|
||||
setupView()
|
||||
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
NavigationView {
|
||||
|
||||
List {
|
||||
|
||||
ForEach(loopDelays, id: \.self.0) { loopDelay in
|
||||
|
||||
HStack {
|
||||
|
||||
// example 320 is converted to 05:20
|
||||
Text(loopDelay.0.convertMinutesToTimeAsString())
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(loopDelay.1.description)
|
||||
|
||||
}
|
||||
|
||||
.contentShape(Rectangle()) //to ensure onTapGesture works also on the Spacer in the HStack, see https://www.hackingwithswift.com/quick-start/swiftui/how-to-control-the-tappable-area-of-a-view-using-contentshape
|
||||
|
||||
.onTapGesture {
|
||||
|
||||
// find index of loopDelay that is clicked, and assign to loopDelayToUpdate
|
||||
loopDelayToUpdate = {
|
||||
for (index, entry) in loopDelays.enumerated() {
|
||||
if entry.0 == loopDelay.0 {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
// loopDelayToUpdate should not be nil, otherwise there's a coding error
|
||||
guard let loopDelayToUpdate = loopDelayToUpdate else {return}
|
||||
|
||||
selectedDelay = loopDelays[loopDelayToUpdate].1 / 5
|
||||
|
||||
let nowAt000 = Date().toMidnight()
|
||||
|
||||
selectedDate = Date(timeInterval: TimeInterval(Double(loopDelays[loopDelayToUpdate].0) * 60.0), since: nowAt000)
|
||||
|
||||
// show the sheet that allows to udpate
|
||||
addMode = true
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// to delete rows
|
||||
.onDelete(perform: delete)
|
||||
|
||||
}
|
||||
|
||||
// Open the sheet to add a new row, when clicking the plus
|
||||
.navigationBarItems(trailing: Button(action: {addMode = true}) { Image(systemName: "plus").foregroundColor(ConstantsUI.plusButtonColor) })
|
||||
|
||||
// sheet to add a new row
|
||||
.sheet(isPresented: $addMode, onDismiss: {
|
||||
|
||||
if showLoopDelayAlertOnDismiss {
|
||||
|
||||
showLoopDelayAlertOnDismiss = false
|
||||
|
||||
// add a small delay before setting duplicateLoopDelayAlertIsPresented to true, as explained here https://stackoverflow.com/a/71638878
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
duplicateLoopDelayAlertIsPresented = true
|
||||
}
|
||||
|
||||
}
|
||||
}, content: {
|
||||
|
||||
Form {
|
||||
|
||||
Section(header: Text(Texts_SettingsView.selectTime)
|
||||
.foregroundColor(ConstantsUI.sectionHeaderColor),
|
||||
|
||||
footer: Text(Texts_SettingsView.expanatoryTextSelectTime)
|
||||
.foregroundColor(ConstantsUI.sectionFooterColor)) {
|
||||
|
||||
DatePicker("Label hidden", selection: $selectedDate, displayedComponents: .hourAndMinute)
|
||||
.labelsHidden()
|
||||
|
||||
}
|
||||
|
||||
Section(header: Text(Texts_SettingsView.selectValue)
|
||||
.foregroundColor(ConstantsUI.sectionHeaderColor),
|
||||
|
||||
footer: Text(Texts_SettingsView.expanatoryTextSelectValue)
|
||||
.foregroundColor(ConstantsUI.sectionFooterColor)) {
|
||||
|
||||
Picker("Label hidden", selection: $selectedDelay) {
|
||||
ForEach((0...(LoopDelayScheduleView.maximumAmountOfValues - 1)), id: \.self) {
|
||||
Text("\($0*5)")
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.labelsHidden()
|
||||
|
||||
}
|
||||
|
||||
HStack(alignment: VerticalAlignment.center, spacing: nil) {
|
||||
|
||||
Button(action: {
|
||||
resetStateVariables()
|
||||
}, label: {
|
||||
Text(Texts_Common.Cancel)
|
||||
.foregroundColor(ConstantsUI.dismissOrCancelColor)
|
||||
})
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
addOrUpdateRow()
|
||||
}, label: {
|
||||
Text(Texts_Common.Ok)
|
||||
})
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
.alert(isPresented: $duplicateLoopDelayAlertIsPresented, content: {
|
||||
|
||||
// after dismissing the alert, then reopen the sheet
|
||||
Alert(title: Text(Texts_SettingsView.warningLoopDelayAlreadyExists), dismissButton: .default(Text(Texts_Common.Ok)) {
|
||||
|
||||
// add a small delay before setting to true, as explained here https://stackoverflow.com/a/71638878
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
addMode = true
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
.onAppear() {
|
||||
|
||||
// initialize loopDelays
|
||||
initializeLoopDelays()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func delete(at offsets: IndexSet) {
|
||||
|
||||
// there should be a first element, otherwise deletion would not be possible
|
||||
guard let first = offsets.first else { return }
|
||||
|
||||
// only delete the first element in offsets (don't know if there can be many)
|
||||
loopDelays.remove(at: first)
|
||||
|
||||
}
|
||||
|
||||
/// intialize the state variable loopDelays based on contents in UserDefaults
|
||||
private func initializeLoopDelays() {
|
||||
|
||||
if loopDelays.count == 0, let storedloopDelaySchedule = UserDefaults.standard.loopDelaySchedule?.splitToInt(), let storedloopDelayValues = UserDefaults.standard.loopDelayValueInMinutes?.splitToInt() , storedloopDelaySchedule.count == storedloopDelayValues.count {
|
||||
|
||||
for (index, _) in storedloopDelaySchedule.enumerated() {
|
||||
|
||||
loopDelays.insert((storedloopDelaySchedule[index], storedloopDelayValues[index]), at: 0)
|
||||
|
||||
}
|
||||
|
||||
loopDelays = loopDelays.sorted(by: {$0.0 < $1.0})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// setup colors etc.
|
||||
private func setupView() {
|
||||
|
||||
// background color
|
||||
UITableView.appearance().backgroundColor = ConstantsUI.listBackGroundUIColor
|
||||
|
||||
}
|
||||
|
||||
/// update loopDelays based on State variables selectedDate and selectedDelay
|
||||
private func addOrUpdateRow() {
|
||||
|
||||
// calculate the number of minutes since midnight
|
||||
let minutesSinceMidnight = selectedDate.minutesSinceMidNightLocalTime()
|
||||
|
||||
// check if there's already an entry with the same value for minutes
|
||||
for (index, loopDelay) in loopDelays.enumerated() {
|
||||
|
||||
if loopDelay.0 == minutesSinceMidnight && index != loopDelayToUpdate {
|
||||
showLoopDelayAlertOnDismiss = true
|
||||
addMode = false
|
||||
return
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// sort loopDelays array by minutes before leaving the function
|
||||
// and remove the sheet that allows to create/update a scheduled entry
|
||||
defer {
|
||||
|
||||
sortAndStoreLoopDelaysInUserDefaults()
|
||||
|
||||
}
|
||||
|
||||
// if it's a loopdelay being updated, then update it
|
||||
if let loopDelayToUpdate = loopDelayToUpdate {
|
||||
|
||||
loopDelays[loopDelayToUpdate].0 = minutesSinceMidnight
|
||||
loopDelays[loopDelayToUpdate].1 = selectedDelay * 5
|
||||
|
||||
} else {
|
||||
|
||||
// create a new loopDelay and add it to loopDelays array
|
||||
loopDelays.append((minutesSinceMidnight, selectedDelay * 5))
|
||||
|
||||
}
|
||||
|
||||
resetStateVariables()
|
||||
|
||||
}
|
||||
|
||||
/// sort loopDelays array, and store current values of loopDelays in both UserDefaults.standard.loopDelaySchedule and UserDefaults.standard.loopDelayValueInMinutes
|
||||
private func sortAndStoreLoopDelaysInUserDefaults() {
|
||||
|
||||
loopDelays = loopDelays.sorted(by: {$0.0 < $1.0})
|
||||
|
||||
// store loopDelays in UserDefaults
|
||||
|
||||
var scheduleToStore: String?
|
||||
var delaysToStore: String?
|
||||
|
||||
for loopDelay in loopDelays {
|
||||
|
||||
if scheduleToStore == nil {
|
||||
|
||||
scheduleToStore = loopDelay.0.description
|
||||
delaysToStore = loopDelay.1.description
|
||||
|
||||
} else {
|
||||
|
||||
scheduleToStore = scheduleToStore! + "-" + loopDelay.0.description
|
||||
delaysToStore = delaysToStore! + "-" + loopDelay.1.description
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
UserDefaults.standard.loopDelaySchedule = scheduleToStore
|
||||
UserDefaults.standard.loopDelayValueInMinutes = delaysToStore
|
||||
|
||||
}
|
||||
|
||||
/// reset addMode to false, selectedDate to now, selectedDelay to 0, loopDelayToUpdate to nil
|
||||
private func resetStateVariables() {
|
||||
|
||||
addMode = false
|
||||
|
||||
loopDelayToUpdate = nil
|
||||
|
||||
selectedDate = Date()
|
||||
|
||||
selectedDelay = 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct LoopDelayScheduleView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
LoopDelayScheduleView()
|
||||
}
|
||||
}
|
|
@ -523,6 +523,30 @@ class Texts_SettingsView {
|
|||
return NSLocalizedString("suppressLoopShare", tableName: filename, bundle: Bundle.main, value: "Suppress Loop Share", comment: "When enabled, readings will not be reading to shared user defaults (for loop)")
|
||||
}()
|
||||
|
||||
static let selectTime: String = {
|
||||
return NSLocalizedString("Select Time", tableName: filename, bundle: Bundle.main, value: "Select Time", comment: "Settings screen for loop delay")
|
||||
}()
|
||||
|
||||
static let expanatoryTextSelectTime: String = {
|
||||
return NSLocalizedString("expanatoryTextSelectTime", tableName: filename, bundle: Bundle.main, value: "As of what time should the value apply", comment: "Settings screen for loop delay, explanatory text for time")
|
||||
}()
|
||||
|
||||
static let selectValue: String = {
|
||||
return NSLocalizedString("Select Value", tableName: filename, bundle: Bundle.main, value: "Select Value", comment: "Settings screen for loop delay")
|
||||
}()
|
||||
|
||||
static let loopDelaysScreenTitle: String = {
|
||||
return NSLocalizedString("loopDelaysScreenTitle", tableName: filename, bundle: Bundle.main, value: "Loop delays", comment: "Title for screen where loop delays are configured.")
|
||||
}()
|
||||
|
||||
static let expanatoryTextSelectValue: String = {
|
||||
return NSLocalizedString("expanatoryTextSelectValue", tableName: filename, bundle: Bundle.main, value: "Delay in minutes, applied to readings shared with Loop", comment: "Settings screen for loop delay, explanatory text for value")
|
||||
}()
|
||||
|
||||
static let warningLoopDelayAlreadyExists: String = {
|
||||
return NSLocalizedString("warningLoopDelayAlreadyExists", tableName: filename, bundle: Bundle.main, value: "There is already a loopDelay for this time.", comment: "When user creates new loopdelay, with a timestamp that already exists - this is the warning text")
|
||||
}()
|
||||
|
||||
static let nsLog: String = {
|
||||
return NSLocalizedString("nslog", tableName: filename, bundle: Bundle.main, value: "NSLog", comment: "deloper settings, row title for NSLog - with NSLog enabled, a developer can view log information as explained here https://github.com/JohanDegraeve/xdripswift/wiki/NSLog")
|
||||
}()
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import SwiftUI
|
||||
|
||||
/// to configure loop delays, time + value to use
|
||||
///
|
||||
/// see https://medium.com/@max.codes/use-swiftui-in-uikit-view-controllers-with-uihostingcontroller-8fe68dfc523b
|
||||
final class LoopDelayScheduleViewController: UIViewController {
|
||||
|
||||
// will use SwiftUI - UIHostingController allows to use SwiftUi in UIKit project
|
||||
let loopDelayScheduleContentView = UIHostingController(rootView: LoopDelayScheduleView())
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
addChild(loopDelayScheduleContentView)
|
||||
view.addSubview(loopDelayScheduleContentView.view)
|
||||
|
||||
title = Texts_SettingsView.loopDelaysScreenTitle
|
||||
|
||||
setupConstraints()
|
||||
}
|
||||
|
||||
private func setupConstraints() {
|
||||
loopDelayScheduleContentView.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
loopDelayScheduleContentView.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
|
||||
loopDelayScheduleContentView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
|
||||
loopDelayScheduleContentView.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
|
||||
loopDelayScheduleContentView.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +121,7 @@ final class SettingsViewController: UIViewController {
|
|||
}
|
||||
|
||||
|
||||
// MARK:- public functions
|
||||
// MARK: - public functions
|
||||
|
||||
/// configure
|
||||
public func configure(coreDataManager:CoreDataManager?, soundPlayer:SoundPlayer?) {
|
||||
|
@ -233,7 +233,11 @@ final class SettingsViewController: UIViewController {
|
|||
if let vc = segue.destination as? TimeScheduleViewController, let sender = sender as? TimeSchedule {
|
||||
vc.configure(timeSchedule: sender)
|
||||
}
|
||||
|
||||
|
||||
case .settingsToLoopDelaySchedule:
|
||||
//nothing to configure
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -339,6 +343,9 @@ extension SettingsViewController {
|
|||
/// to go from general settings to schedule screen
|
||||
case settingsToSchedule = "settingsToSchedule"
|
||||
|
||||
/// to go from general settings to loop delay schedule
|
||||
case settingsToLoopDelaySchedule = "settingsToLoopDelaySchedule"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ struct SettingsViewDevelopmentSettingsViewModel:SettingsViewModelProtocol {
|
|||
return Texts_SettingsView.suppressLoopShare
|
||||
|
||||
case .loopDelay:
|
||||
return "Loop Delay"
|
||||
return Texts_SettingsView.loopDelaysScreenTitle
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ struct SettingsViewDevelopmentSettingsViewModel:SettingsViewModelProtocol {
|
|||
return nil
|
||||
|
||||
case .loopDelay:
|
||||
return UserDefaults.standard.loopDelay.description
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
|
@ -175,7 +175,7 @@ struct SettingsViewDevelopmentSettingsViewModel:SettingsViewModelProtocol {
|
|||
return .nothing
|
||||
|
||||
case .loopDelay:
|
||||
return SettingsSelectedRowAction.askText(title: "Loop Delay", message: "Artificial delay in readings when sending to Loop (minutes) - 0 means no delay. Use maximum 10 minutes.", keyboardType: .numberPad, text: UserDefaults.standard.loopDelay.description, placeHolder: "0", actionTitle: nil, cancelTitle: nil, actionHandler: {(interval:String) in if let interval = Int(interval) {UserDefaults.standard.loopDelay = Int(interval)}}, cancelHandler: nil, inputValidator: nil)
|
||||
return .performSegue(withIdentifier: SettingsViewController.SegueIdentifiers.settingsToLoopDelaySchedule.rawValue, sender: self)
|
||||
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue