@@ -11,6 +11,7 @@ |
||
| 11 | 11 |
4308CF882417770D0002E80B /* DataGroupsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4308CF872417770D0002E80B /* DataGroupsView.swift */; };
|
| 12 | 12 |
430CB4FC245E07EB006525C2 /* ChevronView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430CB4FB245E07EB006525C2 /* ChevronView.swift */; };
|
| 13 | 13 |
4311E63A241384960080EA59 /* DeviceHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4311E639241384960080EA59 /* DeviceHelpView.swift */; };
|
| 14 |
+ E430FB6B7CB3E0D4189F6D7D /* MeterMappingDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56BA4CE53B6B2C4EBA42C81A /* MeterMappingDebugView.swift */; };
|
|
| 14 | 15 |
4327461B24619CED0009BE4B /* MeterRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4327461A24619CED0009BE4B /* MeterRowView.swift */; };
|
| 15 | 16 |
432EA6442445A559006FC905 /* ChartContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 432EA6432445A559006FC905 /* ChartContext.swift */; };
|
| 16 | 17 |
4347F01D28D717C1007EE7B1 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 4347F01C28D717C1007EE7B1 /* CryptoSwift */; };
|
@@ -18,7 +19,6 @@ |
||
| 18 | 19 |
43554B2F24443939004E66F5 /* MeasurementsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43554B2E24443939004E66F5 /* MeasurementsView.swift */; };
|
| 19 | 20 |
43554B32244449B5004E66F5 /* MeasurementPointView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43554B31244449B5004E66F5 /* MeasurementPointView.swift */; };
|
| 20 | 21 |
43554B3424444B0E004E66F5 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43554B3324444B0E004E66F5 /* Date.swift */; };
|
| 21 |
- 43567FE92443AD7C00000282 /* ICloudDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43567FE82443AD7C00000282 /* ICloudDefault.swift */; };
|
|
| 22 | 22 |
4360A34D241CBB3800B464F9 /* RSSIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4360A34C241CBB3800B464F9 /* RSSIView.swift */; };
|
| 23 | 23 |
4360A34F241D5CF100B464F9 /* MeterSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4360A34E241D5CF100B464F9 /* MeterSettingsView.swift */; };
|
| 24 | 24 |
437D47D12415F91B00B7768E /* LiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437D47D02415F91B00B7768E /* LiveView.swift */; };
|
@@ -29,6 +29,7 @@ |
||
| 29 | 29 |
4383B460240EB2D000DAAEBF /* Meter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4383B45F240EB2D000DAAEBF /* Meter.swift */; };
|
| 30 | 30 |
4383B462240EB5E400DAAEBF /* AppData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4383B461240EB5E400DAAEBF /* AppData.swift */; };
|
| 31 | 31 |
4383B465240EB6B200DAAEBF /* UserDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4383B464240EB6B200DAAEBF /* UserDefault.swift */; };
|
| 32 |
+ 3407A133FADB8858DC2A1FED /* MeterNameStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7396C8BB36F4E7F8E0CD8FF8 /* MeterNameStore.swift */; };
|
|
| 32 | 33 |
4383B468240F845500DAAEBF /* MacAdress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4383B467240F845500DAAEBF /* MacAdress.swift */; };
|
| 33 | 34 |
4383B46A240FE4A600DAAEBF /* MeterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4383B469240FE4A600DAAEBF /* MeterView.swift */; };
|
| 34 | 35 |
438695892463F062008855A9 /* Measurements.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438695882463F062008855A9 /* Measurements.swift */; };
|
@@ -42,7 +43,6 @@ |
||
| 42 | 43 |
439D996524234B98008DE3AA /* BluetoothRadio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439D996424234B98008DE3AA /* BluetoothRadio.swift */; };
|
| 43 | 44 |
43CBF660240BF3EB00255B8B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CBF65F240BF3EB00255B8B /* AppDelegate.swift */; };
|
| 44 | 45 |
43CBF662240BF3EB00255B8B /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CBF661240BF3EB00255B8B /* SceneDelegate.swift */; };
|
| 45 |
- 43CBF665240BF3EB00255B8B /* CKModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 43CBF663240BF3EB00255B8B /* CKModel.xcdatamodeld */; };
|
|
| 46 | 46 |
43CBF667240BF3EB00255B8B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CBF666240BF3EB00255B8B /* ContentView.swift */; };
|
| 47 | 47 |
43CBF669240BF3ED00255B8B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43CBF668240BF3ED00255B8B /* Assets.xcassets */; };
|
| 48 | 48 |
43CBF66C240BF3ED00255B8B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43CBF66B240BF3ED00255B8B /* Preview Assets.xcassets */; };
|
@@ -90,13 +90,13 @@ |
||
| 90 | 90 |
4308CF872417770D0002E80B /* DataGroupsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataGroupsView.swift; sourceTree = "<group>"; };
|
| 91 | 91 |
430CB4FB245E07EB006525C2 /* ChevronView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChevronView.swift; sourceTree = "<group>"; };
|
| 92 | 92 |
4311E639241384960080EA59 /* DeviceHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceHelpView.swift; sourceTree = "<group>"; };
|
| 93 |
+ 56BA4CE53B6B2C4EBA42C81A /* MeterMappingDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterMappingDebugView.swift; sourceTree = "<group>"; };
|
|
| 93 | 94 |
4327461A24619CED0009BE4B /* MeterRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterRowView.swift; sourceTree = "<group>"; };
|
| 94 | 95 |
432EA6432445A559006FC905 /* ChartContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartContext.swift; sourceTree = "<group>"; };
|
| 95 | 96 |
4351E7BA24685ACD00E798A3 /* CGPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGPoint.swift; sourceTree = "<group>"; };
|
| 96 | 97 |
43554B2E24443939004E66F5 /* MeasurementsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeasurementsView.swift; sourceTree = "<group>"; };
|
| 97 | 98 |
43554B31244449B5004E66F5 /* MeasurementPointView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeasurementPointView.swift; sourceTree = "<group>"; };
|
| 98 | 99 |
43554B3324444B0E004E66F5 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = "<group>"; };
|
| 99 |
- 43567FE82443AD7C00000282 /* ICloudDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ICloudDefault.swift; sourceTree = "<group>"; };
|
|
| 100 | 100 |
4360A34C241CBB3800B464F9 /* RSSIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RSSIView.swift; sourceTree = "<group>"; };
|
| 101 | 101 |
4360A34E241D5CF100B464F9 /* MeterSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterSettingsView.swift; sourceTree = "<group>"; };
|
| 102 | 102 |
437D47D02415F91B00B7768E /* LiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveView.swift; sourceTree = "<group>"; };
|
@@ -107,6 +107,7 @@ |
||
| 107 | 107 |
4383B45F240EB2D000DAAEBF /* Meter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Meter.swift; sourceTree = "<group>"; };
|
| 108 | 108 |
4383B461240EB5E400DAAEBF /* AppData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppData.swift; sourceTree = "<group>"; };
|
| 109 | 109 |
4383B464240EB6B200DAAEBF /* UserDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefault.swift; sourceTree = "<group>"; };
|
| 110 |
+ 7396C8BB36F4E7F8E0CD8FF8 /* MeterNameStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterNameStore.swift; sourceTree = "<group>"; };
|
|
| 110 | 111 |
4383B467240F845500DAAEBF /* MacAdress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacAdress.swift; sourceTree = "<group>"; };
|
| 111 | 112 |
4383B469240FE4A600DAAEBF /* MeterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeterView.swift; sourceTree = "<group>"; };
|
| 112 | 113 |
438695882463F062008855A9 /* Measurements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Measurements.swift; sourceTree = "<group>"; };
|
@@ -121,7 +122,6 @@ |
||
| 121 | 122 |
43CBF65C240BF3EB00255B8B /* USB Meter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "USB Meter.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
| 122 | 123 |
43CBF65F240BF3EB00255B8B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
| 123 | 124 |
43CBF661240BF3EB00255B8B /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
| 124 |
- 43CBF664240BF3EB00255B8B /* USB_Meter.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = USB_Meter.xcdatamodel; sourceTree = "<group>"; };
|
|
| 125 | 125 |
43CBF666240BF3EB00255B8B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
| 126 | 126 |
43CBF668240BF3ED00255B8B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
| 127 | 127 |
43CBF66B240BF3ED00255B8B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
@@ -295,7 +295,6 @@ |
||
| 295 | 295 |
isa = PBXGroup; |
| 296 | 296 |
children = ( |
| 297 | 297 |
4383B464240EB6B200DAAEBF /* UserDefault.swift */, |
| 298 |
- 43567FE82443AD7C00000282 /* ICloudDefault.swift */, |
|
| 299 | 298 |
); |
| 300 | 299 |
path = Templates; |
| 301 | 300 |
sourceTree = "<group>"; |
@@ -357,6 +356,7 @@ |
||
| 357 | 356 |
isa = PBXGroup; |
| 358 | 357 |
children = ( |
| 359 | 358 |
4383B461240EB5E400DAAEBF /* AppData.swift */, |
| 359 |
+ 7396C8BB36F4E7F8E0CD8FF8 /* MeterNameStore.swift */, |
|
| 360 | 360 |
43CBF676240C043E00255B8B /* BluetoothManager.swift */, |
| 361 | 361 |
4383B45F240EB2D000DAAEBF /* Meter.swift */, |
| 362 | 362 |
43ED78AD2420A0BE00974487 /* BluetoothSerial.swift */, |
@@ -364,7 +364,6 @@ |
||
| 364 | 364 |
4386958E2F6A4E3E008855A9 /* MeterCapabilities.swift */, |
| 365 | 365 |
4386958A2F6A1001008855A9 /* UMProtocol.swift */, |
| 366 | 366 |
4386958C2F6A1002008855A9 /* TC66Protocol.swift */, |
| 367 |
- 43CBF663240BF3EB00255B8B /* CKModel.xcdatamodeld */, |
|
| 368 | 367 |
438695882463F062008855A9 /* Measurements.swift */, |
| 369 | 368 |
432EA6432445A559006FC905 /* ChartContext.swift */, |
| 370 | 369 |
); |
@@ -375,6 +374,7 @@ |
||
| 375 | 374 |
isa = PBXGroup; |
| 376 | 375 |
children = ( |
| 377 | 376 |
43CBF666240BF3EB00255B8B /* ContentView.swift */, |
| 377 |
+ 56BA4CE53B6B2C4EBA42C81A /* MeterMappingDebugView.swift */, |
|
| 378 | 378 |
4327461A24619CED0009BE4B /* MeterRowView.swift */, |
| 379 | 379 |
437D47CF2415F8CF00B7768E /* Meter */, |
| 380 | 380 |
4311E639241384960080EA59 /* DeviceHelpView.swift */, |
@@ -435,6 +435,11 @@ |
||
| 435 | 435 |
TargetAttributes = {
|
| 436 | 436 |
43CBF65B240BF3EB00255B8B = {
|
| 437 | 437 |
CreatedOnToolsVersion = 11.3.1; |
| 438 |
+ SystemCapabilities = {
|
|
| 439 |
+ com.apple.iCloud = {
|
|
| 440 |
+ enabled = 1; |
|
| 441 |
+ }; |
|
| 442 |
+ }; |
|
| 438 | 443 |
}; |
| 439 | 444 |
}; |
| 440 | 445 |
}; |
@@ -480,15 +485,14 @@ |
||
| 480 | 485 |
43874C852415611200525397 /* Double.swift in Sources */, |
| 481 | 486 |
437D47D72415FDF300B7768E /* ControlView.swift in Sources */, |
| 482 | 487 |
4308CF882417770D0002E80B /* DataGroupsView.swift in Sources */, |
| 483 |
- 43567FE92443AD7C00000282 /* ICloudDefault.swift in Sources */, |
|
| 484 | 488 |
4383B468240F845500DAAEBF /* MacAdress.swift in Sources */, |
| 485 | 489 |
43CBF681240D153000255B8B /* CBManagerState.swift in Sources */, |
| 486 | 490 |
4383B46A240FE4A600DAAEBF /* MeterView.swift in Sources */, |
| 487 | 491 |
4360A34D241CBB3800B464F9 /* RSSIView.swift in Sources */, |
| 488 | 492 |
437D47D12415F91B00B7768E /* LiveView.swift in Sources */, |
| 489 |
- 43CBF665240BF3EB00255B8B /* CKModel.xcdatamodeld in Sources */, |
|
| 490 | 493 |
4360A34F241D5CF100B464F9 /* MeterSettingsView.swift in Sources */, |
| 491 | 494 |
4383B465240EB6B200DAAEBF /* UserDefault.swift in Sources */, |
| 495 |
+ 3407A133FADB8858DC2A1FED /* MeterNameStore.swift in Sources */, |
|
| 492 | 496 |
43CBF677240C043E00255B8B /* BluetoothManager.swift in Sources */, |
| 493 | 497 |
43CBF660240BF3EB00255B8B /* AppDelegate.swift in Sources */, |
| 494 | 498 |
438B9555246D2D7500E61AE7 /* Path.swift in Sources */, |
@@ -514,6 +518,7 @@ |
||
| 514 | 518 |
430CB4FC245E07EB006525C2 /* ChevronView.swift in Sources */, |
| 515 | 519 |
43554B3424444B0E004E66F5 /* Date.swift in Sources */, |
| 516 | 520 |
4311E63A241384960080EA59 /* DeviceHelpView.swift in Sources */, |
| 521 |
+ E430FB6B7CB3E0D4189F6D7D /* MeterMappingDebugView.swift in Sources */, |
|
| 517 | 522 |
439D996524234B98008DE3AA /* BluetoothRadio.swift in Sources */, |
| 518 | 523 |
438695892463F062008855A9 /* Measurements.swift in Sources */, |
| 519 | 524 |
4386958B2F6A1001008855A9 /* UMProtocol.swift in Sources */, |
@@ -747,18 +752,6 @@ |
||
| 747 | 752 |
}; |
| 748 | 753 |
/* End XCSwiftPackageProductDependency section */ |
| 749 | 754 |
|
| 750 |
-/* Begin XCVersionGroup section */ |
|
| 751 |
- 43CBF663240BF3EB00255B8B /* CKModel.xcdatamodeld */ = {
|
|
| 752 |
- isa = XCVersionGroup; |
|
| 753 |
- children = ( |
|
| 754 |
- 43CBF664240BF3EB00255B8B /* USB_Meter.xcdatamodel */, |
|
| 755 |
- ); |
|
| 756 |
- currentVersion = 43CBF664240BF3EB00255B8B /* USB_Meter.xcdatamodel */; |
|
| 757 |
- path = CKModel.xcdatamodeld; |
|
| 758 |
- sourceTree = "<group>"; |
|
| 759 |
- versionGroupType = wrapper.xcdatamodel; |
|
| 760 |
- }; |
|
| 761 |
-/* End XCVersionGroup section */ |
|
| 762 | 755 |
}; |
| 763 | 756 |
rootObject = 43CBF654240BF3EB00255B8B /* Project object */; |
| 764 | 757 |
} |
@@ -7,7 +7,6 @@ |
||
| 7 | 7 |
// |
| 8 | 8 |
|
| 9 | 9 |
import UIKit |
| 10 |
-import CoreData |
|
| 11 | 10 |
|
| 12 | 11 |
//let btSerial = BluetoothSerial(delegate: BSD()) |
| 13 | 12 |
let appData = AppData() |
@@ -32,10 +31,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||
| 32 | 31 |
|
| 33 | 32 |
|
| 34 | 33 |
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
| 35 |
- // Override point for customization after application launch. |
|
| 34 |
+ logRuntimeICloudDiagnostics() |
|
| 36 | 35 |
return true |
| 37 | 36 |
} |
| 38 | 37 |
|
| 38 |
+ private func logRuntimeICloudDiagnostics() {
|
|
| 39 |
+ #if DEBUG |
|
| 40 |
+ let hasUbiquityIdentityToken = FileManager.default.ubiquityIdentityToken != nil |
|
| 41 |
+ track("Runtime iCloud diagnostics: ubiquityIdentityTokenAvailable=\(hasUbiquityIdentityToken)")
|
|
| 42 |
+ #endif |
|
| 43 |
+ } |
|
| 44 |
+ |
|
| 39 | 45 |
// MARK: UISceneSession Lifecycle |
| 40 | 46 |
|
| 41 | 47 |
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
@@ -49,51 +55,4 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||
| 49 | 55 |
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. |
| 50 | 56 |
// Use this method to release any resources that were specific to the discarded scenes, as they will not return. |
| 51 | 57 |
} |
| 52 |
- |
|
| 53 |
- // MARK: - Core Data stack |
|
| 54 |
- |
|
| 55 |
- lazy var persistentContainer: NSPersistentCloudKitContainer = {
|
|
| 56 |
- /* |
|
| 57 |
- The persistent container for the application. This implementation |
|
| 58 |
- creates and returns a container, having loaded the store for the |
|
| 59 |
- application to it. This property is optional since there are legitimate |
|
| 60 |
- error conditions that could cause the creation of the store to fail. |
|
| 61 |
- */ |
|
| 62 |
- let container = NSPersistentCloudKitContainer(name: "CKModel") |
|
| 63 |
- container.loadPersistentStores(completionHandler: { (storeDescription, error) in
|
|
| 64 |
- if let error = error as NSError? {
|
|
| 65 |
- // Replace this implementation with code to handle the error appropriately. |
|
| 66 |
- // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. |
|
| 67 |
- |
|
| 68 |
- /* |
|
| 69 |
- Typical reasons for an error here include: |
|
| 70 |
- * The parent directory does not exist, cannot be created, or disallows writing. |
|
| 71 |
- * The persistent store is not accessible, due to permissions or data protection when the device is locked. |
|
| 72 |
- * The device is out of space. |
|
| 73 |
- * The store could not be migrated to the current model version. |
|
| 74 |
- Check the error message to determine what the actual problem was. |
|
| 75 |
- */ |
|
| 76 |
- fatalError("Unresolved error \(error), \(error.userInfo)")
|
|
| 77 |
- } |
|
| 78 |
- }) |
|
| 79 |
- return container |
|
| 80 |
- }() |
|
| 81 |
- |
|
| 82 |
- // MARK: - Core Data Saving support |
|
| 83 |
- |
|
| 84 |
- func saveContext () {
|
|
| 85 |
- let context = persistentContainer.viewContext |
|
| 86 |
- if context.hasChanges {
|
|
| 87 |
- do {
|
|
| 88 |
- try context.save() |
|
| 89 |
- } catch {
|
|
| 90 |
- // Replace this implementation with code to handle the error appropriately. |
|
| 91 |
- // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. |
|
| 92 |
- let nserror = error as NSError |
|
| 93 |
- fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
|
|
| 94 |
- } |
|
| 95 |
- } |
|
| 96 |
- } |
|
| 97 |
- |
|
| 98 | 58 |
} |
| 99 |
- |
|
@@ -11,56 +11,52 @@ import Combine |
||
| 11 | 11 |
import CoreBluetooth |
| 12 | 12 |
|
| 13 | 13 |
final class AppData : ObservableObject {
|
| 14 |
- private var icloudGefaultsNotification: AnyCancellable? |
|
| 15 | 14 |
private var bluetoothManagerNotification: AnyCancellable? |
| 15 |
+ private var meterStoreObserver: AnyCancellable? |
|
| 16 |
+ private var meterStoreCloudObserver: AnyCancellable? |
|
| 17 |
+ private let meterStore = MeterNameStore.shared |
|
| 16 | 18 |
|
| 17 | 19 |
init() {
|
| 18 |
- icloudGefaultsNotification = NotificationCenter.default.publisher(for: NSUbiquitousKeyValueStore.didChangeExternallyNotification).sink(receiveValue: test) |
|
| 19 | 20 |
bluetoothManagerNotification = bluetoothManager.objectWillChange.sink { [weak self] _ in
|
| 20 | 21 |
self?.scheduleObjectWillChange() |
| 21 | 22 |
} |
| 22 |
- //NotificationCenter.default.publisher(for: NSUbiquitousKeyValueStore.didChangeExternallyNotification).sink(receiveValue: { notification in
|
|
| 23 |
- |
|
| 23 |
+ meterStoreObserver = NotificationCenter.default.publisher(for: .meterNameStoreDidChange) |
|
| 24 |
+ .receive(on: DispatchQueue.main) |
|
| 25 |
+ .sink { [weak self] _ in
|
|
| 26 |
+ self?.refreshMeterMetadata() |
|
| 27 |
+ } |
|
| 28 |
+ meterStoreCloudObserver = NotificationCenter.default.publisher(for: .meterNameStoreCloudStatusDidChange) |
|
| 29 |
+ .receive(on: DispatchQueue.main) |
|
| 30 |
+ .sink { [weak self] _ in
|
|
| 31 |
+ self?.scheduleObjectWillChange() |
|
| 32 |
+ } |
|
| 24 | 33 |
} |
| 25 |
- |
|
| 34 |
+ |
|
| 26 | 35 |
let bluetoothManager = BluetoothManager() |
| 27 |
- |
|
| 36 |
+ |
|
| 28 | 37 |
@Published var enableRecordFeature: Bool = true |
| 29 |
- |
|
| 38 |
+ |
|
| 30 | 39 |
@Published var meters: [UUID:Meter] = [UUID:Meter]() |
| 31 |
- |
|
| 32 |
- @ICloudDefault(key: "MeterNames", defaultValue: [:]) var meterNames: [String:String] |
|
| 33 |
- @ICloudDefault(key: "TC66TemperatureUnits", defaultValue: [:]) var tc66TemperatureUnits: [String:String] |
|
| 34 |
- func test(notification: NotificationCenter.Publisher.Output) -> Void {
|
|
| 35 |
- if let changedKeys = notification.userInfo?["NSUbiquitousKeyValueStoreChangedKeysKey"] as? [String] {
|
|
| 36 |
- var somethingChanged = false |
|
| 37 |
- for changedKey in changedKeys {
|
|
| 38 |
- switch changedKey {
|
|
| 39 |
- case "MeterNames": |
|
| 40 |
- for meter in self.meters.values {
|
|
| 41 |
- if let newName = self.meterNames[meter.btSerial.macAddress.description] {
|
|
| 42 |
- if meter.name != newName {
|
|
| 43 |
- meter.name = newName |
|
| 44 |
- somethingChanged = true |
|
| 45 |
- } |
|
| 46 |
- } |
|
| 47 |
- } |
|
| 48 |
- case "TC66TemperatureUnits": |
|
| 49 |
- for meter in self.meters.values where meter.supportsManualTemperatureUnitSelection {
|
|
| 50 |
- meter.reloadTemperatureUnitPreference() |
|
| 51 |
- somethingChanged = true |
|
| 52 |
- } |
|
| 53 |
- default: |
|
| 54 |
- track("Unknown key: '\(changedKey)' changed in iCloud)")
|
|
| 55 |
- } |
|
| 56 |
- if changedKey == "MeterNames" {
|
|
| 57 |
- |
|
| 58 |
- } |
|
| 59 |
- } |
|
| 60 |
- if somethingChanged {
|
|
| 61 |
- scheduleObjectWillChange() |
|
| 62 |
- } |
|
| 63 |
- } |
|
| 40 |
+ |
|
| 41 |
+ var cloudAvailability: MeterNameStore.CloudAvailability {
|
|
| 42 |
+ meterStore.currentCloudAvailability |
|
| 43 |
+ } |
|
| 44 |
+ |
|
| 45 |
+ func meterName(for macAddress: String) -> String? {
|
|
| 46 |
+ meterStore.name(for: macAddress) |
|
| 47 |
+ } |
|
| 48 |
+ |
|
| 49 |
+ func setMeterName(_ name: String, for macAddress: String) {
|
|
| 50 |
+ meterStore.upsert(macAddress: macAddress, name: name, temperatureUnitRawValue: nil) |
|
| 51 |
+ } |
|
| 52 |
+ |
|
| 53 |
+ func temperatureUnitPreference(for macAddress: String) -> TemperatureUnitPreference {
|
|
| 54 |
+ let rawValue = meterStore.temperatureUnitRawValue(for: macAddress) ?? TemperatureUnitPreference.celsius.rawValue |
|
| 55 |
+ return TemperatureUnitPreference(rawValue: rawValue) ?? .celsius |
|
| 56 |
+ } |
|
| 57 |
+ |
|
| 58 |
+ func setTemperatureUnitPreference(_ preference: TemperatureUnitPreference, for macAddress: String) {
|
|
| 59 |
+ meterStore.upsert(macAddress: macAddress, name: nil, temperatureUnitRawValue: preference.rawValue) |
|
| 64 | 60 |
} |
| 65 | 61 |
|
| 66 | 62 |
private func scheduleObjectWillChange() {
|
@@ -68,4 +64,29 @@ final class AppData : ObservableObject {
|
||
| 68 | 64 |
self?.objectWillChange.send() |
| 69 | 65 |
} |
| 70 | 66 |
} |
| 67 |
+ |
|
| 68 |
+ private func refreshMeterMetadata() {
|
|
| 69 |
+ DispatchQueue.main.async { [weak self] in
|
|
| 70 |
+ guard let self else { return }
|
|
| 71 |
+ var didUpdateAnyMeter = false |
|
| 72 |
+ for meter in self.meters.values {
|
|
| 73 |
+ let mac = meter.btSerial.macAddress.description |
|
| 74 |
+ let displayName = self.meterName(for: mac) ?? mac |
|
| 75 |
+ if meter.name != displayName {
|
|
| 76 |
+ meter.updateNameFromStore(displayName) |
|
| 77 |
+ didUpdateAnyMeter = true |
|
| 78 |
+ } |
|
| 79 |
+ |
|
| 80 |
+ let previousTemperaturePreference = meter.tc66TemperatureUnitPreference |
|
| 81 |
+ meter.reloadTemperatureUnitPreference() |
|
| 82 |
+ if meter.tc66TemperatureUnitPreference != previousTemperaturePreference {
|
|
| 83 |
+ didUpdateAnyMeter = true |
|
| 84 |
+ } |
|
| 85 |
+ } |
|
| 86 |
+ |
|
| 87 |
+ if didUpdateAnyMeter {
|
|
| 88 |
+ self.scheduleObjectWillChange() |
|
| 89 |
+ } |
|
| 90 |
+ } |
|
| 91 |
+ } |
|
| 71 | 92 |
} |
@@ -71,6 +71,10 @@ class BluetoothManager : NSObject, ObservableObject {
|
||
| 71 | 71 |
} else if let meter = appData.meters[peripheral.identifier] {
|
| 72 | 72 |
meter.lastSeen = Date() |
| 73 | 73 |
meter.btSerial.updateRSSI(RSSI.intValue) |
| 74 |
+ let macAddress = meter.btSerial.macAddress.description |
|
| 75 |
+ if meter.name == macAddress, let syncedName = appData.meterName(for: macAddress), syncedName != macAddress {
|
|
| 76 |
+ meter.updateNameFromStore(syncedName) |
|
| 77 |
+ } |
|
| 74 | 78 |
if peripheral.delegate == nil {
|
| 75 | 79 |
peripheral.delegate = meter.btSerial |
| 76 | 80 |
} |
@@ -1,8 +0,0 @@ |
||
| 1 |
-<?xml version="1.0" encoding="UTF-8"?> |
|
| 2 |
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
|
| 3 |
-<plist version="1.0"> |
|
| 4 |
-<dict> |
|
| 5 |
- <key>_XCCurrentVersionName</key> |
|
| 6 |
- <string>USB_Meter.xcdatamodel</string> |
|
| 7 |
-</dict> |
|
| 8 |
-</plist> |
|
@@ -1,7 +0,0 @@ |
||
| 1 |
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?> |
|
| 2 |
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19E287" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier=""> |
|
| 3 |
- <entity name="Entity" representedClassName="Entity" syncable="YES" codeGenerationType="class"/> |
|
| 4 |
- <elements> |
|
| 5 |
- <element name="Entity" positionX="-63" positionY="-18" width="128" height="43"/> |
|
| 6 |
- </elements> |
|
| 7 |
-</model> |
|
@@ -170,9 +170,13 @@ class Meter : NSObject, ObservableObject, Identifiable {
|
||
| 170 | 170 |
var model: Model |
| 171 | 171 |
var modelString: String |
| 172 | 172 |
|
| 173 |
- var name: String {
|
|
| 173 |
+ private var isSyncingNameFromStore = false |
|
| 174 |
+ |
|
| 175 |
+ @Published var name: String {
|
|
| 174 | 176 |
didSet {
|
| 175 |
- appData.meterNames[btSerial.macAddress.description] = name |
|
| 177 |
+ guard !isSyncingNameFromStore else { return }
|
|
| 178 |
+ guard oldValue != name else { return }
|
|
| 179 |
+ appData.setMeterName(name, for: btSerial.macAddress.description) |
|
| 176 | 180 |
} |
| 177 | 181 |
} |
| 178 | 182 |
|
@@ -441,9 +445,7 @@ class Meter : NSObject, ObservableObject, Identifiable {
|
||
| 441 | 445 |
didSet {
|
| 442 | 446 |
guard supportsManualTemperatureUnitSelection else { return }
|
| 443 | 447 |
guard oldValue != tc66TemperatureUnitPreference else { return }
|
| 444 |
- var settings = appData.tc66TemperatureUnits |
|
| 445 |
- settings[btSerial.macAddress.description] = tc66TemperatureUnitPreference.rawValue |
|
| 446 |
- appData.tc66TemperatureUnits = settings |
|
| 448 |
+ appData.setTemperatureUnitPreference(tc66TemperatureUnitPreference, for: btSerial.macAddress.description) |
|
| 447 | 449 |
} |
| 448 | 450 |
} |
| 449 | 451 |
|
@@ -544,7 +546,7 @@ class Meter : NSObject, ObservableObject, Identifiable {
|
||
| 544 | 546 |
modelString = serialPort.peripheral.name! |
| 545 | 547 |
self.model = model |
| 546 | 548 |
btSerial = serialPort |
| 547 |
- name = appData.meterNames[serialPort.macAddress.description] ?? serialPort.macAddress.description |
|
| 549 |
+ name = appData.meterName(for: serialPort.macAddress.description) ?? serialPort.macAddress.description |
|
| 548 | 550 |
super.init() |
| 549 | 551 |
btSerial.delegate = self |
| 550 | 552 |
reloadTemperatureUnitPreference() |
@@ -556,13 +558,19 @@ class Meter : NSObject, ObservableObject, Identifiable {
|
||
| 556 | 558 |
|
| 557 | 559 |
func reloadTemperatureUnitPreference() {
|
| 558 | 560 |
guard supportsManualTemperatureUnitSelection else { return }
|
| 559 |
- let rawValue = appData.tc66TemperatureUnits[btSerial.macAddress.description] ?? TemperatureUnitPreference.celsius.rawValue |
|
| 560 |
- let persistedPreference = TemperatureUnitPreference(rawValue: rawValue) ?? .celsius |
|
| 561 |
+ let persistedPreference = appData.temperatureUnitPreference(for: btSerial.macAddress.description) |
|
| 561 | 562 |
if tc66TemperatureUnitPreference != persistedPreference {
|
| 562 | 563 |
tc66TemperatureUnitPreference = persistedPreference |
| 563 | 564 |
} |
| 564 | 565 |
} |
| 565 | 566 |
|
| 567 |
+ func updateNameFromStore(_ newName: String) {
|
|
| 568 |
+ guard newName != name else { return }
|
|
| 569 |
+ isSyncingNameFromStore = true |
|
| 570 |
+ name = newName |
|
| 571 |
+ isSyncingNameFromStore = false |
|
| 572 |
+ } |
|
| 573 |
+ |
|
| 566 | 574 |
private func cancelPendingDataDumpRequest(reason: String) {
|
| 567 | 575 |
guard let pendingDataDumpWorkItem else { return }
|
| 568 | 576 |
track("\(name) - Cancel scheduled data request (\(reason))")
|
@@ -0,0 +1,325 @@ |
||
| 1 |
+// MeterNameStore.swift |
|
| 2 |
+// USB Meter |
|
| 3 |
+// |
|
| 4 |
+// Created by Codex on 2026. |
|
| 5 |
+// |
|
| 6 |
+ |
|
| 7 |
+import Foundation |
|
| 8 |
+ |
|
| 9 |
+final class MeterNameStore {
|
|
| 10 |
+ struct Record: Identifiable {
|
|
| 11 |
+ let macAddress: String |
|
| 12 |
+ let customName: String? |
|
| 13 |
+ let temperatureUnit: String? |
|
| 14 |
+ |
|
| 15 |
+ var id: String {
|
|
| 16 |
+ macAddress |
|
| 17 |
+ } |
|
| 18 |
+ } |
|
| 19 |
+ |
|
| 20 |
+ enum CloudAvailability: Equatable {
|
|
| 21 |
+ case unknown |
|
| 22 |
+ case available |
|
| 23 |
+ case noAccount |
|
| 24 |
+ case error(String) |
|
| 25 |
+ |
|
| 26 |
+ var helpTitle: String {
|
|
| 27 |
+ switch self {
|
|
| 28 |
+ case .unknown: |
|
| 29 |
+ return "Cloud Sync Status Unknown" |
|
| 30 |
+ case .available: |
|
| 31 |
+ return "Cloud Sync Ready" |
|
| 32 |
+ case .noAccount: |
|
| 33 |
+ return "Enable iCloud Drive" |
|
| 34 |
+ case .error: |
|
| 35 |
+ return "Cloud Sync Error" |
|
| 36 |
+ } |
|
| 37 |
+ } |
|
| 38 |
+ |
|
| 39 |
+ var helpMessage: String {
|
|
| 40 |
+ switch self {
|
|
| 41 |
+ case .unknown: |
|
| 42 |
+ return "The app is still checking whether iCloud sync is available on this device." |
|
| 43 |
+ case .available: |
|
| 44 |
+ return "iCloud sync is available for meter names and TC66 temperature preferences." |
|
| 45 |
+ case .noAccount: |
|
| 46 |
+ return "Meter names and TC66 temperature preferences sync through iCloud Drive. The app keeps a local copy too, but cross-device sync stays off until iCloud Drive is available." |
|
| 47 |
+ case .error(let description): |
|
| 48 |
+ return "The app keeps local values, but iCloud sync reported an error: \(description)" |
|
| 49 |
+ } |
|
| 50 |
+ } |
|
| 51 |
+ } |
|
| 52 |
+ |
|
| 53 |
+ static let shared = MeterNameStore() |
|
| 54 |
+ |
|
| 55 |
+ private enum Keys {
|
|
| 56 |
+ static let localMeterNames = "MeterNameStore.localMeterNames" |
|
| 57 |
+ static let localTemperatureUnits = "MeterNameStore.localTemperatureUnits" |
|
| 58 |
+ static let cloudMeterNames = "MeterNameStore.cloudMeterNames" |
|
| 59 |
+ static let cloudTemperatureUnits = "MeterNameStore.cloudTemperatureUnits" |
|
| 60 |
+ } |
|
| 61 |
+ |
|
| 62 |
+ private let defaults = UserDefaults.standard |
|
| 63 |
+ private let ubiquitousStore = NSUbiquitousKeyValueStore.default |
|
| 64 |
+ private let workQueue = DispatchQueue(label: "MeterNameStore.Queue") |
|
| 65 |
+ private var cloudAvailability: CloudAvailability = .unknown |
|
| 66 |
+ private var ubiquitousObserver: NSObjectProtocol? |
|
| 67 |
+ private var ubiquityIdentityObserver: NSObjectProtocol? |
|
| 68 |
+ |
|
| 69 |
+ private init() {
|
|
| 70 |
+ ubiquitousObserver = NotificationCenter.default.addObserver( |
|
| 71 |
+ forName: NSUbiquitousKeyValueStore.didChangeExternallyNotification, |
|
| 72 |
+ object: ubiquitousStore, |
|
| 73 |
+ queue: nil |
|
| 74 |
+ ) { [weak self] notification in
|
|
| 75 |
+ self?.handleUbiquitousStoreChange(notification) |
|
| 76 |
+ } |
|
| 77 |
+ |
|
| 78 |
+ ubiquityIdentityObserver = NotificationCenter.default.addObserver( |
|
| 79 |
+ forName: NSNotification.Name.NSUbiquityIdentityDidChange, |
|
| 80 |
+ object: nil, |
|
| 81 |
+ queue: nil |
|
| 82 |
+ ) { [weak self] _ in
|
|
| 83 |
+ self?.refreshCloudAvailability(reason: "identity-changed") |
|
| 84 |
+ self?.syncLocalValuesToCloudIfPossible(reason: "identity-changed") |
|
| 85 |
+ } |
|
| 86 |
+ |
|
| 87 |
+ refreshCloudAvailability(reason: "startup") |
|
| 88 |
+ ubiquitousStore.synchronize() |
|
| 89 |
+ syncLocalValuesToCloudIfPossible(reason: "startup") |
|
| 90 |
+ } |
|
| 91 |
+ |
|
| 92 |
+ var currentCloudAvailability: CloudAvailability {
|
|
| 93 |
+ workQueue.sync {
|
|
| 94 |
+ cloudAvailability |
|
| 95 |
+ } |
|
| 96 |
+ } |
|
| 97 |
+ |
|
| 98 |
+ func name(for macAddress: String) -> String? {
|
|
| 99 |
+ let normalizedMAC = normalizedMACAddress(macAddress) |
|
| 100 |
+ guard !normalizedMAC.isEmpty else { return nil }
|
|
| 101 |
+ return mergedDictionary(localKey: Keys.localMeterNames, cloudKey: Keys.cloudMeterNames)[normalizedMAC] |
|
| 102 |
+ } |
|
| 103 |
+ |
|
| 104 |
+ func temperatureUnitRawValue(for macAddress: String) -> String? {
|
|
| 105 |
+ let normalizedMAC = normalizedMACAddress(macAddress) |
|
| 106 |
+ guard !normalizedMAC.isEmpty else { return nil }
|
|
| 107 |
+ return mergedDictionary(localKey: Keys.localTemperatureUnits, cloudKey: Keys.cloudTemperatureUnits)[normalizedMAC] |
|
| 108 |
+ } |
|
| 109 |
+ |
|
| 110 |
+ func upsert(macAddress: String, name: String?, temperatureUnitRawValue: String?) {
|
|
| 111 |
+ let normalizedMAC = normalizedMACAddress(macAddress) |
|
| 112 |
+ guard !normalizedMAC.isEmpty else {
|
|
| 113 |
+ track("MeterNameStore ignored upsert with invalid MAC '\(macAddress)'")
|
|
| 114 |
+ return |
|
| 115 |
+ } |
|
| 116 |
+ |
|
| 117 |
+ var didChange = false |
|
| 118 |
+ |
|
| 119 |
+ if let name {
|
|
| 120 |
+ didChange = updateDictionaryValue( |
|
| 121 |
+ for: normalizedMAC, |
|
| 122 |
+ value: normalizedName(name), |
|
| 123 |
+ localKey: Keys.localMeterNames, |
|
| 124 |
+ cloudKey: Keys.cloudMeterNames |
|
| 125 |
+ ) || didChange |
|
| 126 |
+ } |
|
| 127 |
+ |
|
| 128 |
+ if let temperatureUnitRawValue {
|
|
| 129 |
+ didChange = updateDictionaryValue( |
|
| 130 |
+ for: normalizedMAC, |
|
| 131 |
+ value: normalizedTemperatureUnit(temperatureUnitRawValue), |
|
| 132 |
+ localKey: Keys.localTemperatureUnits, |
|
| 133 |
+ cloudKey: Keys.cloudTemperatureUnits |
|
| 134 |
+ ) || didChange |
|
| 135 |
+ } |
|
| 136 |
+ |
|
| 137 |
+ if didChange {
|
|
| 138 |
+ notifyChange() |
|
| 139 |
+ } |
|
| 140 |
+ } |
|
| 141 |
+ |
|
| 142 |
+ func allRecords() -> [Record] {
|
|
| 143 |
+ let names = mergedDictionary(localKey: Keys.localMeterNames, cloudKey: Keys.cloudMeterNames) |
|
| 144 |
+ let temperatureUnits = mergedDictionary(localKey: Keys.localTemperatureUnits, cloudKey: Keys.cloudTemperatureUnits) |
|
| 145 |
+ let macAddresses = Set(names.keys).union(temperatureUnits.keys) |
|
| 146 |
+ |
|
| 147 |
+ return macAddresses.sorted().map { macAddress in
|
|
| 148 |
+ Record( |
|
| 149 |
+ macAddress: macAddress, |
|
| 150 |
+ customName: names[macAddress], |
|
| 151 |
+ temperatureUnit: temperatureUnits[macAddress] |
|
| 152 |
+ ) |
|
| 153 |
+ } |
|
| 154 |
+ } |
|
| 155 |
+ |
|
| 156 |
+ private func normalizedMACAddress(_ macAddress: String) -> String {
|
|
| 157 |
+ macAddress.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() |
|
| 158 |
+ } |
|
| 159 |
+ |
|
| 160 |
+ private func normalizedName(_ name: String?) -> String? {
|
|
| 161 |
+ guard let trimmed = name?.trimmingCharacters(in: .whitespacesAndNewlines), |
|
| 162 |
+ !trimmed.isEmpty else {
|
|
| 163 |
+ return nil |
|
| 164 |
+ } |
|
| 165 |
+ return trimmed |
|
| 166 |
+ } |
|
| 167 |
+ |
|
| 168 |
+ private func normalizedTemperatureUnit(_ value: String?) -> String? {
|
|
| 169 |
+ guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines), |
|
| 170 |
+ !trimmed.isEmpty else {
|
|
| 171 |
+ return nil |
|
| 172 |
+ } |
|
| 173 |
+ return trimmed |
|
| 174 |
+ } |
|
| 175 |
+ |
|
| 176 |
+ private func dictionary(for key: String, store: KeyValueReading) -> [String: String] {
|
|
| 177 |
+ (store.object(forKey: key) as? [String: String]) ?? [:] |
|
| 178 |
+ } |
|
| 179 |
+ |
|
| 180 |
+ private func mergedDictionary(localKey: String, cloudKey: String) -> [String: String] {
|
|
| 181 |
+ let localValues = dictionary(for: localKey, store: defaults) |
|
| 182 |
+ let cloudValues = dictionary(for: cloudKey, store: ubiquitousStore) |
|
| 183 |
+ return localValues.merging(cloudValues) { _, cloudValue in
|
|
| 184 |
+ cloudValue |
|
| 185 |
+ } |
|
| 186 |
+ } |
|
| 187 |
+ |
|
| 188 |
+ @discardableResult |
|
| 189 |
+ private func updateDictionaryValue( |
|
| 190 |
+ for macAddress: String, |
|
| 191 |
+ value: String?, |
|
| 192 |
+ localKey: String, |
|
| 193 |
+ cloudKey: String |
|
| 194 |
+ ) -> Bool {
|
|
| 195 |
+ var localValues = dictionary(for: localKey, store: defaults) |
|
| 196 |
+ let didChangeLocal = setDictionaryValue(&localValues, for: macAddress, value: value) |
|
| 197 |
+ if didChangeLocal {
|
|
| 198 |
+ defaults.set(localValues, forKey: localKey) |
|
| 199 |
+ } |
|
| 200 |
+ |
|
| 201 |
+ var didChangeCloud = false |
|
| 202 |
+ if isICloudDriveAvailable {
|
|
| 203 |
+ var cloudValues = dictionary(for: cloudKey, store: ubiquitousStore) |
|
| 204 |
+ didChangeCloud = setDictionaryValue(&cloudValues, for: macAddress, value: value) |
|
| 205 |
+ if didChangeCloud {
|
|
| 206 |
+ ubiquitousStore.set(cloudValues, forKey: cloudKey) |
|
| 207 |
+ ubiquitousStore.synchronize() |
|
| 208 |
+ } |
|
| 209 |
+ } |
|
| 210 |
+ |
|
| 211 |
+ return didChangeLocal || didChangeCloud |
|
| 212 |
+ } |
|
| 213 |
+ |
|
| 214 |
+ @discardableResult |
|
| 215 |
+ private func setDictionaryValue( |
|
| 216 |
+ _ dictionary: inout [String: String], |
|
| 217 |
+ for macAddress: String, |
|
| 218 |
+ value: String? |
|
| 219 |
+ ) -> Bool {
|
|
| 220 |
+ let currentValue = dictionary[macAddress] |
|
| 221 |
+ guard currentValue != value else { return false }
|
|
| 222 |
+ if let value {
|
|
| 223 |
+ dictionary[macAddress] = value |
|
| 224 |
+ } else {
|
|
| 225 |
+ dictionary.removeValue(forKey: macAddress) |
|
| 226 |
+ } |
|
| 227 |
+ return true |
|
| 228 |
+ } |
|
| 229 |
+ |
|
| 230 |
+ private var isICloudDriveAvailable: Bool {
|
|
| 231 |
+ FileManager.default.ubiquityIdentityToken != nil |
|
| 232 |
+ } |
|
| 233 |
+ |
|
| 234 |
+ private func refreshCloudAvailability(reason: String) {
|
|
| 235 |
+ let newAvailability: CloudAvailability = isICloudDriveAvailable ? .available : .noAccount |
|
| 236 |
+ |
|
| 237 |
+ var shouldNotify = false |
|
| 238 |
+ workQueue.sync {
|
|
| 239 |
+ guard cloudAvailability != newAvailability else { return }
|
|
| 240 |
+ cloudAvailability = newAvailability |
|
| 241 |
+ shouldNotify = true |
|
| 242 |
+ } |
|
| 243 |
+ |
|
| 244 |
+ guard shouldNotify else { return }
|
|
| 245 |
+ track("MeterNameStore iCloud availability (\(reason)): \(newAvailability)")
|
|
| 246 |
+ DispatchQueue.main.async {
|
|
| 247 |
+ NotificationCenter.default.post(name: .meterNameStoreCloudStatusDidChange, object: nil) |
|
| 248 |
+ } |
|
| 249 |
+ } |
|
| 250 |
+ |
|
| 251 |
+ private func handleUbiquitousStoreChange(_ notification: Notification) {
|
|
| 252 |
+ refreshCloudAvailability(reason: "ubiquitous-store-change") |
|
| 253 |
+ if let changedKeys = notification.userInfo?[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String], !changedKeys.isEmpty {
|
|
| 254 |
+ track("MeterNameStore received ubiquitous changes for keys: \(changedKeys.joined(separator: ", "))")
|
|
| 255 |
+ } |
|
| 256 |
+ notifyChange() |
|
| 257 |
+ } |
|
| 258 |
+ |
|
| 259 |
+ private func syncLocalValuesToCloudIfPossible(reason: String) {
|
|
| 260 |
+ guard isICloudDriveAvailable else {
|
|
| 261 |
+ refreshCloudAvailability(reason: reason) |
|
| 262 |
+ return |
|
| 263 |
+ } |
|
| 264 |
+ |
|
| 265 |
+ let localNames = dictionary(for: Keys.localMeterNames, store: defaults) |
|
| 266 |
+ let localTemperatureUnits = dictionary(for: Keys.localTemperatureUnits, store: defaults) |
|
| 267 |
+ |
|
| 268 |
+ var cloudNames = dictionary(for: Keys.cloudMeterNames, store: ubiquitousStore) |
|
| 269 |
+ var cloudTemperatureUnits = dictionary(for: Keys.cloudTemperatureUnits, store: ubiquitousStore) |
|
| 270 |
+ |
|
| 271 |
+ let mergedNames = cloudNames.merging(localNames) { cloudValue, _ in
|
|
| 272 |
+ cloudValue |
|
| 273 |
+ } |
|
| 274 |
+ let mergedTemperatureUnits = cloudTemperatureUnits.merging(localTemperatureUnits) { cloudValue, _ in
|
|
| 275 |
+ cloudValue |
|
| 276 |
+ } |
|
| 277 |
+ |
|
| 278 |
+ var didChange = false |
|
| 279 |
+ if cloudNames != mergedNames {
|
|
| 280 |
+ cloudNames = mergedNames |
|
| 281 |
+ ubiquitousStore.set(cloudNames, forKey: Keys.cloudMeterNames) |
|
| 282 |
+ didChange = true |
|
| 283 |
+ } |
|
| 284 |
+ if cloudTemperatureUnits != mergedTemperatureUnits {
|
|
| 285 |
+ cloudTemperatureUnits = mergedTemperatureUnits |
|
| 286 |
+ ubiquitousStore.set(cloudTemperatureUnits, forKey: Keys.cloudTemperatureUnits) |
|
| 287 |
+ didChange = true |
|
| 288 |
+ } |
|
| 289 |
+ |
|
| 290 |
+ refreshCloudAvailability(reason: reason) |
|
| 291 |
+ |
|
| 292 |
+ if didChange {
|
|
| 293 |
+ ubiquitousStore.synchronize() |
|
| 294 |
+ track("MeterNameStore pushed local fallback values into iCloud KVS (\(reason)).")
|
|
| 295 |
+ notifyChange() |
|
| 296 |
+ } |
|
| 297 |
+ } |
|
| 298 |
+ |
|
| 299 |
+ private func notifyChange() {
|
|
| 300 |
+ DispatchQueue.main.async {
|
|
| 301 |
+ NotificationCenter.default.post(name: .meterNameStoreDidChange, object: nil) |
|
| 302 |
+ } |
|
| 303 |
+ } |
|
| 304 |
+ |
|
| 305 |
+ deinit {
|
|
| 306 |
+ if let observer = ubiquitousObserver {
|
|
| 307 |
+ NotificationCenter.default.removeObserver(observer) |
|
| 308 |
+ } |
|
| 309 |
+ if let observer = ubiquityIdentityObserver {
|
|
| 310 |
+ NotificationCenter.default.removeObserver(observer) |
|
| 311 |
+ } |
|
| 312 |
+ } |
|
| 313 |
+} |
|
| 314 |
+ |
|
| 315 |
+private protocol KeyValueReading {
|
|
| 316 |
+ func object(forKey defaultName: String) -> Any? |
|
| 317 |
+} |
|
| 318 |
+ |
|
| 319 |
+extension UserDefaults: KeyValueReading {}
|
|
| 320 |
+extension NSUbiquitousKeyValueStore: KeyValueReading {}
|
|
| 321 |
+ |
|
| 322 |
+extension Notification.Name {
|
|
| 323 |
+ static let meterNameStoreDidChange = Notification.Name("MeterNameStoreDidChange")
|
|
| 324 |
+ static let meterNameStoreCloudStatusDidChange = Notification.Name("MeterNameStoreCloudStatusDidChange")
|
|
| 325 |
+} |
|
@@ -20,13 +20,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||
| 20 | 20 |
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene. |
| 21 | 21 |
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). |
| 22 | 22 |
|
| 23 |
- // Get the managed object context from the shared persistent container. |
|
| 24 |
- let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext |
|
| 25 |
- |
|
| 26 |
- // Create the SwiftUI view and set the context as the value for the managedObjectContext environment keyPath. |
|
| 27 |
- // Add `@Environment(\.managedObjectContext)` in the views that will need the context. |
|
| 28 | 23 |
let contentView = ContentView() |
| 29 |
- .environment(\.managedObjectContext, context) |
|
| 30 | 24 |
.environmentObject(appData) |
| 31 | 25 |
|
| 32 | 26 |
// Use a UIHostingController as window root view controller. |
@@ -65,10 +59,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||
| 65 | 59 |
// Use this method to save data, release shared resources, and store enough scene-specific state information |
| 66 | 60 |
// to restore the scene back to its current state. |
| 67 | 61 |
|
| 68 |
- // Save changes in the application's managed object context when the application transitions to the background. |
|
| 69 |
- (UIApplication.shared.delegate as? AppDelegate)?.saveContext() |
|
| 70 | 62 |
} |
| 71 | 63 |
|
| 72 | 64 |
|
| 73 | 65 |
} |
| 74 |
- |
|
@@ -1,29 +0,0 @@ |
||
| 1 |
-// |
|
| 2 |
-// ICloudDefault.swift |
|
| 3 |
-// USB Meter |
|
| 4 |
-// |
|
| 5 |
-// Created by Bogdan Timofte on 12/04/2020. |
|
| 6 |
-// Copyright © 2020 Bogdan Timofte. All rights reserved. |
|
| 7 |
-// |
|
| 8 |
- |
|
| 9 |
-import Foundation |
|
| 10 |
-// https://github.com/lobodpav/Xcode11.4Issues/blob/master/Sources/Xcode11.4Test/CloudListener.swift |
|
| 11 |
-// https://medium.com/@craiggrummitt/boss-level-property-wrappers-and-user-defaults-6a28c7527cf |
|
| 12 |
-@propertyWrapper struct ICloudDefault<T> {
|
|
| 13 |
- let key: String |
|
| 14 |
- let defaultValue: T |
|
| 15 |
- |
|
| 16 |
- var wrappedValue: T {
|
|
| 17 |
- get {
|
|
| 18 |
- return NSUbiquitousKeyValueStore.default.object(forKey: key) as? T ?? defaultValue |
|
| 19 |
- } |
|
| 20 |
- set {
|
|
| 21 |
- NSUbiquitousKeyValueStore.default.set(newValue, forKey: key) |
|
| 22 |
- /* MARK: Sincronizarea forțată |
|
| 23 |
- Face ca sincronizarea intre dispozitive mai repidă dar există o limita de update-uri catre iloud |
|
| 24 |
- */ |
|
| 25 |
- NSUbiquitousKeyValueStore.default.synchronize() |
|
| 26 |
- track("Pushed into iCloud value: '\(newValue)' for key: '\(key)'")
|
|
| 27 |
- } |
|
| 28 |
- } |
|
| 29 |
-} |
|
@@ -2,8 +2,6 @@ |
||
| 2 | 2 |
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
| 3 | 3 |
<plist version="1.0"> |
| 4 | 4 |
<dict> |
| 5 |
- <key>com.apple.developer.icloud-container-identifiers</key> |
|
| 6 |
- <array/> |
|
| 7 | 5 |
<key>com.apple.developer.ubiquity-kvstore-identifier</key> |
| 8 | 6 |
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string> |
| 9 | 7 |
</dict> |
@@ -14,12 +14,15 @@ import Combine |
||
| 14 | 14 |
struct ContentView: View {
|
| 15 | 15 |
private enum HelpAutoReason: String {
|
| 16 | 16 |
case bluetoothPermission |
| 17 |
+ case cloudSyncUnavailable |
|
| 17 | 18 |
case noDevicesDetected |
| 18 | 19 |
|
| 19 | 20 |
var tint: Color {
|
| 20 | 21 |
switch self {
|
| 21 | 22 |
case .bluetoothPermission: |
| 22 | 23 |
return .orange |
| 24 |
+ case .cloudSyncUnavailable: |
|
| 25 |
+ return .indigo |
|
| 23 | 26 |
case .noDevicesDetected: |
| 24 | 27 |
return .yellow |
| 25 | 28 |
} |
@@ -29,6 +32,8 @@ struct ContentView: View {
|
||
| 29 | 32 |
switch self {
|
| 30 | 33 |
case .bluetoothPermission: |
| 31 | 34 |
return "bolt.horizontal.circle.fill" |
| 35 |
+ case .cloudSyncUnavailable: |
|
| 36 |
+ return "icloud.slash.fill" |
|
| 32 | 37 |
case .noDevicesDetected: |
| 33 | 38 |
return "magnifyingglass.circle.fill" |
| 34 | 39 |
} |
@@ -38,25 +43,28 @@ struct ContentView: View {
|
||
| 38 | 43 |
switch self {
|
| 39 | 44 |
case .bluetoothPermission: |
| 40 | 45 |
return "Required" |
| 46 |
+ case .cloudSyncUnavailable: |
|
| 47 |
+ return "Sync Off" |
|
| 41 | 48 |
case .noDevicesDetected: |
| 42 | 49 |
return "Suggested" |
| 43 | 50 |
} |
| 44 | 51 |
} |
| 45 | 52 |
} |
| 46 |
- |
|
| 53 |
+ |
|
| 47 | 54 |
@EnvironmentObject private var appData: AppData |
| 48 | 55 |
@State private var isHelpExpanded = false |
| 49 | 56 |
@State private var dismissedAutoHelpReason: HelpAutoReason? |
| 50 | 57 |
@State private var now = Date() |
| 51 | 58 |
private let helpRefreshTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() |
| 52 | 59 |
private let noDevicesHelpDelay: TimeInterval = 12 |
| 53 |
- |
|
| 60 |
+ |
|
| 54 | 61 |
var body: some View {
|
| 55 | 62 |
NavigationView {
|
| 56 | 63 |
ScrollView {
|
| 57 | 64 |
VStack(alignment: .leading, spacing: 18) {
|
| 58 | 65 |
headerCard |
| 59 | 66 |
helpSection |
| 67 |
+ debugLink |
|
| 60 | 68 |
devicesSection |
| 61 | 69 |
} |
| 62 | 70 |
.padding() |
@@ -159,6 +167,18 @@ struct ContentView: View {
|
||
| 159 | 167 |
helpNoticeCard(for: activeHelpAutoReason) |
| 160 | 168 |
} |
| 161 | 169 |
|
| 170 |
+ if activeHelpAutoReason == .cloudSyncUnavailable {
|
|
| 171 |
+ Button(action: openSettings) {
|
|
| 172 |
+ sidebarLinkCard( |
|
| 173 |
+ title: "Open Settings", |
|
| 174 |
+ subtitle: "Check Apple ID, iCloud Drive, and any restrictions affecting Cloud sync.", |
|
| 175 |
+ symbol: "gearshape.fill", |
|
| 176 |
+ tint: .indigo |
|
| 177 |
+ ) |
|
| 178 |
+ } |
|
| 179 |
+ .buttonStyle(.plain) |
|
| 180 |
+ } |
|
| 181 |
+ |
|
| 162 | 182 |
NavigationLink(destination: appData.bluetoothManager.managerState.helpView) {
|
| 163 | 183 |
sidebarLinkCard( |
| 164 | 184 |
title: "Bluetooth", |
@@ -219,6 +239,18 @@ struct ContentView: View {
|
||
| 219 | 239 |
} |
| 220 | 240 |
} |
| 221 | 241 |
|
| 242 |
+ private var debugLink: some View {
|
|
| 243 |
+ NavigationLink(destination: MeterMappingDebugView()) {
|
|
| 244 |
+ sidebarLinkCard( |
|
| 245 |
+ title: "Meter Mapping Debug", |
|
| 246 |
+ subtitle: "Inspect the MAC ↔ name/TC66 table as seen by this device.", |
|
| 247 |
+ symbol: "list.bullet.rectangle", |
|
| 248 |
+ tint: .purple |
|
| 249 |
+ ) |
|
| 250 |
+ } |
|
| 251 |
+ .buttonStyle(.plain) |
|
| 252 |
+ } |
|
| 253 |
+ |
|
| 222 | 254 |
private var discoveredMeters: [Meter] {
|
| 223 | 255 |
Array(appData.meters.values).sorted { lhs, rhs in
|
| 224 | 256 |
lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending |
@@ -259,12 +291,24 @@ struct ContentView: View {
|
||
| 259 | 291 |
if appData.bluetoothManager.managerState == .unauthorized {
|
| 260 | 292 |
return .bluetoothPermission |
| 261 | 293 |
} |
| 294 |
+ if shouldPromptForCloudSync {
|
|
| 295 |
+ return .cloudSyncUnavailable |
|
| 296 |
+ } |
|
| 262 | 297 |
if hasWaitedLongEnoughForDevices {
|
| 263 | 298 |
return .noDevicesDetected |
| 264 | 299 |
} |
| 265 | 300 |
return nil |
| 266 | 301 |
} |
| 267 | 302 |
|
| 303 |
+ private var shouldPromptForCloudSync: Bool {
|
|
| 304 |
+ switch appData.cloudAvailability {
|
|
| 305 |
+ case .noAccount, .error: |
|
| 306 |
+ return true |
|
| 307 |
+ case .unknown, .available: |
|
| 308 |
+ return false |
|
| 309 |
+ } |
|
| 310 |
+ } |
|
| 311 |
+ |
|
| 268 | 312 |
private var hasWaitedLongEnoughForDevices: Bool {
|
| 269 | 313 |
guard appData.bluetoothManager.managerState == .poweredOn else {
|
| 270 | 314 |
return false |
@@ -310,6 +354,8 @@ struct ContentView: View {
|
||
| 310 | 354 |
switch activeHelpAutoReason {
|
| 311 | 355 |
case .bluetoothPermission: |
| 312 | 356 |
return "Bluetooth permission is needed before scanning can begin." |
| 357 |
+ case .cloudSyncUnavailable: |
|
| 358 |
+ return appData.cloudAvailability.helpMessage |
|
| 313 | 359 |
case .noDevicesDetected: |
| 314 | 360 |
return "No supported devices were found after \(Int(noDevicesHelpDelay)) seconds." |
| 315 | 361 |
case nil: |
@@ -328,6 +374,13 @@ struct ContentView: View {
|
||
| 328 | 374 |
} |
| 329 | 375 |
} |
| 330 | 376 |
|
| 377 |
+ private func openSettings() {
|
|
| 378 |
+ guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else {
|
|
| 379 |
+ return |
|
| 380 |
+ } |
|
| 381 |
+ UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil) |
|
| 382 |
+ } |
|
| 383 |
+ |
|
| 331 | 384 |
private func helpNoticeCard(for reason: HelpAutoReason) -> some View {
|
| 332 | 385 |
VStack(alignment: .leading, spacing: 8) {
|
| 333 | 386 |
Text(helpNoticeTitle(for: reason)) |
@@ -345,6 +398,8 @@ struct ContentView: View {
|
||
| 345 | 398 |
switch reason {
|
| 346 | 399 |
case .bluetoothPermission: |
| 347 | 400 |
return "Bluetooth access needs attention" |
| 401 |
+ case .cloudSyncUnavailable: |
|
| 402 |
+ return appData.cloudAvailability.helpTitle |
|
| 348 | 403 |
case .noDevicesDetected: |
| 349 | 404 |
return "No supported meters found yet" |
| 350 | 405 |
} |
@@ -354,6 +409,8 @@ struct ContentView: View {
|
||
| 354 | 409 |
switch reason {
|
| 355 | 410 |
case .bluetoothPermission: |
| 356 | 411 |
return "Open Bluetooth help to review the permission state and jump into Settings if access is blocked." |
| 412 |
+ case .cloudSyncUnavailable: |
|
| 413 |
+ return appData.cloudAvailability.helpMessage |
|
| 357 | 414 |
case .noDevicesDetected: |
| 358 | 415 |
return "Open Device help for quick checks like meter power, Bluetooth availability on the meter, or an existing connection to another phone." |
| 359 | 416 |
} |
@@ -0,0 +1,60 @@ |
||
| 1 |
+// MeterMappingDebugView.swift |
|
| 2 |
+// USB Meter |
|
| 3 |
+// |
|
| 4 |
+// Created by Codex on 2026. |
|
| 5 |
+// |
|
| 6 |
+ |
|
| 7 |
+import SwiftUI |
|
| 8 |
+ |
|
| 9 |
+struct MeterMappingDebugView: View {
|
|
| 10 |
+ @State private var records: [MeterNameRecord] = [] |
|
| 11 |
+ private let store = MeterNameStore.shared |
|
| 12 |
+ private let changePublisher = NotificationCenter.default.publisher(for: .meterNameStoreDidChange) |
|
| 13 |
+ |
|
| 14 |
+ var body: some View {
|
|
| 15 |
+ List(records) { record in
|
|
| 16 |
+ VStack(alignment: .leading, spacing: 6) {
|
|
| 17 |
+ Text(record.customName) |
|
| 18 |
+ .font(.headline) |
|
| 19 |
+ Text(record.macAddress) |
|
| 20 |
+ .font(.caption.monospaced()) |
|
| 21 |
+ .foregroundColor(.secondary) |
|
| 22 |
+ HStack {
|
|
| 23 |
+ Text("TC66 unit:")
|
|
| 24 |
+ .font(.caption.weight(.semibold)) |
|
| 25 |
+ Text(record.temperatureUnit) |
|
| 26 |
+ .font(.caption.monospaced()) |
|
| 27 |
+ .foregroundColor(.blue) |
|
| 28 |
+ } |
|
| 29 |
+ } |
|
| 30 |
+ .padding(.vertical, 8) |
|
| 31 |
+ } |
|
| 32 |
+ .listStyle(.insetGrouped) |
|
| 33 |
+ .navigationTitle("Meter Name Mapping")
|
|
| 34 |
+ .onAppear(perform: reload) |
|
| 35 |
+ .onReceive(changePublisher) { _ in reload() }
|
|
| 36 |
+ .toolbar {
|
|
| 37 |
+ Button("Refresh") {
|
|
| 38 |
+ reload() |
|
| 39 |
+ } |
|
| 40 |
+ } |
|
| 41 |
+ } |
|
| 42 |
+ |
|
| 43 |
+ private func reload() {
|
|
| 44 |
+ records = store.allRecords().map { record in
|
|
| 45 |
+ MeterNameRecord( |
|
| 46 |
+ id: record.id, |
|
| 47 |
+ macAddress: record.macAddress, |
|
| 48 |
+ customName: record.customName ?? "<unnamed>", |
|
| 49 |
+ temperatureUnit: record.temperatureUnit ?? "n/a" |
|
| 50 |
+ ) |
|
| 51 |
+ } |
|
| 52 |
+ } |
|
| 53 |
+} |
|
| 54 |
+ |
|
| 55 |
+private struct MeterNameRecord: Identifiable {
|
|
| 56 |
+ let id: String |
|
| 57 |
+ let macAddress: String |
|
| 58 |
+ let customName: String |
|
| 59 |
+ let temperatureUnit: String |
|
| 60 |
+} |
|