From c1fdaa031264a5baa5dc47a3755ef8d3c580a4d3 Mon Sep 17 00:00:00 2001 From: mark Date: Mon, 26 Oct 2026 13:08:52 -0700 Subject: [PATCH 1/2] Fix startup crash from forced cast in DeviceStatus parsing lastDeviceStatus?["pump"], ["loop"], and ["openaps"] were force-cast (as!) to [String: AnyObject]?. When Nightscout returns a non-dictionary value for one of these keys (e.g. explicit null), the forced cast throws a Swift runtime fatal error (swift_dynamicCastFailure), crashing the app on the main thread shortly after launch during device status processing. Switched to safe optional casts (as?) so a missing or unexpected value is simply treated as absent. Co-Authored-By: Claude Sonnet 5 --- LoopFollow/Controllers/Nightscout/DeviceStatus.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index 4cd3a65a1..13a0cc4eb 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -92,7 +92,7 @@ extension MainViewController { Observable.shared.previousAlertLastLoopTime.value = Observable.shared.alertLastLoopTime.value - if let lastPumpRecord = lastDeviceStatus?["pump"] as! [String: AnyObject]? { + if let lastPumpRecord = lastDeviceStatus?["pump"] as? [String: AnyObject] { if let bolusIncrement = lastPumpRecord["bolusIncrement"] as? Double, bolusIncrement > 0 { Storage.shared.bolusIncrement.value = HKQuantity(unit: .internationalUnit(), doubleValue: bolusIncrement) Storage.shared.bolusIncrementDetected.value = true @@ -174,7 +174,7 @@ extension MainViewController { } // Loop - handle new data - if let lastLoopRecord = lastDeviceStatus?["loop"] as! [String: AnyObject]? { + if let lastLoopRecord = lastDeviceStatus?["loop"] as? [String: AnyObject] { // Some pumps report no `pump.clock`; without it alertLastLoopTime stays 0 // and the forecast anchors to epoch 0. Fall back to the loop cycle timestamp. if (lastDeviceStatus?["pump"] as? [String: AnyObject])?["clock"] == nil, @@ -215,7 +215,7 @@ extension MainViewController { } // OpenAPS - handle new data - if let lastLoopRecord = lastDeviceStatus?["openaps"] as! [String: AnyObject]? { + if let lastLoopRecord = lastDeviceStatus?["openaps"] as? [String: AnyObject] { DeviceStatusOpenAPS(formatter: formatter, lastDeviceStatus: lastDeviceStatus, lastLoopRecord: lastLoopRecord) } From d1e61959eb17041d9ded241e47e8c44986ea0640 Mon Sep 17 00:00:00 2001 From: mark Date: Mon, 26 Oct 2026 13:41:18 -0700 Subject: [PATCH 2/2] Fix crash from forced cast on treatment timestamp field SMB/Carbs/Bolus/Notes processing and StatsDataFetcher checked currentEntry["timestamp"] != nil before force-casting it to String, but that check passes for an explicit JSON null (NSNull), which is not nil in Swift. Uploaders that always include a "timestamp" key (set to null when unused, falling back to created_at) made every treatment with a null timestamp crash with swift_dynamicCastFailure on the main thread. Replaced with a single safe-cast fallback chain (timestamp as? String ?? created_at as? String) that correctly skips a null/missing timestamp and falls through to created_at instead of crashing. Co-Authored-By: Claude Sonnet 5 --- .../Nightscout/Treatments/Bolus.swift | 7 +------ .../Nightscout/Treatments/Carbs.swift | 7 +------ .../Nightscout/Treatments/Notes.swift | 7 +------ .../Nightscout/Treatments/SMB.swift | 7 +------ LoopFollow/Stats/StatsDataFetcher.swift | 21 +++---------------- 5 files changed, 7 insertions(+), 42 deletions(-) diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift b/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift index 58314a4d9..32e944286 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift @@ -11,12 +11,7 @@ extension MainViewController { var lastFoundIndex = 0 for currentEntry in entries.reversed() { - var bolusDate: String - if currentEntry["timestamp"] != nil { - bolusDate = currentEntry["timestamp"] as! String - } else if currentEntry["created_at"] != nil { - bolusDate = currentEntry["created_at"] as! String - } else { + guard let bolusDate = currentEntry["timestamp"] as? String ?? currentEntry["created_at"] as? String else { continue } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift b/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift index 23e2e0427..c64950763 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift @@ -13,12 +13,7 @@ extension MainViewController { var lastFoundSmb = 0 for currentEntry in entries.reversed() { - var carbDate: String - if currentEntry["timestamp"] != nil { - carbDate = currentEntry["timestamp"] as! String - } else if currentEntry["created_at"] != nil { - carbDate = currentEntry["created_at"] as! String - } else { + guard let carbDate = currentEntry["timestamp"] as? String ?? currentEntry["created_at"] as? String else { continue } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift b/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift index cc4435212..8b1e8af5c 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift @@ -13,12 +13,7 @@ extension MainViewController { for currentEntry in entries.reversed() { guard let currentEntry = currentEntry as? [String: AnyObject] else { continue } - var date: String - if currentEntry["timestamp"] != nil { - date = currentEntry["timestamp"] as! String - } else if currentEntry["created_at"] != nil { - date = currentEntry["created_at"] as! String - } else { + guard let date = currentEntry["timestamp"] as? String ?? currentEntry["created_at"] as? String else { continue } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift b/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift index f896e4941..64730289a 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift @@ -10,12 +10,7 @@ extension MainViewController { var lastFoundIndex = 0 for currentEntry in entries.reversed() { - var bolusDate: String - if currentEntry["timestamp"] != nil { - bolusDate = currentEntry["timestamp"] as! String - } else if currentEntry["created_at"] != nil { - bolusDate = currentEntry["created_at"] as! String - } else { + guard let bolusDate = currentEntry["timestamp"] as? String ?? currentEntry["created_at"] as? String else { continue } diff --git a/LoopFollow/Stats/StatsDataFetcher.swift b/LoopFollow/Stats/StatsDataFetcher.swift index 18353dce0..b841d9fd9 100644 --- a/LoopFollow/Stats/StatsDataFetcher.swift +++ b/LoopFollow/Stats/StatsDataFetcher.swift @@ -201,12 +201,7 @@ class StatsDataFetcher { var lastFoundIndex = 0 for currentEntry in bolusEntries.reversed() { - var bolusDate: String - if currentEntry["timestamp"] != nil { - bolusDate = currentEntry["timestamp"] as! String - } else if currentEntry["created_at"] != nil { - bolusDate = currentEntry["created_at"] as! String - } else { + guard let bolusDate = currentEntry["timestamp"] as? String ?? currentEntry["created_at"] as? String else { continue } @@ -250,12 +245,7 @@ class StatsDataFetcher { var lastFoundIndex = 0 for currentEntry in smbEntries.reversed() { - var bolusDate: String - if currentEntry["timestamp"] != nil { - bolusDate = currentEntry["timestamp"] as! String - } else if currentEntry["created_at"] != nil { - bolusDate = currentEntry["created_at"] as! String - } else { + guard let bolusDate = currentEntry["timestamp"] as? String ?? currentEntry["created_at"] as? String else { continue } @@ -296,12 +286,7 @@ class StatsDataFetcher { var lastFoundBolus = 0 for currentEntry in carbEntries.reversed() { - var carbDate: String - if currentEntry["timestamp"] != nil { - carbDate = currentEntry["timestamp"] as! String - } else if currentEntry["created_at"] != nil { - carbDate = currentEntry["created_at"] as! String - } else { + guard let carbDate = currentEntry["timestamp"] as? String ?? currentEntry["created_at"] as? String else { continue }