user locale implemented for all UI date and time displays

- chart hour axis label and scroll back timestamp will be shown in user locale
- treatment list time will be shown in user locale
- snooze screen times and dates will be shown in user locale
- BLE peripherals dates and times will be shown in user locale
- alarm dates and times will be shown in user locale
- landscape view date will be shown in user locale
- small improvements to layout of alarms screen
- schema updated to include location and allow region changes in simulator/debug
This commit is contained in:
Paul Plant 2022-06-05 20:54:24 +02:00
parent f27b691c1d
commit a4b8e67ff1
17 changed files with 119 additions and 59 deletions

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1310"
version = "1.3">
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
@ -66,6 +66,10 @@
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
<LocationScenarioReference
identifier = "London, England"
referenceType = "1">
</LocationScenarioReference>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"

View File

@ -5,11 +5,6 @@ enum ConstantsGlucoseChart {
/// default value for glucosechart width in hours
static let defaultChartWidthInHours = 6.0;
/// default value for timeformat for labels in chart, time axis
/// H is hour 24 hour format, "h a" is hour 12 hour format with a either am or pm
/// options can be "H", "HH", "HH:00"
static let defaultTimeAxisLabelFormat = "HH"
/// usually 40.0 mgdl is the lowest value that cgm's give, putting it to 38 guarantees the points will always be visible
/// only in mgdl because the label will not be shown, hence no bizar values to be shown when going to mgdl
static let absoluteMinimumChartValueInMgdl = 38.0
@ -239,7 +234,7 @@ enum ConstantsGlucoseChart {
static let maximumElementsInGlucoseChartPointsArray:Int = 1000
/// dateformat for minutesAgo label when user is panning the chart back in time. The label will show the timestamp of the latest shown value in the chart
static let dateFormatLatestChartPointWhenPanning = "E d MMM HH:mm"
static let dateFormatLatestChartPointWhenPanning = "E d MMM jj:mm"
/// dateformat for the date label in the 24 hours static landscape chart
static let dateFormatLandscapeChart = "EEEE dd/MM/yyyy"

View File

@ -38,4 +38,19 @@ enum ConstantsUI {
/// clock label font size (ideally should be set to the same as the bigger valueLabel font size
static let clockLabelFontSize = UIFont.systemFont(ofSize: 120)
/// time format for displaying just the hour
static let timeFormatHoursOnly = "j"
/// time format for displaying hours and minutes
static let timeFormatHoursMins = "jj:mm"
/// date format for displaying the full short date
static let dateFormatDayMonthYear = "dd/MM/yyyy"
/// string to be used to show am time if the user locale shows a 12 hour clock
static let timeFormatAM = "am"
/// string to be used to show pm time if the user locale shows a 12 hour clock
static let timeFormatPM = "pm"
}

View File

@ -102,6 +102,19 @@ extension Date {
return dateFormatter.string(from: self)
}
/// date to string, with date and time as specified by one of the values in DateFormatter.Style and formatted to match the user's locale
/// Example return: "31/12/2022, 17:48" (spain locale)
/// Example return: "12/31/2022, 5:48 pm" (us locale)
func toStringInUserLocale(timeStyle: DateFormatter.Style, dateStyle: DateFormatter.Style) -> String {
let dateFormatter = DateFormatter()
dateFormatter.timeStyle = timeStyle
dateFormatter.dateStyle = dateStyle
dateFormatter.amSymbol = ConstantsUI.timeFormatAM
dateFormatter.pmSymbol = ConstantsUI.timeFormatPM
dateFormatter.setLocalizedDateFormatFromTemplate("dd/MM/yyyy, jj:mm")
return dateFormatter.string(from: self)
}
/// returns seconds since 1.1.1970 local time for current timezone
func toSecondsAsInt64Local() -> Int64 {
let calendar = Calendar.current
@ -114,4 +127,28 @@ extension Date {
(timeIntervalSinceReferenceDate / 3600.0).rounded(.down) * 3600.0)
}
/// returns the Nightscout style string showing the days and hours since a date (e.g. "6d11h")
/// Example return: "6d11h" if optional appendAgo is false or not used
/// Example return: "6d11h ago" if optional appendAgo is true
func daysAndHoursAgo(appendAgo: Bool? = false) -> String {
// set a default value assuming that we're unable to calculate the hours + days
var daysAndHoursAgoString: String = "n/a"
let diffComponents = Calendar.current.dateComponents([.day, .hour], from: self, to: Date())
if let days = diffComponents.day, let hours = diffComponents.hour {
daysAndHoursAgoString = days.description + "d" + hours.description + "h"
// if the function was called using appendAgo == true, then add the "ago" string
if appendAgo ?? false {
daysAndHoursAgoString += " " + Texts_HomeView.ago
}
}
return daysAndHoursAgoString
}
}

View File

@ -2,17 +2,30 @@ import Foundation
extension Int {
/// example value 320 minutes is 5 hours and 20 minutes, would be converted to 05:20
/// this is then returned as a time string as per the user's locale and region
/// Example return: "17:48" (spain locale)
/// Example return: "5:48 pm" (us locale)
func convertMinutesToTimeAsString() -> String {
let hours = (self / 60)
let minutes = self - hours * 60
let minutes = self - hours * 60
// create calendar object
let calendar = Calendar.current
var hoursAsString = String(describing: hours)
var minutesAsString = String(describing: minutes)
// create a date based upon today's date (it could be any date as we will ignore it later) and set the hours and minutes
let date = calendar.date(bySettingHour: hours, minute: minutes, second: 0, of: Date())
let dateFormatter = DateFormatter()
if hoursAsString.count == 1 {hoursAsString = "0" + hoursAsString}
if minutesAsString.count == 1 {minutesAsString = "0" + minutesAsString}
dateFormatter.amSymbol = ConstantsUI.timeFormatAM
dateFormatter.pmSymbol = ConstantsUI.timeFormatPM
dateFormatter.setLocalizedDateFormatFromTemplate("jj:mm")
return dateFormatter.string(from: date!)
return hoursAsString + ":" + minutesAsString
}
/// converts Int to array of UInt8 - (probably only works for positive values <= 2147483647 ?)

View File

@ -8,9 +8,6 @@ extension UserDefaults {
/// chart width in hours
case chartWidthInHours = "chartWidthInHours"
/// timeformat for labels in chart, time axis
case chartTimeAxisLabelFormat = "chartTimeAxisLabelFormat"
}
/// chart width in hours
@ -30,18 +27,5 @@ extension UserDefaults {
set(newValue, forKey: KeysCharts.chartWidthInHours.rawValue)
}
}
/// timeformat for labels in chart, time axis
@objc dynamic var chartTimeAxisLabelFormat:String {
get {
if let returnValue = string(forKey: KeysCharts.chartTimeAxisLabelFormat.rawValue) {
return returnValue
} else {
return ConstantsGlucoseChart.defaultTimeAxisLabelFormat
}
}
set {
set(newValue, forKey: KeysCharts.chartTimeAxisLabelFormat.rawValue)
}
}
}

View File

@ -1458,10 +1458,12 @@ public class GlucoseChartManager {
chartGuideLinesLayerSettings = ChartGuideLinesLayerSettings(linesColor: UserDefaults.standard.useObjectives ? ConstantsGlucoseChart.gridColorObjectives : ConstantsGlucoseChart.gridColor, linesWidth: 0.5)
}
// intialize axisLabelTimeFormatter
// intialize axisLabelTimeFormatter to use the user's locale and region settings
if axisLabelTimeFormatter == nil {
axisLabelTimeFormatter = DateFormatter()
axisLabelTimeFormatter!.dateFormat = UserDefaults.standard.chartTimeAxisLabelFormat
axisLabelTimeFormatter!.amSymbol = ConstantsUI.timeFormatAM
axisLabelTimeFormatter!.pmSymbol = ConstantsUI.timeFormatPM
axisLabelTimeFormatter!.setLocalizedDateFormatFromTemplate(ConstantsUI.timeFormatHoursOnly)
}
// initialize bgReadingsAccessor

View File

@ -175,20 +175,23 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00:00" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" adjustsFontForContentSizeCategory="YES" translatesAutoresizingMaskIntoConstraints="NO" id="JuE-mg-S0t">
<rect key="frame" x="19.999999999999996" y="3.3333333333333339" width="43.666666666666657" height="19.333333333333329"/>
<rect key="frame" x="4" y="3.3333333333333339" width="75" height="19.333333333333329"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="75" id="fJv-mh-fXl"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="highlightedColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" alpha="0.80000000000000004" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="v0f-JH-O27">
<rect key="frame" x="80" y="6" width="14" height="14"/>
<rect key="frame" x="95" y="6" width="14" height="14"/>
<imageReference key="image" image="circle.fill" catalog="system" symbolScale="small"/>
<preferredSymbolConfiguration key="preferredSymbolConfiguration" configurationType="font" scale="small" weight="regular">
<fontDescription key="fontDescription" type="system" pointSize="15"/>
</preferredSymbolConfiguration>
</imageView>
<label opaque="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Insulin" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" adjustsFontForContentSizeCategory="YES" translatesAutoresizingMaskIntoConstraints="NO" id="U6e-jp-cms">
<rect key="frame" x="110.00000000000001" y="3.3333333333333339" width="46.666666666666671" height="19.333333333333329"/>
<rect key="frame" x="125.00000000000001" y="3.3333333333333339" width="46.666666666666671" height="19.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="highlightedColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@ -207,17 +210,17 @@
</label>
</subviews>
<constraints>
<constraint firstItem="JuE-mg-S0t" firstAttribute="centerY" secondItem="DBW-d4-0o5" secondAttribute="centerY" id="84R-oM-joS"/>
<constraint firstItem="v0f-JH-O27" firstAttribute="leading" secondItem="DBW-d4-0o5" secondAttribute="leading" constant="80" id="FgJ-Op-Uvh"/>
<constraint firstItem="v0f-JH-O27" firstAttribute="leading" secondItem="JuE-mg-S0t" secondAttribute="trailing" constant="16" id="6X4-GL-hxL"/>
<constraint firstItem="JuE-mg-S0t" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="DBW-d4-0o5" secondAttribute="leading" constant="4" id="6bY-sY-17o"/>
<constraint firstItem="v0f-JH-O27" firstAttribute="leading" secondItem="DBW-d4-0o5" secondAttribute="leading" constant="95" id="FgJ-Op-Uvh"/>
<constraint firstItem="JuE-mg-S0t" firstAttribute="baseline" secondItem="U6e-jp-cms" secondAttribute="baseline" id="Hwo-Ak-Tsf"/>
<constraint firstAttribute="trailingMargin" secondItem="ACN-iY-zpP" secondAttribute="trailing" id="J1K-Uc-ySX"/>
<constraint firstItem="0na-qZ-kDB" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="U6e-jp-cms" secondAttribute="trailing" constant="8" symbolic="YES" id="M2z-50-BVR"/>
<constraint firstItem="v0f-JH-O27" firstAttribute="centerY" secondItem="DBW-d4-0o5" secondAttribute="centerY" id="X4l-JC-GWN"/>
<constraint firstItem="ACN-iY-zpP" firstAttribute="centerY" secondItem="DBW-d4-0o5" secondAttribute="centerY" id="ctq-Os-Q9z"/>
<constraint firstItem="v0f-JH-O27" firstAttribute="leading" secondItem="JuE-mg-S0t" secondAttribute="trailing" constant="16.333333333333343" id="eZh-Jc-02b"/>
<constraint firstItem="JuE-mg-S0t" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="DBW-d4-0o5" secondAttribute="leadingMargin" id="je8-Tg-HZP"/>
<constraint firstItem="U6e-jp-cms" firstAttribute="leading" secondItem="DBW-d4-0o5" secondAttribute="leadingMargin" constant="90" id="jun-UY-ULN"/>
<constraint firstItem="JuE-mg-S0t" firstAttribute="baseline" secondItem="ACN-iY-zpP" secondAttribute="baseline" id="jMs-h7-cjn"/>
<constraint firstItem="U6e-jp-cms" firstAttribute="leading" secondItem="DBW-d4-0o5" secondAttribute="leadingMargin" constant="105" id="jun-UY-ULN"/>
<constraint firstItem="0na-qZ-kDB" firstAttribute="baseline" secondItem="U6e-jp-cms" secondAttribute="baseline" id="msL-Ny-DNX"/>
<constraint firstItem="0na-qZ-kDB" firstAttribute="baseline" secondItem="JuE-mg-S0t" secondAttribute="baseline" id="oc4-P3-VQ8"/>
<constraint firstItem="ACN-iY-zpP" firstAttribute="leading" secondItem="0na-qZ-kDB" secondAttribute="trailing" constant="3" id="ysQ-u2-2AH"/>
</constraints>
</tableViewCellContentView>
@ -800,7 +803,7 @@
<rect key="frame" x="0.0" y="0.0" width="390" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="tIz-Hj-yxH">
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" adjustsFontForContentSizeCategory="YES" id="tIz-Hj-yxH">
<rect key="frame" x="20" y="11.999999999999998" width="33" height="20.333333333333332"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
@ -1198,7 +1201,7 @@
<rect key="frame" x="0.0" y="0.0" width="390" height="43.666667938232422"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="c4h-5z-5mU">
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" adjustsFontForContentSizeCategory="YES" id="c4h-5z-5mU">
<rect key="frame" x="20" y="11.999999999999998" width="33" height="20.333333333333332"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>

View File

@ -1007,7 +1007,7 @@ extension BluetoothPeripheralViewController: UITableViewDataSource, UITableViewD
}
cell.detailTextLabel?.text = lastConnectionStatusChangeTimeStamp.toString(timeStyle: .short, dateStyle: .short)
cell.detailTextLabel?.text = lastConnectionStatusChangeTimeStamp.toStringInUserLocale(timeStyle: .short, dateStyle: .short)
} else {
cell.textLabel?.text = Texts_BluetoothPeripheralView.connectedAt

View File

@ -226,13 +226,13 @@ extension DexcomG5BluetoothPeripheralViewModel: BluetoothPeripheralViewModel {
case .sensorStartDate:
cell.textLabel?.text = Texts_BluetoothPeripheralView.sensorStartDate
cell.detailTextLabel?.text = dexcomG5.sensorStartDate?.toString(timeStyle: .short, dateStyle: .short)
cell.detailTextLabel?.text = dexcomG5.sensorStartDate?.toStringInUserLocale(timeStyle: .short, dateStyle: .short)
cell.accessoryType = .none
case .transmitterStartDate:
cell.textLabel?.text = Texts_BluetoothPeripheralView.transmittterStartDate
cell.detailTextLabel?.text = dexcomG5.transmitterStartDate?.toString(timeStyle: .short, dateStyle: .short)
cell.detailTextLabel?.text = dexcomG5.transmitterStartDate?.toStringInUserLocale(timeStyle: .short, dateStyle: .short)
cell.accessoryType = .none
case .firmWareVersion:
@ -296,7 +296,7 @@ extension DexcomG5BluetoothPeripheralViewModel: BluetoothPeripheralViewModel {
if let lastResetTimeStamp = dexcomG5.lastResetTimeStamp {
cell.textLabel?.text = Texts_BluetoothPeripheralView.lastResetTimeStamp
cell.detailTextLabel?.text = lastResetTimeStamp.toString(timeStyle: .short, dateStyle: .short)
cell.detailTextLabel?.text = lastResetTimeStamp.toStringInUserLocale(timeStyle: .short, dateStyle: .short)
cell.accessoryType = .none
} else {

View File

@ -130,7 +130,7 @@ extension Libre2BluetoothPeripheralViewModel: BluetoothPeripheralViewModel {
cell.textLabel?.text = Texts_HomeView.sensorStart
if let sensorTimeInMinutes = libre2.sensorTimeInMinutes {
cell.detailTextLabel?.text = Date(timeIntervalSinceNow: -Double(sensorTimeInMinutes*60)).toString(timeStyle: .short, dateStyle: .short)
cell.detailTextLabel?.text = Date(timeIntervalSinceNow: -Double(sensorTimeInMinutes*60)).toStringInUserLocale(timeStyle: .short, dateStyle: .short)
}
cell.accessoryType = .none

View File

@ -72,7 +72,7 @@ class LandscapeChartViewController: UIViewController {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = ConstantsGlucoseChart.dateFormatLandscapeChart
dateFormatter.setLocalizedDateFormatFromTemplate(ConstantsGlucoseChart.dateFormatLandscapeChart)
return dateFormatter

View File

@ -360,8 +360,14 @@ final class RootViewController: UIViewController {
/// dateformatter for minutesLabelOutlet, when user is panning the chart
private let dateTimeFormatterForMinutesLabelWhenPanning: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = ConstantsGlucoseChart.dateFormatLatestChartPointWhenPanning
dateFormatter.amSymbol = ConstantsUI.timeFormatAM
dateFormatter.pmSymbol = ConstantsUI.timeFormatPM
dateFormatter.setLocalizedDateFormatFromTemplate(ConstantsGlucoseChart.dateFormatLatestChartPointWhenPanning)
return dateFormatter
}()
@ -1188,7 +1194,7 @@ final class RootViewController: UIViewController {
// first check keyValueObserverTimeKeeper
switch keyPathEnumCharts {
case UserDefaults.KeysCharts.chartWidthInHours, UserDefaults.KeysCharts.chartTimeAxisLabelFormat :
case UserDefaults.KeysCharts.chartWidthInHours :
if !keyValueObserverTimeKeeper.verifyKey(forKey: keyPathEnumCharts.rawValue, withMinimumDelayMilliSeconds: 200) {
return

View File

@ -114,7 +114,7 @@ extension SnoozeViewController: UITableViewDataSource {
// if snoozed till after 00:00 then show date and time when it ends, else only show time
let showDate = snoozedTillDate.toMidnight() > Date()
cell.textLabel?.text = TextsSnooze.snoozed_until + " " + snoozedTillDate.toString(timeStyle: .short, dateStyle: showDate ? .short : .none)
cell.textLabel?.text = TextsSnooze.snoozed_until + " " + snoozedTillDate.toStringInUserLocale(timeStyle: .short, dateStyle: showDate ? .short : .none)
} else {

View File

@ -162,11 +162,11 @@ extension AlertsSettingsViewController:UITableViewDataSource, UITableViewDelegat
// get alertValue as Double
let alertValue = alertEntry.value
// start creating the textLabel, star with start time in presentation hh:mm
// start creating the textLabel, start with start time in user's locale and region format
var textLabelToUse = (Int(alertEntry.start)).convertMinutesToTimeAsString()
// add a space
textLabelToUse = textLabelToUse + " "
textLabelToUse = textLabelToUse + " "
// do we add the alert value or not ?
// - is the alerttype enabled ? If it's not no need to show the value (it was like that in iosxdrip, seems a good approach)
@ -178,7 +178,7 @@ extension AlertsSettingsViewController:UITableViewDataSource, UITableViewDelegat
} else {
textLabelToUse = textLabelToUse + alertValue.description
}
textLabelToUse = textLabelToUse + " "
textLabelToUse = textLabelToUse + " "
}
// and now the name of the alerttype

View File

@ -17,10 +17,11 @@ class TreatmentTableViewCell: UITableViewCell {
public func setupWithTreatment(_ treatment: TreatmentEntry) {
// date label
// date label - formatted as per the user's locale and region settings
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm"
formatter.amSymbol = ConstantsUI.timeFormatAM
formatter.pmSymbol = ConstantsUI.timeFormatPM
formatter.setLocalizedDateFormatFromTemplate(ConstantsUI.timeFormatHoursMins)
self.dateLabel.text = formatter.string(from: treatment.date)

View File

@ -219,7 +219,7 @@ extension TreatmentsViewController: UITableViewDelegate, UITableViewDataSource {
let date = treatmentCollection.dateOnlyAt(section).date
let formatter = DateFormatter()
formatter.dateFormat = "dd/MM/yyyy"
formatter.setLocalizedDateFormatFromTemplate(ConstantsUI.dateFormatDayMonthYear)
return formatter.string(from: date)
}