@@ -30,23 +30,6 @@ |
||
| 30 | 30 |
4383B465240EB6B200DAAEBF /* UserDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4383B464240EB6B200DAAEBF /* UserDefault.swift */; };
|
| 31 | 31 |
4383B468240F845500DAAEBF /* MacAdress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4383B467240F845500DAAEBF /* MacAdress.swift */; };
|
| 32 | 32 |
4383B46A240FE4A600DAAEBF /* MeterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4383B469240FE4A600DAAEBF /* MeterView.swift */; };
|
| 33 |
- D28F11013C8E4A7A00A10011 /* MeterHomeTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11023C8E4A7A00A10012 /* MeterHomeTabView.swift */; };
|
|
| 34 |
- D28F11033C8E4A7A00A10013 /* MeterLiveTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11043C8E4A7A00A10014 /* MeterLiveTabView.swift */; };
|
|
| 35 |
- D28F11053C8E4A7A00A10015 /* MeterChartTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11063C8E4A7A00A10016 /* MeterChartTabView.swift */; };
|
|
| 36 |
- D28F11073C8E4A7A00A10017 /* MeterSettingsTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11083C8E4A7A00A10018 /* MeterSettingsTabView.swift */; };
|
|
| 37 |
- D28F11113C8E4A7A00A10021 /* MeterInfoCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11123C8E4A7A00A10022 /* MeterInfoCardView.swift */; };
|
|
| 38 |
- D28F11133C8E4A7A00A10023 /* MeterInfoRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11143C8E4A7A00A10024 /* MeterInfoRowView.swift */; };
|
|
| 39 |
- D28F11153C8E4A7A00A10025 /* MeterNameEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11163C8E4A7A00A10026 /* MeterNameEditorView.swift */; };
|
|
| 40 |
- D28F11173C8E4A7A00A10027 /* ScreenTimeoutEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11183C8E4A7A00A10028 /* ScreenTimeoutEditorView.swift */; };
|
|
| 41 |
- D28F11193C8E4A7A00A10029 /* ScreenBrightnessEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F111A3C8E4A7A00A1002A /* ScreenBrightnessEditorView.swift */; };
|
|
| 42 |
- D28F11213C8E4A7A00A10031 /* MeterLiveMetricRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11223C8E4A7A00A10032 /* MeterLiveMetricRange.swift */; };
|
|
| 43 |
- D28F11233C8E4A7A00A10033 /* LoadResistanceIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11243C8E4A7A00A10034 /* LoadResistanceIconView.swift */; };
|
|
| 44 |
- D28F11313C8E4A7A00A10041 /* MeterScreenControlButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11323C8E4A7A00A10042 /* MeterScreenControlButtonView.swift */; };
|
|
| 45 |
- D28F11333C8E4A7A00A10043 /* MeterCurrentScreenSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11343C8E4A7A00A10044 /* MeterCurrentScreenSummaryView.swift */; };
|
|
| 46 |
- D28F11353C8E4A7A00A10045 /* ChargeRecordMetricsTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11363C8E4A7A00A10046 /* ChargeRecordMetricsTableView.swift */; };
|
|
| 47 |
- D28F11393C8E4A7A00A10049 /* MeterConnectionStatusBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F113A3C8E4A7A00A1004A /* MeterConnectionStatusBadgeView.swift */; };
|
|
| 48 |
- D28F113B3C8E4A7A00A1004B /* MeterConnectionActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F113C3C8E4A7A00A1004C /* MeterConnectionActionView.swift */; };
|
|
| 49 |
- D28F113D3C8E4A7A00A1004D /* MeterOverviewSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F113E3C8E4A7A00A1004E /* MeterOverviewSectionView.swift */; };
|
|
| 50 | 33 |
438695892463F062008855A9 /* Measurements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438695882463F062008855A9 /* Measurements.swift */; };
|
| 51 | 34 |
4386958B2F6A1001008855A9 /* UMProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4386958A2F6A1001008855A9 /* UMProtocol.swift */; };
|
| 52 | 35 |
4386958D2F6A1002008855A9 /* TC66Protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4386958C2F6A1002008855A9 /* TC66Protocol.swift */; };
|
@@ -68,6 +51,23 @@ |
||
| 68 | 51 |
43F7792B2465AE1600745DF4 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F7792A2465AE1600745DF4 /* UIView.swift */; };
|
| 69 | 52 |
AAD5F9A72B1CAC0700F8E4F9 /* MeterDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD5F9A32B1CAC0700F8E4F9 /* MeterDetailView.swift */; };
|
| 70 | 53 |
AAD5F9B12B1CAC7A00F8E4F9 /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD5F9B22B1CAC7A00F8E4F9 /* SidebarView.swift */; };
|
| 54 |
+ D28F11013C8E4A7A00A10011 /* MeterHomeTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11023C8E4A7A00A10012 /* MeterHomeTabView.swift */; };
|
|
| 55 |
+ D28F11033C8E4A7A00A10013 /* MeterLiveTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11043C8E4A7A00A10014 /* MeterLiveTabView.swift */; };
|
|
| 56 |
+ D28F11053C8E4A7A00A10015 /* MeterChartTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11063C8E4A7A00A10016 /* MeterChartTabView.swift */; };
|
|
| 57 |
+ D28F11073C8E4A7A00A10017 /* MeterSettingsTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11083C8E4A7A00A10018 /* MeterSettingsTabView.swift */; };
|
|
| 58 |
+ D28F11113C8E4A7A00A10021 /* MeterInfoCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11123C8E4A7A00A10022 /* MeterInfoCardView.swift */; };
|
|
| 59 |
+ D28F11133C8E4A7A00A10023 /* MeterInfoRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11143C8E4A7A00A10024 /* MeterInfoRowView.swift */; };
|
|
| 60 |
+ D28F11153C8E4A7A00A10025 /* MeterNameEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11163C8E4A7A00A10026 /* MeterNameEditorView.swift */; };
|
|
| 61 |
+ D28F11173C8E4A7A00A10027 /* ScreenTimeoutEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11183C8E4A7A00A10028 /* ScreenTimeoutEditorView.swift */; };
|
|
| 62 |
+ D28F11193C8E4A7A00A10029 /* ScreenBrightnessEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F111A3C8E4A7A00A1002A /* ScreenBrightnessEditorView.swift */; };
|
|
| 63 |
+ D28F11213C8E4A7A00A10031 /* MeterLiveMetricRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11223C8E4A7A00A10032 /* MeterLiveMetricRange.swift */; };
|
|
| 64 |
+ D28F11233C8E4A7A00A10033 /* LoadResistanceIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11243C8E4A7A00A10034 /* LoadResistanceIconView.swift */; };
|
|
| 65 |
+ D28F11313C8E4A7A00A10041 /* MeterScreenControlButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11323C8E4A7A00A10042 /* MeterScreenControlButtonView.swift */; };
|
|
| 66 |
+ D28F11333C8E4A7A00A10043 /* MeterCurrentScreenSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11343C8E4A7A00A10044 /* MeterCurrentScreenSummaryView.swift */; };
|
|
| 67 |
+ D28F11353C8E4A7A00A10045 /* ChargeRecordMetricsTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F11363C8E4A7A00A10046 /* ChargeRecordMetricsTableView.swift */; };
|
|
| 68 |
+ D28F11393C8E4A7A00A10049 /* MeterConnectionStatusBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F113A3C8E4A7A00A1004A /* MeterConnectionStatusBadgeView.swift */; };
|
|
| 69 |
+ D28F113B3C8E4A7A00A1004B /* MeterConnectionActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F113C3C8E4A7A00A1004C /* MeterConnectionActionView.swift */; };
|
|
| 70 |
+ D28F113D3C8E4A7A00A1004D /* MeterOverviewSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F113E3C8E4A7A00A1004E /* MeterOverviewSectionView.swift */; };
|
|
| 71 | 71 |
E430FB6B7CB3E0D4189F6D7D /* MeterMappingDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA4CE53B6B2C4EBA42C81A /* MeterMappingDebugView.swift */; };
|
| 72 | 72 |
/* End PBXBuildFile section */ |
| 73 | 73 |
|
@@ -124,23 +124,6 @@ |
||
| 124 | 124 |
4383B464240EB6B200DAAEBF /* UserDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefault.swift; sourceTree = "<group>"; };
|
| 125 | 125 |
4383B467240F845500DAAEBF /* MacAdress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacAdress.swift; sourceTree = "<group>"; };
|
| 126 | 126 |
4383B469240FE4A600DAAEBF /* MeterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterView.swift; sourceTree = "<group>"; };
|
| 127 |
- D28F11023C8E4A7A00A10012 /* MeterHomeTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterHomeTabView.swift; sourceTree = "<group>"; };
|
|
| 128 |
- D28F11043C8E4A7A00A10014 /* MeterLiveTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterLiveTabView.swift; sourceTree = "<group>"; };
|
|
| 129 |
- D28F11063C8E4A7A00A10016 /* MeterChartTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterChartTabView.swift; sourceTree = "<group>"; };
|
|
| 130 |
- D28F11083C8E4A7A00A10018 /* MeterSettingsTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterSettingsTabView.swift; sourceTree = "<group>"; };
|
|
| 131 |
- D28F11123C8E4A7A00A10022 /* MeterInfoCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterInfoCardView.swift; sourceTree = "<group>"; };
|
|
| 132 |
- D28F11143C8E4A7A00A10024 /* MeterInfoRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterInfoRowView.swift; sourceTree = "<group>"; };
|
|
| 133 |
- D28F11163C8E4A7A00A10026 /* MeterNameEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterNameEditorView.swift; sourceTree = "<group>"; };
|
|
| 134 |
- D28F11183C8E4A7A00A10028 /* ScreenTimeoutEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenTimeoutEditorView.swift; sourceTree = "<group>"; };
|
|
| 135 |
- D28F111A3C8E4A7A00A1002A /* ScreenBrightnessEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenBrightnessEditorView.swift; sourceTree = "<group>"; };
|
|
| 136 |
- D28F11223C8E4A7A00A10032 /* MeterLiveMetricRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterLiveMetricRange.swift; sourceTree = "<group>"; };
|
|
| 137 |
- D28F11243C8E4A7A00A10034 /* LoadResistanceIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadResistanceIconView.swift; sourceTree = "<group>"; };
|
|
| 138 |
- D28F11323C8E4A7A00A10042 /* MeterScreenControlButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterScreenControlButtonView.swift; sourceTree = "<group>"; };
|
|
| 139 |
- D28F11343C8E4A7A00A10044 /* MeterCurrentScreenSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterCurrentScreenSummaryView.swift; sourceTree = "<group>"; };
|
|
| 140 |
- D28F11363C8E4A7A00A10046 /* ChargeRecordMetricsTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChargeRecordMetricsTableView.swift; sourceTree = "<group>"; };
|
|
| 141 |
- D28F113A3C8E4A7A00A1004A /* MeterConnectionStatusBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterConnectionStatusBadgeView.swift; sourceTree = "<group>"; };
|
|
| 142 |
- D28F113C3C8E4A7A00A1004C /* MeterConnectionActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterConnectionActionView.swift; sourceTree = "<group>"; };
|
|
| 143 |
- D28F113E3C8E4A7A00A1004E /* MeterOverviewSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterOverviewSectionView.swift; sourceTree = "<group>"; };
|
|
| 144 | 127 |
438695882463F062008855A9 /* Measurements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Measurements.swift; sourceTree = "<group>"; };
|
| 145 | 128 |
4386958A2F6A1001008855A9 /* UMProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UMProtocol.swift; sourceTree = "<group>"; };
|
| 146 | 129 |
4386958C2F6A1002008855A9 /* TC66Protocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TC66Protocol.swift; sourceTree = "<group>"; };
|
@@ -167,6 +150,23 @@ |
||
| 167 | 150 |
7396C8BB36F4E7F8E0CD8FF8 /* MeterNameStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterNameStore.swift; sourceTree = "<group>"; };
|
| 168 | 151 |
AAD5F9A32B1CAC0700F8E4F9 /* MeterDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterDetailView.swift; sourceTree = "<group>"; };
|
| 169 | 152 |
AAD5F9B22B1CAC7A00F8E4F9 /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = "<group>"; };
|
| 153 |
+ D28F11023C8E4A7A00A10012 /* MeterHomeTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterHomeTabView.swift; sourceTree = "<group>"; };
|
|
| 154 |
+ D28F11043C8E4A7A00A10014 /* MeterLiveTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterLiveTabView.swift; sourceTree = "<group>"; };
|
|
| 155 |
+ D28F11063C8E4A7A00A10016 /* MeterChartTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterChartTabView.swift; sourceTree = "<group>"; };
|
|
| 156 |
+ D28F11083C8E4A7A00A10018 /* MeterSettingsTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterSettingsTabView.swift; sourceTree = "<group>"; };
|
|
| 157 |
+ D28F11123C8E4A7A00A10022 /* MeterInfoCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterInfoCardView.swift; sourceTree = "<group>"; };
|
|
| 158 |
+ D28F11143C8E4A7A00A10024 /* MeterInfoRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterInfoRowView.swift; sourceTree = "<group>"; };
|
|
| 159 |
+ D28F11163C8E4A7A00A10026 /* MeterNameEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterNameEditorView.swift; sourceTree = "<group>"; };
|
|
| 160 |
+ D28F11183C8E4A7A00A10028 /* ScreenTimeoutEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenTimeoutEditorView.swift; sourceTree = "<group>"; };
|
|
| 161 |
+ D28F111A3C8E4A7A00A1002A /* ScreenBrightnessEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenBrightnessEditorView.swift; sourceTree = "<group>"; };
|
|
| 162 |
+ D28F11223C8E4A7A00A10032 /* MeterLiveMetricRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterLiveMetricRange.swift; sourceTree = "<group>"; };
|
|
| 163 |
+ D28F11243C8E4A7A00A10034 /* LoadResistanceIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadResistanceIconView.swift; sourceTree = "<group>"; };
|
|
| 164 |
+ D28F11323C8E4A7A00A10042 /* MeterScreenControlButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterScreenControlButtonView.swift; sourceTree = "<group>"; };
|
|
| 165 |
+ D28F11343C8E4A7A00A10044 /* MeterCurrentScreenSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterCurrentScreenSummaryView.swift; sourceTree = "<group>"; };
|
|
| 166 |
+ D28F11363C8E4A7A00A10046 /* ChargeRecordMetricsTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChargeRecordMetricsTableView.swift; sourceTree = "<group>"; };
|
|
| 167 |
+ D28F113A3C8E4A7A00A1004A /* MeterConnectionStatusBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterConnectionStatusBadgeView.swift; sourceTree = "<group>"; };
|
|
| 168 |
+ D28F113C3C8E4A7A00A1004C /* MeterConnectionActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterConnectionActionView.swift; sourceTree = "<group>"; };
|
|
| 169 |
+ D28F113E3C8E4A7A00A1004E /* MeterOverviewSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterOverviewSectionView.swift; sourceTree = "<group>"; };
|
|
| 170 | 170 |
/* End PBXFileReference section */ |
| 171 | 171 |
|
| 172 | 172 |
/* Begin PBXFileSystemSynchronizedRootGroup section */ |
@@ -312,136 +312,152 @@ |
||
| 312 | 312 |
path = MeasurementSeries; |
| 313 | 313 |
sourceTree = "<group>"; |
| 314 | 314 |
}; |
| 315 |
- D28F11253C8E4A7A00A10035 /* Subviews */ = {
|
|
| 316 |
- isa = PBXGroup; |
|
| 317 |
- children = ( |
|
| 318 |
- 437D47D02415F91B00B7768E /* MeterLiveContentView.swift */, |
|
| 319 |
- D28F11223C8E4A7A00A10032 /* MeterLiveMetricRange.swift */, |
|
| 320 |
- D28F11243C8E4A7A00A10034 /* LoadResistanceIconView.swift */, |
|
| 321 |
- ); |
|
| 322 |
- path = Subviews; |
|
| 323 |
- sourceTree = "<group>"; |
|
| 324 |
- }; |
|
| 325 |
- D28F11263C8E4A7A00A10036 /* Subviews */ = {
|
|
| 315 |
+ 437D47CF2415F8CF00B7768E /* Meter */ = {
|
|
| 326 | 316 |
isa = PBXGroup; |
| 327 | 317 |
children = ( |
| 328 |
- 4308CF8524176CAB0002E80B /* DataGroupRowView.swift */, |
|
| 318 |
+ 4383B469240FE4A600DAAEBF /* MeterView.swift */, |
|
| 319 |
+ D28F113F3C8E4A7A00A1004F /* Components */, |
|
| 320 |
+ D28F11093C8E4A7A00A10019 /* Tabs */, |
|
| 321 |
+ D28F10013C8E4A7A00A10001 /* Sheets */, |
|
| 329 | 322 |
); |
| 330 |
- path = Subviews; |
|
| 323 |
+ path = Meter; |
|
| 331 | 324 |
sourceTree = "<group>"; |
| 332 | 325 |
}; |
| 333 |
- D28F11373C8E4A7A00A10047 /* Components */ = {
|
|
| 326 |
+ 4383B463240EB66400DAAEBF /* Templates */ = {
|
|
| 334 | 327 |
isa = PBXGroup; |
| 335 | 328 |
children = ( |
| 336 |
- D28F11323C8E4A7A00A10042 /* MeterScreenControlButtonView.swift */, |
|
| 337 |
- D28F11343C8E4A7A00A10044 /* MeterCurrentScreenSummaryView.swift */, |
|
| 329 |
+ 4383B464240EB6B200DAAEBF /* UserDefault.swift */, |
|
| 338 | 330 |
); |
| 339 |
- path = Components; |
|
| 331 |
+ path = Templates; |
|
| 340 | 332 |
sourceTree = "<group>"; |
| 341 | 333 |
}; |
| 342 |
- D28F11383C8E4A7A00A10048 /* Subviews */ = {
|
|
| 334 |
+ 4383B466240F842700DAAEBF /* DataTypes */ = {
|
|
| 343 | 335 |
isa = PBXGroup; |
| 344 | 336 |
children = ( |
| 345 |
- D28F11363C8E4A7A00A10046 /* ChargeRecordMetricsTableView.swift */, |
|
| 337 |
+ 4383B467240F845500DAAEBF /* MacAdress.swift */, |
|
| 346 | 338 |
); |
| 347 |
- path = Subviews; |
|
| 339 |
+ path = DataTypes; |
|
| 348 | 340 |
sourceTree = "<group>"; |
| 349 | 341 |
}; |
| 350 |
- D28F11273C8E4A7A00A10037 /* ChargeRecord */ = {
|
|
| 342 |
+ 43CBF653240BF3EB00255B8B = {
|
|
| 351 | 343 |
isa = PBXGroup; |
| 352 | 344 |
children = ( |
| 353 |
- 437D47D42415FD8C00B7768E /* ChargeRecordSheetView.swift */, |
|
| 354 |
- D28F11383C8E4A7A00A10048 /* Subviews */, |
|
| 345 |
+ 1C6B6B952A2D4F5100A0B001 /* Documentation */, |
|
| 346 |
+ 43CBF65E240BF3EB00255B8B /* USB Meter */, |
|
| 347 |
+ 43CBF65D240BF3EB00255B8B /* Products */, |
|
| 348 |
+ 4347F01B28D717C1007EE7B1 /* Frameworks */, |
|
| 355 | 349 |
); |
| 356 |
- path = ChargeRecord; |
|
| 357 | 350 |
sourceTree = "<group>"; |
| 358 | 351 |
}; |
| 359 |
- D28F11283C8E4A7A00A10038 /* Control */ = {
|
|
| 352 |
+ 43CBF65D240BF3EB00255B8B /* Products */ = {
|
|
| 360 | 353 |
isa = PBXGroup; |
| 361 | 354 |
children = ( |
| 362 |
- 437D47D62415FDF300B7768E /* MeterScreenControlsView.swift */, |
|
| 363 |
- D28F11373C8E4A7A00A10047 /* Components */, |
|
| 355 |
+ 43CBF65C240BF3EB00255B8B /* USB Meter.app */, |
|
| 364 | 356 |
); |
| 365 |
- path = Control; |
|
| 357 |
+ name = Products; |
|
| 366 | 358 |
sourceTree = "<group>"; |
| 367 | 359 |
}; |
| 368 |
- D28F10013C8E4A7A00A10001 /* Sheets */ = {
|
|
| 360 |
+ 43CBF65E240BF3EB00255B8B /* USB Meter */ = {
|
|
| 369 | 361 |
isa = PBXGroup; |
| 370 | 362 |
children = ( |
| 371 |
- 4308CF89241777130002E80B /* DataGroups */, |
|
| 372 |
- 43554B3024444983004E66F5 /* MeasurementSeries */, |
|
| 373 |
- D28F11273C8E4A7A00A10037 /* ChargeRecord */, |
|
| 363 |
+ 43CBF67A240C0D8A00255B8B /* USB Meter.entitlements */, |
|
| 364 |
+ 43CBF65F240BF3EB00255B8B /* AppDelegate.swift */, |
|
| 365 |
+ 43CBF661240BF3EB00255B8B /* SceneDelegate.swift */, |
|
| 366 |
+ 43CBF678240C047D00255B8B /* Model */, |
|
| 367 |
+ 43CBF679240C08C600255B8B /* Views */, |
|
| 368 |
+ 43CBF67F240D14AC00255B8B /* Extensions */, |
|
| 369 |
+ 4383B463240EB66400DAAEBF /* Templates */, |
|
| 370 |
+ 4383B466240F842700DAAEBF /* DataTypes */, |
|
| 371 |
+ 43CBF668240BF3ED00255B8B /* Assets.xcassets */, |
|
| 372 |
+ 43CBF66D240BF3ED00255B8B /* LaunchScreen.storyboard */, |
|
| 373 |
+ 43CBF670240BF3ED00255B8B /* Info.plist */, |
|
| 374 |
+ 43CBF66A240BF3ED00255B8B /* Preview Content */, |
|
| 374 | 375 |
); |
| 375 |
- path = Sheets; |
|
| 376 |
+ path = "USB Meter"; |
|
| 376 | 377 |
sourceTree = "<group>"; |
| 377 | 378 |
}; |
| 378 |
- D28F111B3C8E4A7A00A1002B /* Subviews */ = {
|
|
| 379 |
+ 43CBF66A240BF3ED00255B8B /* Preview Content */ = {
|
|
| 379 | 380 |
isa = PBXGroup; |
| 380 | 381 |
children = ( |
| 381 |
- D28F113A3C8E4A7A00A1004A /* MeterConnectionStatusBadgeView.swift */, |
|
| 382 |
- D28F113C3C8E4A7A00A1004C /* MeterConnectionActionView.swift */, |
|
| 383 |
- D28F113E3C8E4A7A00A1004E /* MeterOverviewSectionView.swift */, |
|
| 382 |
+ 43CBF66B240BF3ED00255B8B /* Preview Assets.xcassets */, |
|
| 384 | 383 |
); |
| 385 |
- path = Subviews; |
|
| 384 |
+ path = "Preview Content"; |
|
| 386 | 385 |
sourceTree = "<group>"; |
| 387 | 386 |
}; |
| 388 |
- D28F111C3C8E4A7A00A1002C /* Subviews */ = {
|
|
| 387 |
+ 43CBF678240C047D00255B8B /* Model */ = {
|
|
| 389 | 388 |
isa = PBXGroup; |
| 390 | 389 |
children = ( |
| 391 |
- D28F11163C8E4A7A00A10026 /* MeterNameEditorView.swift */, |
|
| 392 |
- D28F11183C8E4A7A00A10028 /* ScreenTimeoutEditorView.swift */, |
|
| 393 |
- D28F111A3C8E4A7A00A1002A /* ScreenBrightnessEditorView.swift */, |
|
| 394 |
- 437D47D62415FDF300B7768E /* MeterScreenControlsView.swift */, |
|
| 395 |
- D28F11323C8E4A7A00A10042 /* MeterScreenControlButtonView.swift */, |
|
| 396 |
- D28F11343C8E4A7A00A10044 /* MeterCurrentScreenSummaryView.swift */, |
|
| 390 |
+ 4383B461240EB5E400DAAEBF /* AppData.swift */, |
|
| 391 |
+ 7396C8BB36F4E7F8E0CD8FF8 /* MeterNameStore.swift */, |
|
| 392 |
+ 43CBF676240C043E00255B8B /* BluetoothManager.swift */, |
|
| 393 |
+ 4383B45F240EB2D000DAAEBF /* Meter.swift */, |
|
| 394 |
+ 43ED78AD2420A0BE00974487 /* BluetoothSerial.swift */, |
|
| 395 |
+ 439D996424234B98008DE3AA /* BluetoothRadio.swift */, |
|
| 396 |
+ 4386958E2F6A4E3E008855A9 /* MeterCapabilities.swift */, |
|
| 397 |
+ 4386958A2F6A1001008855A9 /* UMProtocol.swift */, |
|
| 398 |
+ 4386958C2F6A1002008855A9 /* TC66Protocol.swift */, |
|
| 399 |
+ 438695882463F062008855A9 /* Measurements.swift */, |
|
| 400 |
+ 432EA6432445A559006FC905 /* ChartContext.swift */, |
|
| 397 | 401 |
); |
| 398 |
- path = Subviews; |
|
| 402 |
+ path = Model; |
|
| 399 | 403 |
sourceTree = "<group>"; |
| 400 | 404 |
}; |
| 401 |
- D28F110A3C8E4A7A00A1001A /* Home */ = {
|
|
| 405 |
+ 43CBF679240C08C600255B8B /* Views */ = {
|
|
| 402 | 406 |
isa = PBXGroup; |
| 403 | 407 |
children = ( |
| 404 |
- D28F11023C8E4A7A00A10012 /* MeterHomeTabView.swift */, |
|
| 405 |
- D28F111B3C8E4A7A00A1002B /* Subviews */, |
|
| 408 |
+ 43CBF666240BF3EB00255B8B /* ContentView.swift */, |
|
| 409 |
+ AAD5F9BB2B1CC10000F8E4F9 /* Sidebar */, |
|
| 410 |
+ 56BA4CE53B6B2C4EBA42C81A /* MeterMappingDebugView.swift */, |
|
| 411 |
+ 4327461A24619CED0009BE4B /* MeterRowView.swift */, |
|
| 412 |
+ 437D47CF2415F8CF00B7768E /* Meter */, |
|
| 413 |
+ D28F10023C8E4A7A00A10002 /* Components */, |
|
| 414 |
+ 4311E639241384960080EA59 /* DeviceHelpView.swift */, |
|
| 415 |
+ AAD5F9A32B1CAC0700F8E4F9 /* MeterDetailView.swift */, |
|
| 406 | 416 |
); |
| 407 |
- path = Home; |
|
| 417 |
+ path = Views; |
|
| 408 | 418 |
sourceTree = "<group>"; |
| 409 | 419 |
}; |
| 410 |
- D28F110B3C8E4A7A00A1001B /* Live */ = {
|
|
| 420 |
+ 43CBF67F240D14AC00255B8B /* Extensions */ = {
|
|
| 411 | 421 |
isa = PBXGroup; |
| 412 | 422 |
children = ( |
| 413 |
- D28F11043C8E4A7A00A10014 /* MeterLiveTabView.swift */, |
|
| 414 |
- D28F11253C8E4A7A00A10035 /* Subviews */, |
|
| 423 |
+ 43CBF680240D153000255B8B /* CBManagerState.swift */, |
|
| 424 |
+ 4351E7BA24685ACD00E798A3 /* CGPoint.swift */, |
|
| 425 |
+ 43874C7E2414F3F400525397 /* Float.swift */, |
|
| 426 |
+ 43F7792A2465AE1600745DF4 /* UIView.swift */, |
|
| 427 |
+ 43874C842415611200525397 /* Double.swift */, |
|
| 428 |
+ 43874C82241533AD00525397 /* Data.swift */, |
|
| 429 |
+ 437D47D22415FB7E00B7768E /* Decimal.swift */, |
|
| 430 |
+ 43554B3324444B0E004E66F5 /* Date.swift */, |
|
| 431 |
+ 438B9554246D2D7500E61AE7 /* Path.swift */, |
|
| 415 | 432 |
); |
| 416 |
- path = Live; |
|
| 433 |
+ path = Extensions; |
|
| 417 | 434 |
sourceTree = "<group>"; |
| 418 | 435 |
}; |
| 419 |
- D28F110C3C8E4A7A00A1001C /* Chart */ = {
|
|
| 436 |
+ AAD5F9BB2B1CC10000F8E4F9 /* Sidebar */ = {
|
|
| 420 | 437 |
isa = PBXGroup; |
| 421 | 438 |
children = ( |
| 422 |
- D28F11063C8E4A7A00A10016 /* MeterChartTabView.swift */, |
|
| 439 |
+ AAD5F9B22B1CAC7A00F8E4F9 /* SidebarView.swift */, |
|
| 440 |
+ 43BE08E12F78F49500250EEC /* SidebarList */, |
|
| 423 | 441 |
); |
| 424 |
- path = Chart; |
|
| 442 |
+ path = Sidebar; |
|
| 425 | 443 |
sourceTree = "<group>"; |
| 426 | 444 |
}; |
| 427 |
- D28F110D3C8E4A7A00A1001D /* Settings */ = {
|
|
| 445 |
+ D28F10013C8E4A7A00A10001 /* Sheets */ = {
|
|
| 428 | 446 |
isa = PBXGroup; |
| 429 | 447 |
children = ( |
| 430 |
- D28F11083C8E4A7A00A10018 /* MeterSettingsTabView.swift */, |
|
| 431 |
- D28F111C3C8E4A7A00A1002C /* Subviews */, |
|
| 448 |
+ 4308CF89241777130002E80B /* DataGroups */, |
|
| 449 |
+ 43554B3024444983004E66F5 /* MeasurementSeries */, |
|
| 450 |
+ D28F11273C8E4A7A00A10037 /* ChargeRecord */, |
|
| 432 | 451 |
); |
| 433 |
- path = Settings; |
|
| 452 |
+ path = Sheets; |
|
| 434 | 453 |
sourceTree = "<group>"; |
| 435 | 454 |
}; |
| 436 |
- D28F11093C8E4A7A00A10019 /* Tabs */ = {
|
|
| 455 |
+ D28F10023C8E4A7A00A10002 /* Components */ = {
|
|
| 437 | 456 |
isa = PBXGroup; |
| 438 | 457 |
children = ( |
| 439 |
- D28F110A3C8E4A7A00A1001A /* Home */, |
|
| 440 |
- D28F110B3C8E4A7A00A1001B /* Live */, |
|
| 441 |
- D28F110C3C8E4A7A00A1001C /* Chart */, |
|
| 442 |
- D28F110D3C8E4A7A00A1001D /* Settings */, |
|
| 458 |
+ D28F10033C8E4A7A00A10003 /* Generic */, |
|
| 443 | 459 |
); |
| 444 |
- path = Tabs; |
|
| 460 |
+ path = Components; |
|
| 445 | 461 |
sourceTree = "<group>"; |
| 446 | 462 |
}; |
| 447 | 463 |
D28F10033C8E4A7A00A10003 /* Generic */ = {
|
@@ -453,152 +469,118 @@ |
||
| 453 | 469 |
path = Generic; |
| 454 | 470 |
sourceTree = "<group>"; |
| 455 | 471 |
}; |
| 456 |
- D28F10023C8E4A7A00A10002 /* Components */ = {
|
|
| 457 |
- isa = PBXGroup; |
|
| 458 |
- children = ( |
|
| 459 |
- D28F10033C8E4A7A00A10003 /* Generic */, |
|
| 460 |
- ); |
|
| 461 |
- path = Components; |
|
| 462 |
- sourceTree = "<group>"; |
|
| 463 |
- }; |
|
| 464 |
- D28F113F3C8E4A7A00A1004F /* Components */ = {
|
|
| 472 |
+ D28F11093C8E4A7A00A10019 /* Tabs */ = {
|
|
| 465 | 473 |
isa = PBXGroup; |
| 466 | 474 |
children = ( |
| 467 |
- D28F11123C8E4A7A00A10022 /* MeterInfoCardView.swift */, |
|
| 468 |
- D28F11143C8E4A7A00A10024 /* MeterInfoRowView.swift */, |
|
| 469 |
- 437F0AB62463108F005DEBEC /* MeasurementChartView.swift */, |
|
| 475 |
+ D28F110A3C8E4A7A00A1001A /* Home */, |
|
| 476 |
+ D28F110B3C8E4A7A00A1001B /* Live */, |
|
| 477 |
+ D28F110C3C8E4A7A00A1001C /* Chart */, |
|
| 478 |
+ D28F110D3C8E4A7A00A1001D /* Settings */, |
|
| 470 | 479 |
); |
| 471 |
- path = Components; |
|
| 480 |
+ path = Tabs; |
|
| 472 | 481 |
sourceTree = "<group>"; |
| 473 | 482 |
}; |
| 474 |
- 437D47CF2415F8CF00B7768E /* Meter */ = {
|
|
| 483 |
+ D28F110A3C8E4A7A00A1001A /* Home */ = {
|
|
| 475 | 484 |
isa = PBXGroup; |
| 476 | 485 |
children = ( |
| 477 |
- 4383B469240FE4A600DAAEBF /* MeterView.swift */, |
|
| 478 |
- D28F113F3C8E4A7A00A1004F /* Components */, |
|
| 479 |
- D28F11093C8E4A7A00A10019 /* Tabs */, |
|
| 480 |
- D28F10013C8E4A7A00A10001 /* Sheets */, |
|
| 486 |
+ D28F11023C8E4A7A00A10012 /* MeterHomeTabView.swift */, |
|
| 487 |
+ D28F111B3C8E4A7A00A1002B /* Subviews */, |
|
| 481 | 488 |
); |
| 482 |
- path = Meter; |
|
| 489 |
+ path = Home; |
|
| 483 | 490 |
sourceTree = "<group>"; |
| 484 | 491 |
}; |
| 485 |
- 4383B463240EB66400DAAEBF /* Templates */ = {
|
|
| 492 |
+ D28F110B3C8E4A7A00A1001B /* Live */ = {
|
|
| 486 | 493 |
isa = PBXGroup; |
| 487 | 494 |
children = ( |
| 488 |
- 4383B464240EB6B200DAAEBF /* UserDefault.swift */, |
|
| 495 |
+ D28F11043C8E4A7A00A10014 /* MeterLiveTabView.swift */, |
|
| 496 |
+ D28F11253C8E4A7A00A10035 /* Subviews */, |
|
| 489 | 497 |
); |
| 490 |
- path = Templates; |
|
| 498 |
+ path = Live; |
|
| 491 | 499 |
sourceTree = "<group>"; |
| 492 | 500 |
}; |
| 493 |
- 4383B466240F842700DAAEBF /* DataTypes */ = {
|
|
| 501 |
+ D28F110C3C8E4A7A00A1001C /* Chart */ = {
|
|
| 494 | 502 |
isa = PBXGroup; |
| 495 | 503 |
children = ( |
| 496 |
- 4383B467240F845500DAAEBF /* MacAdress.swift */, |
|
| 504 |
+ D28F11063C8E4A7A00A10016 /* MeterChartTabView.swift */, |
|
| 497 | 505 |
); |
| 498 |
- path = DataTypes; |
|
| 506 |
+ path = Chart; |
|
| 499 | 507 |
sourceTree = "<group>"; |
| 500 | 508 |
}; |
| 501 |
- 43CBF653240BF3EB00255B8B = {
|
|
| 509 |
+ D28F110D3C8E4A7A00A1001D /* Settings */ = {
|
|
| 502 | 510 |
isa = PBXGroup; |
| 503 | 511 |
children = ( |
| 504 |
- 1C6B6B952A2D4F5100A0B001 /* Documentation */, |
|
| 505 |
- 43CBF65E240BF3EB00255B8B /* USB Meter */, |
|
| 506 |
- 43CBF65D240BF3EB00255B8B /* Products */, |
|
| 507 |
- 4347F01B28D717C1007EE7B1 /* Frameworks */, |
|
| 512 |
+ D28F11083C8E4A7A00A10018 /* MeterSettingsTabView.swift */, |
|
| 513 |
+ D28F111C3C8E4A7A00A1002C /* Subviews */, |
|
| 508 | 514 |
); |
| 515 |
+ path = Settings; |
|
| 509 | 516 |
sourceTree = "<group>"; |
| 510 | 517 |
}; |
| 511 |
- 43CBF65D240BF3EB00255B8B /* Products */ = {
|
|
| 518 |
+ D28F111B3C8E4A7A00A1002B /* Subviews */ = {
|
|
| 512 | 519 |
isa = PBXGroup; |
| 513 | 520 |
children = ( |
| 514 |
- 43CBF65C240BF3EB00255B8B /* USB Meter.app */, |
|
| 521 |
+ D28F113A3C8E4A7A00A1004A /* MeterConnectionStatusBadgeView.swift */, |
|
| 522 |
+ D28F113C3C8E4A7A00A1004C /* MeterConnectionActionView.swift */, |
|
| 523 |
+ D28F113E3C8E4A7A00A1004E /* MeterOverviewSectionView.swift */, |
|
| 515 | 524 |
); |
| 516 |
- name = Products; |
|
| 525 |
+ path = Subviews; |
|
| 517 | 526 |
sourceTree = "<group>"; |
| 518 | 527 |
}; |
| 519 |
- 43CBF65E240BF3EB00255B8B /* USB Meter */ = {
|
|
| 528 |
+ D28F111C3C8E4A7A00A1002C /* Subviews */ = {
|
|
| 520 | 529 |
isa = PBXGroup; |
| 521 | 530 |
children = ( |
| 522 |
- 43CBF67A240C0D8A00255B8B /* USB Meter.entitlements */, |
|
| 523 |
- 43CBF65F240BF3EB00255B8B /* AppDelegate.swift */, |
|
| 524 |
- 43CBF661240BF3EB00255B8B /* SceneDelegate.swift */, |
|
| 525 |
- 43CBF678240C047D00255B8B /* Model */, |
|
| 526 |
- 43CBF679240C08C600255B8B /* Views */, |
|
| 527 |
- 43CBF67F240D14AC00255B8B /* Extensions */, |
|
| 528 |
- 4383B463240EB66400DAAEBF /* Templates */, |
|
| 529 |
- 4383B466240F842700DAAEBF /* DataTypes */, |
|
| 530 |
- 43CBF668240BF3ED00255B8B /* Assets.xcassets */, |
|
| 531 |
- 43CBF66D240BF3ED00255B8B /* LaunchScreen.storyboard */, |
|
| 532 |
- 43CBF670240BF3ED00255B8B /* Info.plist */, |
|
| 533 |
- 43CBF66A240BF3ED00255B8B /* Preview Content */, |
|
| 531 |
+ D28F11163C8E4A7A00A10026 /* MeterNameEditorView.swift */, |
|
| 532 |
+ D28F11183C8E4A7A00A10028 /* ScreenTimeoutEditorView.swift */, |
|
| 533 |
+ D28F111A3C8E4A7A00A1002A /* ScreenBrightnessEditorView.swift */, |
|
| 534 |
+ 437D47D62415FDF300B7768E /* MeterScreenControlsView.swift */, |
|
| 535 |
+ D28F11323C8E4A7A00A10042 /* MeterScreenControlButtonView.swift */, |
|
| 536 |
+ D28F11343C8E4A7A00A10044 /* MeterCurrentScreenSummaryView.swift */, |
|
| 534 | 537 |
); |
| 535 |
- path = "USB Meter"; |
|
| 538 |
+ path = Subviews; |
|
| 536 | 539 |
sourceTree = "<group>"; |
| 537 | 540 |
}; |
| 538 |
- 43CBF66A240BF3ED00255B8B /* Preview Content */ = {
|
|
| 541 |
+ D28F11253C8E4A7A00A10035 /* Subviews */ = {
|
|
| 539 | 542 |
isa = PBXGroup; |
| 540 | 543 |
children = ( |
| 541 |
- 43CBF66B240BF3ED00255B8B /* Preview Assets.xcassets */, |
|
| 544 |
+ 437D47D02415F91B00B7768E /* MeterLiveContentView.swift */, |
|
| 545 |
+ D28F11223C8E4A7A00A10032 /* MeterLiveMetricRange.swift */, |
|
| 546 |
+ D28F11243C8E4A7A00A10034 /* LoadResistanceIconView.swift */, |
|
| 542 | 547 |
); |
| 543 |
- path = "Preview Content"; |
|
| 548 |
+ path = Subviews; |
|
| 544 | 549 |
sourceTree = "<group>"; |
| 545 | 550 |
}; |
| 546 |
- 43CBF678240C047D00255B8B /* Model */ = {
|
|
| 551 |
+ D28F11263C8E4A7A00A10036 /* Subviews */ = {
|
|
| 547 | 552 |
isa = PBXGroup; |
| 548 | 553 |
children = ( |
| 549 |
- 4383B461240EB5E400DAAEBF /* AppData.swift */, |
|
| 550 |
- 7396C8BB36F4E7F8E0CD8FF8 /* MeterNameStore.swift */, |
|
| 551 |
- 43CBF676240C043E00255B8B /* BluetoothManager.swift */, |
|
| 552 |
- 4383B45F240EB2D000DAAEBF /* Meter.swift */, |
|
| 553 |
- 43ED78AD2420A0BE00974487 /* BluetoothSerial.swift */, |
|
| 554 |
- 439D996424234B98008DE3AA /* BluetoothRadio.swift */, |
|
| 555 |
- 4386958E2F6A4E3E008855A9 /* MeterCapabilities.swift */, |
|
| 556 |
- 4386958A2F6A1001008855A9 /* UMProtocol.swift */, |
|
| 557 |
- 4386958C2F6A1002008855A9 /* TC66Protocol.swift */, |
|
| 558 |
- 438695882463F062008855A9 /* Measurements.swift */, |
|
| 559 |
- 432EA6432445A559006FC905 /* ChartContext.swift */, |
|
| 554 |
+ 4308CF8524176CAB0002E80B /* DataGroupRowView.swift */, |
|
| 560 | 555 |
); |
| 561 |
- path = Model; |
|
| 556 |
+ path = Subviews; |
|
| 562 | 557 |
sourceTree = "<group>"; |
| 563 | 558 |
}; |
| 564 |
- 43CBF679240C08C600255B8B /* Views */ = {
|
|
| 559 |
+ D28F11273C8E4A7A00A10037 /* ChargeRecord */ = {
|
|
| 565 | 560 |
isa = PBXGroup; |
| 566 | 561 |
children = ( |
| 567 |
- 43CBF666240BF3EB00255B8B /* ContentView.swift */, |
|
| 568 |
- AAD5F9BB2B1CC10000F8E4F9 /* Sidebar */, |
|
| 569 |
- 56BA4CE53B6B2C4EBA42C81A /* MeterMappingDebugView.swift */, |
|
| 570 |
- 4327461A24619CED0009BE4B /* MeterRowView.swift */, |
|
| 571 |
- 437D47CF2415F8CF00B7768E /* Meter */, |
|
| 572 |
- D28F10023C8E4A7A00A10002 /* Components */, |
|
| 573 |
- 4311E639241384960080EA59 /* DeviceHelpView.swift */, |
|
| 574 |
- AAD5F9A32B1CAC0700F8E4F9 /* MeterDetailView.swift */, |
|
| 562 |
+ 437D47D42415FD8C00B7768E /* ChargeRecordSheetView.swift */, |
|
| 563 |
+ D28F11383C8E4A7A00A10048 /* Subviews */, |
|
| 575 | 564 |
); |
| 576 |
- path = Views; |
|
| 565 |
+ path = ChargeRecord; |
|
| 577 | 566 |
sourceTree = "<group>"; |
| 578 | 567 |
}; |
| 579 |
- 43CBF67F240D14AC00255B8B /* Extensions */ = {
|
|
| 568 |
+ D28F11383C8E4A7A00A10048 /* Subviews */ = {
|
|
| 580 | 569 |
isa = PBXGroup; |
| 581 | 570 |
children = ( |
| 582 |
- 43CBF680240D153000255B8B /* CBManagerState.swift */, |
|
| 583 |
- 4351E7BA24685ACD00E798A3 /* CGPoint.swift */, |
|
| 584 |
- 43874C7E2414F3F400525397 /* Float.swift */, |
|
| 585 |
- 43F7792A2465AE1600745DF4 /* UIView.swift */, |
|
| 586 |
- 43874C842415611200525397 /* Double.swift */, |
|
| 587 |
- 43874C82241533AD00525397 /* Data.swift */, |
|
| 588 |
- 437D47D22415FB7E00B7768E /* Decimal.swift */, |
|
| 589 |
- 43554B3324444B0E004E66F5 /* Date.swift */, |
|
| 590 |
- 438B9554246D2D7500E61AE7 /* Path.swift */, |
|
| 571 |
+ D28F11363C8E4A7A00A10046 /* ChargeRecordMetricsTableView.swift */, |
|
| 591 | 572 |
); |
| 592 |
- path = Extensions; |
|
| 573 |
+ path = Subviews; |
|
| 593 | 574 |
sourceTree = "<group>"; |
| 594 | 575 |
}; |
| 595 |
- AAD5F9BB2B1CC10000F8E4F9 /* Sidebar */ = {
|
|
| 576 |
+ D28F113F3C8E4A7A00A1004F /* Components */ = {
|
|
| 596 | 577 |
isa = PBXGroup; |
| 597 | 578 |
children = ( |
| 598 |
- AAD5F9B22B1CAC7A00F8E4F9 /* SidebarView.swift */, |
|
| 599 |
- 43BE08E12F78F49500250EEC /* SidebarList */, |
|
| 579 |
+ D28F11123C8E4A7A00A10022 /* MeterInfoCardView.swift */, |
|
| 580 |
+ D28F11143C8E4A7A00A10024 /* MeterInfoRowView.swift */, |
|
| 581 |
+ 437F0AB62463108F005DEBEC /* MeasurementChartView.swift */, |
|
| 600 | 582 |
); |
| 601 |
- path = Sidebar; |
|
| 583 |
+ path = Components; |
|
| 602 | 584 |
sourceTree = "<group>"; |
| 603 | 585 |
}; |
| 604 | 586 |
/* End PBXGroup section */ |
@@ -902,7 +884,10 @@ |
||
| 902 | 884 |
PRODUCT_BUNDLE_IDENTIFIER = "ro.xdev.USB-Meter"; |
| 903 | 885 |
PRODUCT_NAME = "$(TARGET_NAME)"; |
| 904 | 886 |
STRING_CATALOG_GENERATE_SYMBOLS = YES; |
| 887 |
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; |
|
| 905 | 888 |
SUPPORTS_MACCATALYST = YES; |
| 889 |
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; |
|
| 890 |
+ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; |
|
| 906 | 891 |
SWIFT_VERSION = 5.0; |
| 907 | 892 |
TARGETED_DEVICE_FAMILY = "1,2"; |
| 908 | 893 |
}; |
@@ -926,7 +911,10 @@ |
||
| 926 | 911 |
PRODUCT_BUNDLE_IDENTIFIER = "ro.xdev.USB-Meter"; |
| 927 | 912 |
PRODUCT_NAME = "$(TARGET_NAME)"; |
| 928 | 913 |
STRING_CATALOG_GENERATE_SYMBOLS = YES; |
| 914 |
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; |
|
| 929 | 915 |
SUPPORTS_MACCATALYST = YES; |
| 916 |
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; |
|
| 917 |
+ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; |
|
| 930 | 918 |
SWIFT_VERSION = 5.0; |
| 931 | 919 |
TARGETED_DEVICE_FAMILY = "1,2"; |
| 932 | 920 |
}; |
@@ -8,6 +8,11 @@ |
||
| 8 | 8 |
|
| 9 | 9 |
import SwiftUI |
| 10 | 10 |
|
| 11 |
+private enum PresentTrackingMode: CaseIterable, Hashable {
|
|
| 12 |
+ case keepDuration |
|
| 13 |
+ case keepStartTimestamp |
|
| 14 |
+} |
|
| 15 |
+ |
|
| 11 | 16 |
struct MeasurementChartView: View {
|
| 12 | 17 |
private enum SeriesKind {
|
| 13 | 18 |
case power |
@@ -65,6 +70,9 @@ struct MeasurementChartView: View {
|
||
| 65 | 70 |
@State var displayTemperature: Bool = false |
| 66 | 71 |
@State private var showResetConfirmation: Bool = false |
| 67 | 72 |
@State private var chartNow: Date = Date() |
| 73 |
+ @State private var selectedVisibleTimeRange: ClosedRange<Date>? |
|
| 74 |
+ @State private var isPinnedToPresent: Bool = false |
|
| 75 |
+ @State private var presentTrackingMode: PresentTrackingMode = .keepDuration |
|
| 68 | 76 |
@State private var pinOrigin: Bool = false |
| 69 | 77 |
@State private var useSharedOrigin: Bool = false |
| 70 | 78 |
@State private var sharedAxisOrigin: Double = 0 |
@@ -185,15 +193,38 @@ struct MeasurementChartView: View {
|
||
| 185 | 193 |
} |
| 186 | 194 |
|
| 187 | 195 |
var body: some View {
|
| 188 |
- let powerSeries = series(for: measurements.power, kind: .power, minimumYSpan: minimumPowerSpan) |
|
| 189 |
- let voltageSeries = series(for: measurements.voltage, kind: .voltage, minimumYSpan: minimumVoltageSpan) |
|
| 190 |
- let currentSeries = series(for: measurements.current, kind: .current, minimumYSpan: minimumCurrentSpan) |
|
| 191 |
- let temperatureSeries = series(for: measurements.temperature, kind: .temperature, minimumYSpan: minimumTemperatureSpan) |
|
| 196 |
+ let availableTimeRange = availableSelectionTimeRange() |
|
| 197 |
+ let visibleTimeRange = resolvedVisibleTimeRange(within: availableTimeRange) |
|
| 198 |
+ let powerSeries = series( |
|
| 199 |
+ for: measurements.power, |
|
| 200 |
+ kind: .power, |
|
| 201 |
+ minimumYSpan: minimumPowerSpan, |
|
| 202 |
+ visibleTimeRange: visibleTimeRange |
|
| 203 |
+ ) |
|
| 204 |
+ let voltageSeries = series( |
|
| 205 |
+ for: measurements.voltage, |
|
| 206 |
+ kind: .voltage, |
|
| 207 |
+ minimumYSpan: minimumVoltageSpan, |
|
| 208 |
+ visibleTimeRange: visibleTimeRange |
|
| 209 |
+ ) |
|
| 210 |
+ let currentSeries = series( |
|
| 211 |
+ for: measurements.current, |
|
| 212 |
+ kind: .current, |
|
| 213 |
+ minimumYSpan: minimumCurrentSpan, |
|
| 214 |
+ visibleTimeRange: visibleTimeRange |
|
| 215 |
+ ) |
|
| 216 |
+ let temperatureSeries = series( |
|
| 217 |
+ for: measurements.temperature, |
|
| 218 |
+ kind: .temperature, |
|
| 219 |
+ minimumYSpan: minimumTemperatureSpan, |
|
| 220 |
+ visibleTimeRange: visibleTimeRange |
|
| 221 |
+ ) |
|
| 192 | 222 |
let primarySeries = displayedPrimarySeries( |
| 193 | 223 |
powerSeries: powerSeries, |
| 194 | 224 |
voltageSeries: voltageSeries, |
| 195 | 225 |
currentSeries: currentSeries |
| 196 | 226 |
) |
| 227 |
+ let selectorSeries = primarySeries.map { overviewSeries(for: $0.kind) }
|
|
| 197 | 228 |
|
| 198 | 229 |
Group {
|
| 199 | 230 |
if let primarySeries {
|
@@ -280,6 +311,25 @@ struct MeasurementChartView: View {
|
||
| 280 | 311 |
Spacer(minLength: 0) |
| 281 | 312 |
} |
| 282 | 313 |
} |
| 314 |
+ |
|
| 315 |
+ if let availableTimeRange, |
|
| 316 |
+ let selectorSeries, |
|
| 317 |
+ shouldShowRangeSelector( |
|
| 318 |
+ availableTimeRange: availableTimeRange, |
|
| 319 |
+ series: selectorSeries |
|
| 320 |
+ ) {
|
|
| 321 |
+ TimeRangeSelectorView( |
|
| 322 |
+ points: selectorSeries.points, |
|
| 323 |
+ context: selectorSeries.context, |
|
| 324 |
+ availableTimeRange: availableTimeRange, |
|
| 325 |
+ accentColor: selectorSeries.kind.tint, |
|
| 326 |
+ compactLayout: compactLayout, |
|
| 327 |
+ minimumSelectionSpan: minimumTimeSpan, |
|
| 328 |
+ selectedTimeRange: $selectedVisibleTimeRange, |
|
| 329 |
+ isPinnedToPresent: $isPinnedToPresent, |
|
| 330 |
+ presentTrackingMode: $presentTrackingMode |
|
| 331 |
+ ) |
|
| 332 |
+ } |
|
| 283 | 333 |
} |
| 284 | 334 |
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) |
| 285 | 335 |
} |
@@ -695,12 +745,13 @@ struct MeasurementChartView: View {
|
||
| 695 | 745 |
private func series( |
| 696 | 746 |
for measurement: Measurements.Measurement, |
| 697 | 747 |
kind: SeriesKind, |
| 698 |
- minimumYSpan: Double |
|
| 748 |
+ minimumYSpan: Double, |
|
| 749 |
+ visibleTimeRange: ClosedRange<Date>? = nil |
|
| 699 | 750 |
) -> SeriesData {
|
| 700 |
- let points = measurement.points.filter { point in
|
|
| 701 |
- guard let timeRange else { return true }
|
|
| 702 |
- return timeRange.contains(point.timestamp) |
|
| 703 |
- } |
|
| 751 |
+ let points = filteredPoints( |
|
| 752 |
+ measurement, |
|
| 753 |
+ visibleTimeRange: visibleTimeRange |
|
| 754 |
+ ) |
|
| 704 | 755 |
let samplePoints = points.filter { $0.isSample }
|
| 705 | 756 |
let context = ChartContext() |
| 706 | 757 |
|
@@ -708,7 +759,10 @@ struct MeasurementChartView: View {
|
||
| 708 | 759 |
for: samplePoints, |
| 709 | 760 |
minimumYSpan: minimumYSpan |
| 710 | 761 |
) |
| 711 |
- let xBounds = xBounds(for: samplePoints) |
|
| 762 |
+ let xBounds = xBounds( |
|
| 763 |
+ for: samplePoints, |
|
| 764 |
+ visibleTimeRange: visibleTimeRange |
|
| 765 |
+ ) |
|
| 712 | 766 |
let lowerBound = resolvedLowerBound( |
| 713 | 767 |
for: kind, |
| 714 | 768 |
autoLowerBound: autoBounds.lowerBound |
@@ -739,6 +793,40 @@ struct MeasurementChartView: View {
|
||
| 739 | 793 |
) |
| 740 | 794 |
} |
| 741 | 795 |
|
| 796 |
+ private func overviewSeries(for kind: SeriesKind) -> SeriesData {
|
|
| 797 |
+ series( |
|
| 798 |
+ for: measurement(for: kind), |
|
| 799 |
+ kind: kind, |
|
| 800 |
+ minimumYSpan: minimumYSpan(for: kind) |
|
| 801 |
+ ) |
|
| 802 |
+ } |
|
| 803 |
+ |
|
| 804 |
+ private func measurement(for kind: SeriesKind) -> Measurements.Measurement {
|
|
| 805 |
+ switch kind {
|
|
| 806 |
+ case .power: |
|
| 807 |
+ return measurements.power |
|
| 808 |
+ case .voltage: |
|
| 809 |
+ return measurements.voltage |
|
| 810 |
+ case .current: |
|
| 811 |
+ return measurements.current |
|
| 812 |
+ case .temperature: |
|
| 813 |
+ return measurements.temperature |
|
| 814 |
+ } |
|
| 815 |
+ } |
|
| 816 |
+ |
|
| 817 |
+ private func minimumYSpan(for kind: SeriesKind) -> Double {
|
|
| 818 |
+ switch kind {
|
|
| 819 |
+ case .power: |
|
| 820 |
+ return minimumPowerSpan |
|
| 821 |
+ case .voltage: |
|
| 822 |
+ return minimumVoltageSpan |
|
| 823 |
+ case .current: |
|
| 824 |
+ return minimumCurrentSpan |
|
| 825 |
+ case .temperature: |
|
| 826 |
+ return minimumTemperatureSpan |
|
| 827 |
+ } |
|
| 828 |
+ } |
|
| 829 |
+ |
|
| 742 | 830 |
private var supportsSharedOrigin: Bool {
|
| 743 | 831 |
displayVoltage && displayCurrent && !displayPower |
| 744 | 832 |
} |
@@ -844,45 +932,208 @@ struct MeasurementChartView: View {
|
||
| 844 | 932 |
} |
| 845 | 933 |
|
| 846 | 934 |
private func displayedLowerBoundForSeries(_ kind: SeriesKind) -> Double {
|
| 935 |
+ let visibleTimeRange = activeVisibleTimeRange |
|
| 936 |
+ |
|
| 847 | 937 |
switch kind {
|
| 848 | 938 |
case .power: |
| 849 |
- return pinOrigin ? powerAxisOrigin : automaticYBounds(for: filteredSamplePoints(measurements.power), minimumYSpan: minimumPowerSpan).lowerBound |
|
| 939 |
+ return pinOrigin |
|
| 940 |
+ ? powerAxisOrigin |
|
| 941 |
+ : automaticYBounds( |
|
| 942 |
+ for: filteredSamplePoints( |
|
| 943 |
+ measurements.power, |
|
| 944 |
+ visibleTimeRange: visibleTimeRange |
|
| 945 |
+ ), |
|
| 946 |
+ minimumYSpan: minimumPowerSpan |
|
| 947 |
+ ).lowerBound |
|
| 850 | 948 |
case .voltage: |
| 851 | 949 |
if pinOrigin && useSharedOrigin && supportsSharedOrigin {
|
| 852 | 950 |
return sharedAxisOrigin |
| 853 | 951 |
} |
| 854 |
- return pinOrigin ? voltageAxisOrigin : automaticYBounds(for: filteredSamplePoints(measurements.voltage), minimumYSpan: minimumVoltageSpan).lowerBound |
|
| 952 |
+ return pinOrigin |
|
| 953 |
+ ? voltageAxisOrigin |
|
| 954 |
+ : automaticYBounds( |
|
| 955 |
+ for: filteredSamplePoints( |
|
| 956 |
+ measurements.voltage, |
|
| 957 |
+ visibleTimeRange: visibleTimeRange |
|
| 958 |
+ ), |
|
| 959 |
+ minimumYSpan: minimumVoltageSpan |
|
| 960 |
+ ).lowerBound |
|
| 855 | 961 |
case .current: |
| 856 | 962 |
if pinOrigin && useSharedOrigin && supportsSharedOrigin {
|
| 857 | 963 |
return sharedAxisOrigin |
| 858 | 964 |
} |
| 859 |
- return pinOrigin ? currentAxisOrigin : automaticYBounds(for: filteredSamplePoints(measurements.current), minimumYSpan: minimumCurrentSpan).lowerBound |
|
| 965 |
+ return pinOrigin |
|
| 966 |
+ ? currentAxisOrigin |
|
| 967 |
+ : automaticYBounds( |
|
| 968 |
+ for: filteredSamplePoints( |
|
| 969 |
+ measurements.current, |
|
| 970 |
+ visibleTimeRange: visibleTimeRange |
|
| 971 |
+ ), |
|
| 972 |
+ minimumYSpan: minimumCurrentSpan |
|
| 973 |
+ ).lowerBound |
|
| 860 | 974 |
case .temperature: |
| 861 |
- return pinOrigin ? temperatureAxisOrigin : automaticYBounds(for: filteredSamplePoints(measurements.temperature), minimumYSpan: minimumTemperatureSpan).lowerBound |
|
| 975 |
+ return pinOrigin |
|
| 976 |
+ ? temperatureAxisOrigin |
|
| 977 |
+ : automaticYBounds( |
|
| 978 |
+ for: filteredSamplePoints( |
|
| 979 |
+ measurements.temperature, |
|
| 980 |
+ visibleTimeRange: visibleTimeRange |
|
| 981 |
+ ), |
|
| 982 |
+ minimumYSpan: minimumTemperatureSpan |
|
| 983 |
+ ).lowerBound |
|
| 862 | 984 |
} |
| 863 | 985 |
} |
| 864 | 986 |
|
| 865 |
- private func filteredSamplePoints(_ measurement: Measurements.Measurement) -> [Measurements.Measurement.Point] {
|
|
| 987 |
+ private var activeVisibleTimeRange: ClosedRange<Date>? {
|
|
| 988 |
+ resolvedVisibleTimeRange(within: availableSelectionTimeRange()) |
|
| 989 |
+ } |
|
| 990 |
+ |
|
| 991 |
+ private func filteredPoints( |
|
| 992 |
+ _ measurement: Measurements.Measurement, |
|
| 993 |
+ visibleTimeRange: ClosedRange<Date>? = nil |
|
| 994 |
+ ) -> [Measurements.Measurement.Point] {
|
|
| 866 | 995 |
measurement.points.filter { point in
|
| 867 |
- point.isSample && (timeRange?.contains(point.timestamp) ?? true) |
|
| 996 |
+ guard timeRange?.contains(point.timestamp) ?? true else { return false }
|
|
| 997 |
+ return visibleTimeRange?.contains(point.timestamp) ?? true |
|
| 998 |
+ } |
|
| 999 |
+ } |
|
| 1000 |
+ |
|
| 1001 |
+ private func filteredSamplePoints( |
|
| 1002 |
+ _ measurement: Measurements.Measurement, |
|
| 1003 |
+ visibleTimeRange: ClosedRange<Date>? = nil |
|
| 1004 |
+ ) -> [Measurements.Measurement.Point] {
|
|
| 1005 |
+ filteredPoints(measurement, visibleTimeRange: visibleTimeRange).filter { point in
|
|
| 1006 |
+ point.isSample |
|
| 868 | 1007 |
} |
| 869 | 1008 |
} |
| 870 | 1009 |
|
| 871 | 1010 |
private func xBounds( |
| 872 |
- for samplePoints: [Measurements.Measurement.Point] |
|
| 1011 |
+ for samplePoints: [Measurements.Measurement.Point], |
|
| 1012 |
+ visibleTimeRange: ClosedRange<Date>? = nil |
|
| 873 | 1013 |
) -> ClosedRange<Date> {
|
| 1014 |
+ if let visibleTimeRange {
|
|
| 1015 |
+ return normalizedTimeRange(visibleTimeRange) |
|
| 1016 |
+ } |
|
| 1017 |
+ |
|
| 874 | 1018 |
if let timeRange {
|
| 875 |
- return timeRange |
|
| 1019 |
+ return normalizedTimeRange(timeRange) |
|
| 876 | 1020 |
} |
| 877 | 1021 |
|
| 878 | 1022 |
let upperBound = max(samplePoints.last?.timestamp ?? chartNow, chartNow) |
| 879 | 1023 |
let lowerBound = samplePoints.first?.timestamp ?? upperBound.addingTimeInterval(-defaultEmptyChartTimeSpan) |
| 880 | 1024 |
|
| 881 |
- if upperBound.timeIntervalSince(lowerBound) >= minimumTimeSpan {
|
|
| 882 |
- return lowerBound...upperBound |
|
| 1025 |
+ return normalizedTimeRange(lowerBound...upperBound) |
|
| 1026 |
+ } |
|
| 1027 |
+ |
|
| 1028 |
+ private func availableSelectionTimeRange() -> ClosedRange<Date>? {
|
|
| 1029 |
+ if let timeRange {
|
|
| 1030 |
+ return normalizedTimeRange(timeRange) |
|
| 1031 |
+ } |
|
| 1032 |
+ |
|
| 1033 |
+ let samplePoints = timelineSamplePoints() |
|
| 1034 |
+ guard let lowerBound = samplePoints.first?.timestamp else {
|
|
| 1035 |
+ return nil |
|
| 1036 |
+ } |
|
| 1037 |
+ |
|
| 1038 |
+ let upperBound = max(samplePoints.last?.timestamp ?? chartNow, chartNow) |
|
| 1039 |
+ return normalizedTimeRange(lowerBound...upperBound) |
|
| 1040 |
+ } |
|
| 1041 |
+ |
|
| 1042 |
+ private func timelineSamplePoints() -> [Measurements.Measurement.Point] {
|
|
| 1043 |
+ let candidates = [ |
|
| 1044 |
+ filteredSamplePoints(measurements.power), |
|
| 1045 |
+ filteredSamplePoints(measurements.voltage), |
|
| 1046 |
+ filteredSamplePoints(measurements.current), |
|
| 1047 |
+ filteredSamplePoints(measurements.temperature) |
|
| 1048 |
+ ] |
|
| 1049 |
+ |
|
| 1050 |
+ return candidates.first(where: { !$0.isEmpty }) ?? []
|
|
| 1051 |
+ } |
|
| 1052 |
+ |
|
| 1053 |
+ private func resolvedVisibleTimeRange( |
|
| 1054 |
+ within availableTimeRange: ClosedRange<Date>? |
|
| 1055 |
+ ) -> ClosedRange<Date>? {
|
|
| 1056 |
+ guard let availableTimeRange else { return nil }
|
|
| 1057 |
+ guard let selectedVisibleTimeRange else { return availableTimeRange }
|
|
| 1058 |
+ |
|
| 1059 |
+ if isPinnedToPresent {
|
|
| 1060 |
+ let pinnedRange: ClosedRange<Date> |
|
| 1061 |
+ |
|
| 1062 |
+ switch presentTrackingMode {
|
|
| 1063 |
+ case .keepDuration: |
|
| 1064 |
+ let selectionSpan = selectedVisibleTimeRange.upperBound.timeIntervalSince(selectedVisibleTimeRange.lowerBound) |
|
| 1065 |
+ pinnedRange = availableTimeRange.upperBound.addingTimeInterval(-selectionSpan)...availableTimeRange.upperBound |
|
| 1066 |
+ case .keepStartTimestamp: |
|
| 1067 |
+ pinnedRange = selectedVisibleTimeRange.lowerBound...availableTimeRange.upperBound |
|
| 1068 |
+ } |
|
| 1069 |
+ |
|
| 1070 |
+ return clampedTimeRange(pinnedRange, within: availableTimeRange) |
|
| 1071 |
+ } |
|
| 1072 |
+ |
|
| 1073 |
+ return clampedTimeRange(selectedVisibleTimeRange, within: availableTimeRange) |
|
| 1074 |
+ } |
|
| 1075 |
+ |
|
| 1076 |
+ private func clampedTimeRange( |
|
| 1077 |
+ _ candidateRange: ClosedRange<Date>, |
|
| 1078 |
+ within bounds: ClosedRange<Date> |
|
| 1079 |
+ ) -> ClosedRange<Date> {
|
|
| 1080 |
+ let normalizedBounds = normalizedTimeRange(bounds) |
|
| 1081 |
+ let boundsSpan = normalizedBounds.upperBound.timeIntervalSince(normalizedBounds.lowerBound) |
|
| 1082 |
+ |
|
| 1083 |
+ guard boundsSpan > 0 else {
|
|
| 1084 |
+ return normalizedBounds |
|
| 1085 |
+ } |
|
| 1086 |
+ |
|
| 1087 |
+ let minimumSpan = min(max(minimumTimeSpan, 0.1), boundsSpan) |
|
| 1088 |
+ let requestedSpan = min( |
|
| 1089 |
+ max(candidateRange.upperBound.timeIntervalSince(candidateRange.lowerBound), minimumSpan), |
|
| 1090 |
+ boundsSpan |
|
| 1091 |
+ ) |
|
| 1092 |
+ |
|
| 1093 |
+ if requestedSpan >= boundsSpan {
|
|
| 1094 |
+ return normalizedBounds |
|
| 1095 |
+ } |
|
| 1096 |
+ |
|
| 1097 |
+ var lowerBound = max(candidateRange.lowerBound, normalizedBounds.lowerBound) |
|
| 1098 |
+ var upperBound = min(candidateRange.upperBound, normalizedBounds.upperBound) |
|
| 1099 |
+ |
|
| 1100 |
+ if upperBound.timeIntervalSince(lowerBound) < requestedSpan {
|
|
| 1101 |
+ if lowerBound == normalizedBounds.lowerBound {
|
|
| 1102 |
+ upperBound = lowerBound.addingTimeInterval(requestedSpan) |
|
| 1103 |
+ } else {
|
|
| 1104 |
+ lowerBound = upperBound.addingTimeInterval(-requestedSpan) |
|
| 1105 |
+ } |
|
| 1106 |
+ } |
|
| 1107 |
+ |
|
| 1108 |
+ if upperBound > normalizedBounds.upperBound {
|
|
| 1109 |
+ let delta = upperBound.timeIntervalSince(normalizedBounds.upperBound) |
|
| 1110 |
+ upperBound = normalizedBounds.upperBound |
|
| 1111 |
+ lowerBound = lowerBound.addingTimeInterval(-delta) |
|
| 883 | 1112 |
} |
| 884 | 1113 |
|
| 885 |
- return upperBound.addingTimeInterval(-minimumTimeSpan)...upperBound |
|
| 1114 |
+ if lowerBound < normalizedBounds.lowerBound {
|
|
| 1115 |
+ let delta = normalizedBounds.lowerBound.timeIntervalSince(lowerBound) |
|
| 1116 |
+ lowerBound = normalizedBounds.lowerBound |
|
| 1117 |
+ upperBound = upperBound.addingTimeInterval(delta) |
|
| 1118 |
+ } |
|
| 1119 |
+ |
|
| 1120 |
+ return lowerBound...upperBound |
|
| 1121 |
+ } |
|
| 1122 |
+ |
|
| 1123 |
+ private func normalizedTimeRange(_ range: ClosedRange<Date>) -> ClosedRange<Date> {
|
|
| 1124 |
+ let span = range.upperBound.timeIntervalSince(range.lowerBound) |
|
| 1125 |
+ guard span < minimumTimeSpan else { return range }
|
|
| 1126 |
+ |
|
| 1127 |
+ let expansion = (minimumTimeSpan - span) / 2 |
|
| 1128 |
+ return range.lowerBound.addingTimeInterval(-expansion)...range.upperBound.addingTimeInterval(expansion) |
|
| 1129 |
+ } |
|
| 1130 |
+ |
|
| 1131 |
+ private func shouldShowRangeSelector( |
|
| 1132 |
+ availableTimeRange: ClosedRange<Date>, |
|
| 1133 |
+ series: SeriesData |
|
| 1134 |
+ ) -> Bool {
|
|
| 1135 |
+ series.samplePoints.count > 1 && |
|
| 1136 |
+ availableTimeRange.upperBound.timeIntervalSince(availableTimeRange.lowerBound) > minimumTimeSpan |
|
| 886 | 1137 |
} |
| 887 | 1138 |
|
| 888 | 1139 |
private func automaticYBounds( |
@@ -1030,15 +1281,37 @@ struct MeasurementChartView: View {
|
||
| 1030 | 1281 |
} |
| 1031 | 1282 |
|
| 1032 | 1283 |
private func maximumVisibleOrigin(for kind: SeriesKind) -> Double {
|
| 1284 |
+ let visibleTimeRange = activeVisibleTimeRange |
|
| 1285 |
+ |
|
| 1033 | 1286 |
switch kind {
|
| 1034 | 1287 |
case .power: |
| 1035 |
- return snappedOriginValue(filteredSamplePoints(measurements.power).map(\.value).min() ?? 0) |
|
| 1288 |
+ return snappedOriginValue( |
|
| 1289 |
+ filteredSamplePoints( |
|
| 1290 |
+ measurements.power, |
|
| 1291 |
+ visibleTimeRange: visibleTimeRange |
|
| 1292 |
+ ).map(\.value).min() ?? 0 |
|
| 1293 |
+ ) |
|
| 1036 | 1294 |
case .voltage: |
| 1037 |
- return snappedOriginValue(filteredSamplePoints(measurements.voltage).map(\.value).min() ?? 0) |
|
| 1295 |
+ return snappedOriginValue( |
|
| 1296 |
+ filteredSamplePoints( |
|
| 1297 |
+ measurements.voltage, |
|
| 1298 |
+ visibleTimeRange: visibleTimeRange |
|
| 1299 |
+ ).map(\.value).min() ?? 0 |
|
| 1300 |
+ ) |
|
| 1038 | 1301 |
case .current: |
| 1039 |
- return snappedOriginValue(filteredSamplePoints(measurements.current).map(\.value).min() ?? 0) |
|
| 1302 |
+ return snappedOriginValue( |
|
| 1303 |
+ filteredSamplePoints( |
|
| 1304 |
+ measurements.current, |
|
| 1305 |
+ visibleTimeRange: visibleTimeRange |
|
| 1306 |
+ ).map(\.value).min() ?? 0 |
|
| 1307 |
+ ) |
|
| 1040 | 1308 |
case .temperature: |
| 1041 |
- return snappedOriginValue(filteredSamplePoints(measurements.temperature).map(\.value).min() ?? 0) |
|
| 1309 |
+ return snappedOriginValue( |
|
| 1310 |
+ filteredSamplePoints( |
|
| 1311 |
+ measurements.temperature, |
|
| 1312 |
+ visibleTimeRange: visibleTimeRange |
|
| 1313 |
+ ).map(\.value).min() ?? 0 |
|
| 1314 |
+ ) |
|
| 1042 | 1315 |
} |
| 1043 | 1316 |
} |
| 1044 | 1317 |
|
@@ -1308,18 +1581,633 @@ struct MeasurementChartView: View {
|
||
| 1308 | 1581 |
|
| 1309 | 1582 |
} |
| 1310 | 1583 |
|
| 1584 |
+private struct TimeRangeSelectorView: View {
|
|
| 1585 |
+ private enum DragTarget {
|
|
| 1586 |
+ case lowerBound |
|
| 1587 |
+ case upperBound |
|
| 1588 |
+ case window |
|
| 1589 |
+ } |
|
| 1590 |
+ |
|
| 1591 |
+ private struct DragState {
|
|
| 1592 |
+ let target: DragTarget |
|
| 1593 |
+ let initialRange: ClosedRange<Date> |
|
| 1594 |
+ } |
|
| 1595 |
+ |
|
| 1596 |
+ let points: [Measurements.Measurement.Point] |
|
| 1597 |
+ let context: ChartContext |
|
| 1598 |
+ let availableTimeRange: ClosedRange<Date> |
|
| 1599 |
+ let accentColor: Color |
|
| 1600 |
+ let compactLayout: Bool |
|
| 1601 |
+ let minimumSelectionSpan: TimeInterval |
|
| 1602 |
+ |
|
| 1603 |
+ @Binding var selectedTimeRange: ClosedRange<Date>? |
|
| 1604 |
+ @Binding var isPinnedToPresent: Bool |
|
| 1605 |
+ @Binding var presentTrackingMode: PresentTrackingMode |
|
| 1606 |
+ @State private var dragState: DragState? |
|
| 1607 |
+ |
|
| 1608 |
+ private var totalSpan: TimeInterval {
|
|
| 1609 |
+ availableTimeRange.upperBound.timeIntervalSince(availableTimeRange.lowerBound) |
|
| 1610 |
+ } |
|
| 1611 |
+ |
|
| 1612 |
+ private var currentRange: ClosedRange<Date> {
|
|
| 1613 |
+ resolvedSelectionRange() |
|
| 1614 |
+ } |
|
| 1615 |
+ |
|
| 1616 |
+ private var trackHeight: CGFloat {
|
|
| 1617 |
+ compactLayout ? 72 : 86 |
|
| 1618 |
+ } |
|
| 1619 |
+ |
|
| 1620 |
+ private var cornerRadius: CGFloat {
|
|
| 1621 |
+ compactLayout ? 14 : 16 |
|
| 1622 |
+ } |
|
| 1623 |
+ |
|
| 1624 |
+ private var summaryFont: Font {
|
|
| 1625 |
+ compactLayout ? .caption.weight(.semibold) : .footnote.weight(.semibold) |
|
| 1626 |
+ } |
|
| 1627 |
+ |
|
| 1628 |
+ private var boundaryFont: Font {
|
|
| 1629 |
+ compactLayout ? .caption.weight(.semibold) : .footnote.weight(.semibold) |
|
| 1630 |
+ } |
|
| 1631 |
+ |
|
| 1632 |
+ private var symbolButtonSize: CGFloat {
|
|
| 1633 |
+ compactLayout ? 28 : 32 |
|
| 1634 |
+ } |
|
| 1635 |
+ |
|
| 1636 |
+ var body: some View {
|
|
| 1637 |
+ let coversFullRange = selectionCoversFullRange(currentRange) |
|
| 1638 |
+ |
|
| 1639 |
+ VStack(alignment: .leading, spacing: compactLayout ? 6 : 8) {
|
|
| 1640 |
+ if !coversFullRange || isPinnedToPresent {
|
|
| 1641 |
+ HStack(spacing: 8) {
|
|
| 1642 |
+ alignmentButton( |
|
| 1643 |
+ systemName: "arrow.left.to.line.compact", |
|
| 1644 |
+ isActive: currentRange.lowerBound == availableTimeRange.lowerBound && !isPinnedToPresent, |
|
| 1645 |
+ action: alignSelectionToLeadingEdge, |
|
| 1646 |
+ accessibilityLabel: "Align selection to start" |
|
| 1647 |
+ ) |
|
| 1648 |
+ |
|
| 1649 |
+ alignmentButton( |
|
| 1650 |
+ systemName: "arrow.right.to.line.compact", |
|
| 1651 |
+ isActive: isPinnedToPresent || selectionTouchesPresent(currentRange), |
|
| 1652 |
+ action: alignSelectionToTrailingEdge, |
|
| 1653 |
+ accessibilityLabel: "Align selection to present" |
|
| 1654 |
+ ) |
|
| 1655 |
+ |
|
| 1656 |
+ Spacer(minLength: 0) |
|
| 1657 |
+ |
|
| 1658 |
+ if isPinnedToPresent {
|
|
| 1659 |
+ trackingModeToggleButton() |
|
| 1660 |
+ } |
|
| 1661 |
+ } |
|
| 1662 |
+ } |
|
| 1663 |
+ |
|
| 1664 |
+ GeometryReader { geometry in
|
|
| 1665 |
+ let selectionFrame = selectionFrame(in: geometry.size) |
|
| 1666 |
+ let dimmingColor = Color(uiColor: .systemBackground).opacity(0.58) |
|
| 1667 |
+ |
|
| 1668 |
+ ZStack(alignment: .topLeading) {
|
|
| 1669 |
+ RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) |
|
| 1670 |
+ .fill(Color.primary.opacity(0.05)) |
|
| 1671 |
+ |
|
| 1672 |
+ Chart( |
|
| 1673 |
+ points: points, |
|
| 1674 |
+ context: context, |
|
| 1675 |
+ areaChart: true, |
|
| 1676 |
+ strokeColor: accentColor, |
|
| 1677 |
+ areaFillColor: accentColor.opacity(0.22) |
|
| 1678 |
+ ) |
|
| 1679 |
+ .opacity(0.94) |
|
| 1680 |
+ .allowsHitTesting(false) |
|
| 1681 |
+ |
|
| 1682 |
+ Chart( |
|
| 1683 |
+ points: points, |
|
| 1684 |
+ context: context, |
|
| 1685 |
+ strokeColor: accentColor.opacity(0.56) |
|
| 1686 |
+ ) |
|
| 1687 |
+ .opacity(0.82) |
|
| 1688 |
+ .allowsHitTesting(false) |
|
| 1689 |
+ |
|
| 1690 |
+ if selectionFrame.minX > 0 {
|
|
| 1691 |
+ Rectangle() |
|
| 1692 |
+ .fill(dimmingColor) |
|
| 1693 |
+ .frame(width: selectionFrame.minX, height: geometry.size.height) |
|
| 1694 |
+ .allowsHitTesting(false) |
|
| 1695 |
+ } |
|
| 1696 |
+ |
|
| 1697 |
+ if selectionFrame.maxX < geometry.size.width {
|
|
| 1698 |
+ Rectangle() |
|
| 1699 |
+ .fill(dimmingColor) |
|
| 1700 |
+ .frame( |
|
| 1701 |
+ width: max(geometry.size.width - selectionFrame.maxX, 0), |
|
| 1702 |
+ height: geometry.size.height |
|
| 1703 |
+ ) |
|
| 1704 |
+ .offset(x: selectionFrame.maxX) |
|
| 1705 |
+ .allowsHitTesting(false) |
|
| 1706 |
+ } |
|
| 1707 |
+ |
|
| 1708 |
+ RoundedRectangle(cornerRadius: max(cornerRadius - 2, 10), style: .continuous) |
|
| 1709 |
+ .fill(accentColor.opacity(0.18)) |
|
| 1710 |
+ .frame(width: max(selectionFrame.width, 2), height: geometry.size.height) |
|
| 1711 |
+ .offset(x: selectionFrame.minX) |
|
| 1712 |
+ .allowsHitTesting(false) |
|
| 1713 |
+ |
|
| 1714 |
+ RoundedRectangle(cornerRadius: max(cornerRadius - 2, 10), style: .continuous) |
|
| 1715 |
+ .stroke(accentColor.opacity(0.52), lineWidth: 1.2) |
|
| 1716 |
+ .frame(width: max(selectionFrame.width, 2), height: geometry.size.height) |
|
| 1717 |
+ .offset(x: selectionFrame.minX) |
|
| 1718 |
+ .allowsHitTesting(false) |
|
| 1719 |
+ |
|
| 1720 |
+ handleView(height: max(geometry.size.height - 18, 16)) |
|
| 1721 |
+ .offset(x: max(selectionFrame.minX + 6, 6), y: 9) |
|
| 1722 |
+ .allowsHitTesting(false) |
|
| 1723 |
+ |
|
| 1724 |
+ handleView(height: max(geometry.size.height - 18, 16)) |
|
| 1725 |
+ .offset(x: max(selectionFrame.maxX - 12, 6), y: 9) |
|
| 1726 |
+ .allowsHitTesting(false) |
|
| 1727 |
+ } |
|
| 1728 |
+ .clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) |
|
| 1729 |
+ .overlay( |
|
| 1730 |
+ RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) |
|
| 1731 |
+ .stroke(Color.secondary.opacity(0.18), lineWidth: 1) |
|
| 1732 |
+ ) |
|
| 1733 |
+ .contentShape(Rectangle()) |
|
| 1734 |
+ .gesture(selectionGesture(totalWidth: geometry.size.width)) |
|
| 1735 |
+ } |
|
| 1736 |
+ .frame(height: trackHeight) |
|
| 1737 |
+ |
|
| 1738 |
+ HStack {
|
|
| 1739 |
+ Text(boundaryLabel(for: availableTimeRange.lowerBound)) |
|
| 1740 |
+ Spacer(minLength: 0) |
|
| 1741 |
+ Text(boundaryLabel(for: availableTimeRange.upperBound)) |
|
| 1742 |
+ } |
|
| 1743 |
+ .font(boundaryFont) |
|
| 1744 |
+ .foregroundColor(.secondary) |
|
| 1745 |
+ .monospacedDigit() |
|
| 1746 |
+ } |
|
| 1747 |
+ } |
|
| 1748 |
+ |
|
| 1749 |
+ private func handleView(height: CGFloat) -> some View {
|
|
| 1750 |
+ Capsule(style: .continuous) |
|
| 1751 |
+ .fill(Color.white.opacity(0.95)) |
|
| 1752 |
+ .frame(width: 6, height: height) |
|
| 1753 |
+ .shadow(color: .black.opacity(0.08), radius: 2, x: 0, y: 1) |
|
| 1754 |
+ } |
|
| 1755 |
+ |
|
| 1756 |
+ private func alignmentButton( |
|
| 1757 |
+ systemName: String, |
|
| 1758 |
+ isActive: Bool, |
|
| 1759 |
+ action: @escaping () -> Void, |
|
| 1760 |
+ accessibilityLabel: String |
|
| 1761 |
+ ) -> some View {
|
|
| 1762 |
+ Button(action: action) {
|
|
| 1763 |
+ Image(systemName: systemName) |
|
| 1764 |
+ .font(.system(size: compactLayout ? 12 : 13, weight: .semibold)) |
|
| 1765 |
+ .frame(width: symbolButtonSize, height: symbolButtonSize) |
|
| 1766 |
+ } |
|
| 1767 |
+ .buttonStyle(.plain) |
|
| 1768 |
+ .foregroundColor(isActive ? .white : accentColor) |
|
| 1769 |
+ .background( |
|
| 1770 |
+ RoundedRectangle(cornerRadius: 9, style: .continuous) |
|
| 1771 |
+ .fill(isActive ? accentColor : accentColor.opacity(0.14)) |
|
| 1772 |
+ ) |
|
| 1773 |
+ .overlay( |
|
| 1774 |
+ RoundedRectangle(cornerRadius: 9, style: .continuous) |
|
| 1775 |
+ .stroke(accentColor.opacity(0.28), lineWidth: 1) |
|
| 1776 |
+ ) |
|
| 1777 |
+ .accessibilityLabel(accessibilityLabel) |
|
| 1778 |
+ } |
|
| 1779 |
+ |
|
| 1780 |
+ private func trackingModeToggleButton() -> some View {
|
|
| 1781 |
+ Button {
|
|
| 1782 |
+ presentTrackingMode = presentTrackingMode == .keepDuration |
|
| 1783 |
+ ? .keepStartTimestamp |
|
| 1784 |
+ : .keepDuration |
|
| 1785 |
+ } label: {
|
|
| 1786 |
+ Image(systemName: trackingModeSymbolName) |
|
| 1787 |
+ .font(.system(size: compactLayout ? 12 : 13, weight: .semibold)) |
|
| 1788 |
+ .frame(width: symbolButtonSize, height: symbolButtonSize) |
|
| 1789 |
+ } |
|
| 1790 |
+ .buttonStyle(.plain) |
|
| 1791 |
+ .foregroundColor(.white) |
|
| 1792 |
+ .background( |
|
| 1793 |
+ RoundedRectangle(cornerRadius: 9, style: .continuous) |
|
| 1794 |
+ .fill(accentColor) |
|
| 1795 |
+ ) |
|
| 1796 |
+ .overlay( |
|
| 1797 |
+ RoundedRectangle(cornerRadius: 9, style: .continuous) |
|
| 1798 |
+ .stroke(accentColor.opacity(0.28), lineWidth: 1) |
|
| 1799 |
+ ) |
|
| 1800 |
+ .accessibilityLabel(trackingModeAccessibilityLabel) |
|
| 1801 |
+ .accessibilityHint("Toggles how the interval follows the present")
|
|
| 1802 |
+ } |
|
| 1803 |
+ |
|
| 1804 |
+ private var trackingModeSymbolName: String {
|
|
| 1805 |
+ switch presentTrackingMode {
|
|
| 1806 |
+ case .keepDuration: |
|
| 1807 |
+ return "arrow.left.and.right" |
|
| 1808 |
+ case .keepStartTimestamp: |
|
| 1809 |
+ return "arrow.left.to.line.compact" |
|
| 1810 |
+ } |
|
| 1811 |
+ } |
|
| 1812 |
+ |
|
| 1813 |
+ private var trackingModeAccessibilityLabel: String {
|
|
| 1814 |
+ switch presentTrackingMode {
|
|
| 1815 |
+ case .keepDuration: |
|
| 1816 |
+ return "Follow present keeping span" |
|
| 1817 |
+ case .keepStartTimestamp: |
|
| 1818 |
+ return "Follow present keeping start" |
|
| 1819 |
+ } |
|
| 1820 |
+ } |
|
| 1821 |
+ |
|
| 1822 |
+ private func alignSelectionToLeadingEdge() {
|
|
| 1823 |
+ let alignedRange = normalizedSelectionRange( |
|
| 1824 |
+ availableTimeRange.lowerBound...currentRange.upperBound |
|
| 1825 |
+ ) |
|
| 1826 |
+ applySelection(alignedRange, pinToPresent: false) |
|
| 1827 |
+ } |
|
| 1828 |
+ |
|
| 1829 |
+ private func alignSelectionToTrailingEdge() {
|
|
| 1830 |
+ let alignedRange = normalizedSelectionRange( |
|
| 1831 |
+ currentRange.lowerBound...availableTimeRange.upperBound |
|
| 1832 |
+ ) |
|
| 1833 |
+ applySelection(alignedRange, pinToPresent: true) |
|
| 1834 |
+ } |
|
| 1835 |
+ |
|
| 1836 |
+ private func selectionGesture(totalWidth: CGFloat) -> some Gesture {
|
|
| 1837 |
+ DragGesture(minimumDistance: 0) |
|
| 1838 |
+ .onChanged { value in
|
|
| 1839 |
+ updateSelectionDrag(value: value, totalWidth: totalWidth) |
|
| 1840 |
+ } |
|
| 1841 |
+ .onEnded { _ in
|
|
| 1842 |
+ dragState = nil |
|
| 1843 |
+ } |
|
| 1844 |
+ } |
|
| 1845 |
+ |
|
| 1846 |
+ private func updateSelectionDrag( |
|
| 1847 |
+ value: DragGesture.Value, |
|
| 1848 |
+ totalWidth: CGFloat |
|
| 1849 |
+ ) {
|
|
| 1850 |
+ let startingRange = resolvedSelectionRange() |
|
| 1851 |
+ |
|
| 1852 |
+ if dragState == nil {
|
|
| 1853 |
+ dragState = DragState( |
|
| 1854 |
+ target: dragTarget( |
|
| 1855 |
+ for: value.startLocation.x, |
|
| 1856 |
+ selectionFrame: selectionFrame(for: startingRange, width: totalWidth) |
|
| 1857 |
+ ), |
|
| 1858 |
+ initialRange: startingRange |
|
| 1859 |
+ ) |
|
| 1860 |
+ } |
|
| 1861 |
+ |
|
| 1862 |
+ guard let dragState else { return }
|
|
| 1863 |
+ |
|
| 1864 |
+ let resultingRange = snappedToEdges( |
|
| 1865 |
+ adjustedRange( |
|
| 1866 |
+ from: dragState.initialRange, |
|
| 1867 |
+ target: dragState.target, |
|
| 1868 |
+ translationX: value.translation.width, |
|
| 1869 |
+ totalWidth: totalWidth |
|
| 1870 |
+ ), |
|
| 1871 |
+ target: dragState.target, |
|
| 1872 |
+ totalWidth: totalWidth |
|
| 1873 |
+ ) |
|
| 1874 |
+ |
|
| 1875 |
+ applySelection( |
|
| 1876 |
+ resultingRange, |
|
| 1877 |
+ pinToPresent: shouldKeepPresentPin( |
|
| 1878 |
+ during: dragState.target, |
|
| 1879 |
+ initialRange: dragState.initialRange, |
|
| 1880 |
+ resultingRange: resultingRange |
|
| 1881 |
+ ), |
|
| 1882 |
+ ) |
|
| 1883 |
+ } |
|
| 1884 |
+ |
|
| 1885 |
+ private func dragTarget( |
|
| 1886 |
+ for startX: CGFloat, |
|
| 1887 |
+ selectionFrame: CGRect |
|
| 1888 |
+ ) -> DragTarget {
|
|
| 1889 |
+ let handleZone: CGFloat = compactLayout ? 20 : 24 |
|
| 1890 |
+ |
|
| 1891 |
+ if abs(startX - selectionFrame.minX) <= handleZone {
|
|
| 1892 |
+ return .lowerBound |
|
| 1893 |
+ } |
|
| 1894 |
+ |
|
| 1895 |
+ if abs(startX - selectionFrame.maxX) <= handleZone {
|
|
| 1896 |
+ return .upperBound |
|
| 1897 |
+ } |
|
| 1898 |
+ |
|
| 1899 |
+ if selectionFrame.contains(CGPoint(x: startX, y: selectionFrame.midY)) {
|
|
| 1900 |
+ return .window |
|
| 1901 |
+ } |
|
| 1902 |
+ |
|
| 1903 |
+ return startX < selectionFrame.minX ? .lowerBound : .upperBound |
|
| 1904 |
+ } |
|
| 1905 |
+ |
|
| 1906 |
+ private func adjustedRange( |
|
| 1907 |
+ from initialRange: ClosedRange<Date>, |
|
| 1908 |
+ target: DragTarget, |
|
| 1909 |
+ translationX: CGFloat, |
|
| 1910 |
+ totalWidth: CGFloat |
|
| 1911 |
+ ) -> ClosedRange<Date> {
|
|
| 1912 |
+ guard totalSpan > 0, totalWidth > 0 else {
|
|
| 1913 |
+ return availableTimeRange |
|
| 1914 |
+ } |
|
| 1915 |
+ |
|
| 1916 |
+ let delta = TimeInterval(translationX / totalWidth) * totalSpan |
|
| 1917 |
+ let minimumSpan = min(max(minimumSelectionSpan, 0.1), totalSpan) |
|
| 1918 |
+ |
|
| 1919 |
+ switch target {
|
|
| 1920 |
+ case .lowerBound: |
|
| 1921 |
+ let maximumLowerBound = initialRange.upperBound.addingTimeInterval(-minimumSpan) |
|
| 1922 |
+ let newLowerBound = min( |
|
| 1923 |
+ max(initialRange.lowerBound.addingTimeInterval(delta), availableTimeRange.lowerBound), |
|
| 1924 |
+ maximumLowerBound |
|
| 1925 |
+ ) |
|
| 1926 |
+ return normalizedSelectionRange(newLowerBound...initialRange.upperBound) |
|
| 1927 |
+ |
|
| 1928 |
+ case .upperBound: |
|
| 1929 |
+ let minimumUpperBound = initialRange.lowerBound.addingTimeInterval(minimumSpan) |
|
| 1930 |
+ let newUpperBound = max( |
|
| 1931 |
+ min(initialRange.upperBound.addingTimeInterval(delta), availableTimeRange.upperBound), |
|
| 1932 |
+ minimumUpperBound |
|
| 1933 |
+ ) |
|
| 1934 |
+ return normalizedSelectionRange(initialRange.lowerBound...newUpperBound) |
|
| 1935 |
+ |
|
| 1936 |
+ case .window: |
|
| 1937 |
+ let span = initialRange.upperBound.timeIntervalSince(initialRange.lowerBound) |
|
| 1938 |
+ guard span < totalSpan else { return availableTimeRange }
|
|
| 1939 |
+ |
|
| 1940 |
+ var lowerBound = initialRange.lowerBound.addingTimeInterval(delta) |
|
| 1941 |
+ var upperBound = initialRange.upperBound.addingTimeInterval(delta) |
|
| 1942 |
+ |
|
| 1943 |
+ if lowerBound < availableTimeRange.lowerBound {
|
|
| 1944 |
+ upperBound = upperBound.addingTimeInterval( |
|
| 1945 |
+ availableTimeRange.lowerBound.timeIntervalSince(lowerBound) |
|
| 1946 |
+ ) |
|
| 1947 |
+ lowerBound = availableTimeRange.lowerBound |
|
| 1948 |
+ } |
|
| 1949 |
+ |
|
| 1950 |
+ if upperBound > availableTimeRange.upperBound {
|
|
| 1951 |
+ lowerBound = lowerBound.addingTimeInterval( |
|
| 1952 |
+ -upperBound.timeIntervalSince(availableTimeRange.upperBound) |
|
| 1953 |
+ ) |
|
| 1954 |
+ upperBound = availableTimeRange.upperBound |
|
| 1955 |
+ } |
|
| 1956 |
+ |
|
| 1957 |
+ return normalizedSelectionRange(lowerBound...upperBound) |
|
| 1958 |
+ } |
|
| 1959 |
+ } |
|
| 1960 |
+ |
|
| 1961 |
+ private func snappedToEdges( |
|
| 1962 |
+ _ candidateRange: ClosedRange<Date>, |
|
| 1963 |
+ target: DragTarget, |
|
| 1964 |
+ totalWidth: CGFloat |
|
| 1965 |
+ ) -> ClosedRange<Date> {
|
|
| 1966 |
+ guard totalSpan > 0 else {
|
|
| 1967 |
+ return availableTimeRange |
|
| 1968 |
+ } |
|
| 1969 |
+ |
|
| 1970 |
+ let snapInterval = edgeSnapInterval(for: totalWidth) |
|
| 1971 |
+ let selectionSpan = candidateRange.upperBound.timeIntervalSince(candidateRange.lowerBound) |
|
| 1972 |
+ var lowerBound = candidateRange.lowerBound |
|
| 1973 |
+ var upperBound = candidateRange.upperBound |
|
| 1974 |
+ |
|
| 1975 |
+ if target != .upperBound, |
|
| 1976 |
+ lowerBound.timeIntervalSince(availableTimeRange.lowerBound) <= snapInterval {
|
|
| 1977 |
+ lowerBound = availableTimeRange.lowerBound |
|
| 1978 |
+ if target == .window {
|
|
| 1979 |
+ upperBound = lowerBound.addingTimeInterval(selectionSpan) |
|
| 1980 |
+ } |
|
| 1981 |
+ } |
|
| 1982 |
+ |
|
| 1983 |
+ if target != .lowerBound, |
|
| 1984 |
+ availableTimeRange.upperBound.timeIntervalSince(upperBound) <= snapInterval {
|
|
| 1985 |
+ upperBound = availableTimeRange.upperBound |
|
| 1986 |
+ if target == .window {
|
|
| 1987 |
+ lowerBound = upperBound.addingTimeInterval(-selectionSpan) |
|
| 1988 |
+ } |
|
| 1989 |
+ } |
|
| 1990 |
+ |
|
| 1991 |
+ return normalizedSelectionRange(lowerBound...upperBound) |
|
| 1992 |
+ } |
|
| 1993 |
+ |
|
| 1994 |
+ private func edgeSnapInterval( |
|
| 1995 |
+ for totalWidth: CGFloat |
|
| 1996 |
+ ) -> TimeInterval {
|
|
| 1997 |
+ guard totalWidth > 0 else { return minimumSelectionSpan }
|
|
| 1998 |
+ |
|
| 1999 |
+ let snapWidth = min( |
|
| 2000 |
+ max(compactLayout ? 18 : 22, totalWidth * 0.04), |
|
| 2001 |
+ totalWidth * 0.18 |
|
| 2002 |
+ ) |
|
| 2003 |
+ let intervalFromWidth = TimeInterval(snapWidth / totalWidth) * totalSpan |
|
| 2004 |
+ return min( |
|
| 2005 |
+ max(intervalFromWidth, minimumSelectionSpan * 0.5), |
|
| 2006 |
+ totalSpan / 4 |
|
| 2007 |
+ ) |
|
| 2008 |
+ } |
|
| 2009 |
+ |
|
| 2010 |
+ private func resolvedSelectionRange() -> ClosedRange<Date> {
|
|
| 2011 |
+ guard let selectedTimeRange else { return availableTimeRange }
|
|
| 2012 |
+ |
|
| 2013 |
+ if isPinnedToPresent {
|
|
| 2014 |
+ switch presentTrackingMode {
|
|
| 2015 |
+ case .keepDuration: |
|
| 2016 |
+ let selectionSpan = selectedTimeRange.upperBound.timeIntervalSince(selectedTimeRange.lowerBound) |
|
| 2017 |
+ return normalizedSelectionRange( |
|
| 2018 |
+ availableTimeRange.upperBound.addingTimeInterval(-selectionSpan)...availableTimeRange.upperBound |
|
| 2019 |
+ ) |
|
| 2020 |
+ case .keepStartTimestamp: |
|
| 2021 |
+ return normalizedSelectionRange( |
|
| 2022 |
+ selectedTimeRange.lowerBound...availableTimeRange.upperBound |
|
| 2023 |
+ ) |
|
| 2024 |
+ } |
|
| 2025 |
+ } |
|
| 2026 |
+ |
|
| 2027 |
+ return normalizedSelectionRange(selectedTimeRange) |
|
| 2028 |
+ } |
|
| 2029 |
+ |
|
| 2030 |
+ private func normalizedSelectionRange( |
|
| 2031 |
+ _ candidateRange: ClosedRange<Date> |
|
| 2032 |
+ ) -> ClosedRange<Date> {
|
|
| 2033 |
+ let availableSpan = totalSpan |
|
| 2034 |
+ guard availableSpan > 0 else { return availableTimeRange }
|
|
| 2035 |
+ |
|
| 2036 |
+ let minimumSpan = min(max(minimumSelectionSpan, 0.1), availableSpan) |
|
| 2037 |
+ let requestedSpan = min( |
|
| 2038 |
+ max(candidateRange.upperBound.timeIntervalSince(candidateRange.lowerBound), minimumSpan), |
|
| 2039 |
+ availableSpan |
|
| 2040 |
+ ) |
|
| 2041 |
+ |
|
| 2042 |
+ if requestedSpan >= availableSpan {
|
|
| 2043 |
+ return availableTimeRange |
|
| 2044 |
+ } |
|
| 2045 |
+ |
|
| 2046 |
+ var lowerBound = max(candidateRange.lowerBound, availableTimeRange.lowerBound) |
|
| 2047 |
+ var upperBound = min(candidateRange.upperBound, availableTimeRange.upperBound) |
|
| 2048 |
+ |
|
| 2049 |
+ if upperBound.timeIntervalSince(lowerBound) < requestedSpan {
|
|
| 2050 |
+ if lowerBound == availableTimeRange.lowerBound {
|
|
| 2051 |
+ upperBound = lowerBound.addingTimeInterval(requestedSpan) |
|
| 2052 |
+ } else {
|
|
| 2053 |
+ lowerBound = upperBound.addingTimeInterval(-requestedSpan) |
|
| 2054 |
+ } |
|
| 2055 |
+ } |
|
| 2056 |
+ |
|
| 2057 |
+ if upperBound > availableTimeRange.upperBound {
|
|
| 2058 |
+ let delta = upperBound.timeIntervalSince(availableTimeRange.upperBound) |
|
| 2059 |
+ upperBound = availableTimeRange.upperBound |
|
| 2060 |
+ lowerBound = lowerBound.addingTimeInterval(-delta) |
|
| 2061 |
+ } |
|
| 2062 |
+ |
|
| 2063 |
+ if lowerBound < availableTimeRange.lowerBound {
|
|
| 2064 |
+ let delta = availableTimeRange.lowerBound.timeIntervalSince(lowerBound) |
|
| 2065 |
+ lowerBound = availableTimeRange.lowerBound |
|
| 2066 |
+ upperBound = upperBound.addingTimeInterval(delta) |
|
| 2067 |
+ } |
|
| 2068 |
+ |
|
| 2069 |
+ return lowerBound...upperBound |
|
| 2070 |
+ } |
|
| 2071 |
+ |
|
| 2072 |
+ private func shouldKeepPresentPin( |
|
| 2073 |
+ during target: DragTarget, |
|
| 2074 |
+ initialRange: ClosedRange<Date>, |
|
| 2075 |
+ resultingRange: ClosedRange<Date> |
|
| 2076 |
+ ) -> Bool {
|
|
| 2077 |
+ let startedPinnedToPresent = |
|
| 2078 |
+ isPinnedToPresent || |
|
| 2079 |
+ selectionCoversFullRange(initialRange) |
|
| 2080 |
+ |
|
| 2081 |
+ guard startedPinnedToPresent else {
|
|
| 2082 |
+ return selectionTouchesPresent(resultingRange) |
|
| 2083 |
+ } |
|
| 2084 |
+ |
|
| 2085 |
+ switch target {
|
|
| 2086 |
+ case .lowerBound: |
|
| 2087 |
+ return true |
|
| 2088 |
+ case .upperBound, .window: |
|
| 2089 |
+ return selectionTouchesPresent(resultingRange) |
|
| 2090 |
+ } |
|
| 2091 |
+ } |
|
| 2092 |
+ |
|
| 2093 |
+ private func applySelection( |
|
| 2094 |
+ _ candidateRange: ClosedRange<Date>, |
|
| 2095 |
+ pinToPresent: Bool |
|
| 2096 |
+ ) {
|
|
| 2097 |
+ let normalizedRange = normalizedSelectionRange(candidateRange) |
|
| 2098 |
+ |
|
| 2099 |
+ if selectionCoversFullRange(normalizedRange) && !pinToPresent {
|
|
| 2100 |
+ selectedTimeRange = nil |
|
| 2101 |
+ } else {
|
|
| 2102 |
+ selectedTimeRange = normalizedRange |
|
| 2103 |
+ } |
|
| 2104 |
+ |
|
| 2105 |
+ isPinnedToPresent = pinToPresent |
|
| 2106 |
+ } |
|
| 2107 |
+ |
|
| 2108 |
+ private func selectionTouchesPresent( |
|
| 2109 |
+ _ range: ClosedRange<Date> |
|
| 2110 |
+ ) -> Bool {
|
|
| 2111 |
+ let tolerance = max(0.5, minimumSelectionSpan * 0.25) |
|
| 2112 |
+ return abs(range.upperBound.timeIntervalSince(availableTimeRange.upperBound)) <= tolerance |
|
| 2113 |
+ } |
|
| 2114 |
+ |
|
| 2115 |
+ private func selectionCoversFullRange( |
|
| 2116 |
+ _ range: ClosedRange<Date> |
|
| 2117 |
+ ) -> Bool {
|
|
| 2118 |
+ let tolerance = max(0.5, minimumSelectionSpan * 0.25) |
|
| 2119 |
+ return abs(range.lowerBound.timeIntervalSince(availableTimeRange.lowerBound)) <= tolerance && |
|
| 2120 |
+ abs(range.upperBound.timeIntervalSince(availableTimeRange.upperBound)) <= tolerance |
|
| 2121 |
+ } |
|
| 2122 |
+ |
|
| 2123 |
+ private func selectionFrame(in size: CGSize) -> CGRect {
|
|
| 2124 |
+ selectionFrame(for: currentRange, width: size.width) |
|
| 2125 |
+ } |
|
| 2126 |
+ |
|
| 2127 |
+ private func selectionFrame( |
|
| 2128 |
+ for range: ClosedRange<Date>, |
|
| 2129 |
+ width: CGFloat |
|
| 2130 |
+ ) -> CGRect {
|
|
| 2131 |
+ guard width > 0, totalSpan > 0 else {
|
|
| 2132 |
+ return CGRect(origin: .zero, size: CGSize(width: width, height: trackHeight)) |
|
| 2133 |
+ } |
|
| 2134 |
+ |
|
| 2135 |
+ let minimumX = xPosition(for: range.lowerBound, width: width) |
|
| 2136 |
+ let maximumX = xPosition(for: range.upperBound, width: width) |
|
| 2137 |
+ return CGRect( |
|
| 2138 |
+ x: minimumX, |
|
| 2139 |
+ y: 0, |
|
| 2140 |
+ width: max(maximumX - minimumX, 2), |
|
| 2141 |
+ height: trackHeight |
|
| 2142 |
+ ) |
|
| 2143 |
+ } |
|
| 2144 |
+ |
|
| 2145 |
+ private func xPosition(for date: Date, width: CGFloat) -> CGFloat {
|
|
| 2146 |
+ guard width > 0, totalSpan > 0 else { return 0 }
|
|
| 2147 |
+ |
|
| 2148 |
+ let offset = date.timeIntervalSince(availableTimeRange.lowerBound) |
|
| 2149 |
+ let normalizedOffset = min(max(offset / totalSpan, 0), 1) |
|
| 2150 |
+ return CGFloat(normalizedOffset) * width |
|
| 2151 |
+ } |
|
| 2152 |
+ |
|
| 2153 |
+ private func boundaryLabel(for date: Date) -> String {
|
|
| 2154 |
+ date.format(as: boundaryDateFormat) |
|
| 2155 |
+ } |
|
| 2156 |
+ |
|
| 2157 |
+ private func selectionSummary( |
|
| 2158 |
+ for range: ClosedRange<Date> |
|
| 2159 |
+ ) -> String {
|
|
| 2160 |
+ "\(range.lowerBound.format(as: summaryDateFormat)) - \(range.upperBound.format(as: summaryDateFormat))" |
|
| 2161 |
+ } |
|
| 2162 |
+ |
|
| 2163 |
+ private var boundaryDateFormat: String {
|
|
| 2164 |
+ switch totalSpan {
|
|
| 2165 |
+ case 0..<86400: |
|
| 2166 |
+ return "HH:mm" |
|
| 2167 |
+ case 86400..<604800: |
|
| 2168 |
+ return "MMM d HH:mm" |
|
| 2169 |
+ default: |
|
| 2170 |
+ return "MMM d" |
|
| 2171 |
+ } |
|
| 2172 |
+ } |
|
| 2173 |
+ |
|
| 2174 |
+ private var summaryDateFormat: String {
|
|
| 2175 |
+ switch totalSpan {
|
|
| 2176 |
+ case 0..<3600: |
|
| 2177 |
+ return "HH:mm:ss" |
|
| 2178 |
+ case 3600..<172800: |
|
| 2179 |
+ return "MMM d HH:mm" |
|
| 2180 |
+ default: |
|
| 2181 |
+ return "MMM d" |
|
| 2182 |
+ } |
|
| 2183 |
+ } |
|
| 2184 |
+} |
|
| 2185 |
+ |
|
| 1311 | 2186 |
struct Chart : View {
|
| 1312 | 2187 |
|
| 1313 | 2188 |
let points: [Measurements.Measurement.Point] |
| 1314 | 2189 |
let context: ChartContext |
| 1315 | 2190 |
var areaChart: Bool = false |
| 1316 | 2191 |
var strokeColor: Color = .black |
| 2192 |
+ var areaFillColor: Color? = nil |
|
| 1317 | 2193 |
|
| 1318 | 2194 |
var body : some View {
|
| 1319 | 2195 |
GeometryReader { geometry in
|
| 1320 | 2196 |
if self.areaChart {
|
| 2197 |
+ let fillColor = areaFillColor ?? strokeColor.opacity(0.2) |
|
| 1321 | 2198 |
self.path( geometry: geometry ) |
| 1322 |
- .fill(LinearGradient( gradient: .init(colors: [Color.red, Color.green]), startPoint: .init(x: 0.5, y: 0.1), endPoint: .init(x: 0.5, y: 0.9))) |
|
| 2199 |
+ .fill( |
|
| 2200 |
+ LinearGradient( |
|
| 2201 |
+ gradient: .init( |
|
| 2202 |
+ colors: [ |
|
| 2203 |
+ fillColor.opacity(0.72), |
|
| 2204 |
+ fillColor.opacity(0.18) |
|
| 2205 |
+ ] |
|
| 2206 |
+ ), |
|
| 2207 |
+ startPoint: .init(x: 0.5, y: 0.08), |
|
| 2208 |
+ endPoint: .init(x: 0.5, y: 0.92) |
|
| 2209 |
+ ) |
|
| 2210 |
+ ) |
|
| 1323 | 2211 |
} else {
|
| 1324 | 2212 |
self.path( geometry: geometry ) |
| 1325 | 2213 |
.stroke(self.strokeColor, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round)) |