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:
Johan Degraeve 2022-06-26 21:06:05 +02:00
parent d764760e52
commit 3f2a021a51
12 changed files with 543 additions and 33 deletions

View File

@ -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",

View File

@ -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

View File

@ -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]()

View File

@ -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)
}
}

View File

@ -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]))
}
}

View File

@ -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>

View File

@ -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.";

View File

@ -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()
}
}

View File

@ -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")
}()

View File

@ -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
}
}

View File

@ -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"
}
}

View File

@ -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)
}