import SwiftUI struct LoopStatusSheetView: View { @Environment(\.colorScheme) var colorScheme @Environment(AppState.self) var appState var state: Home.StateModel @State var sheetDetent = PresentationDetent.medium @State private var statusTitle: String = "" var body: some View { NavigationStack { VStack(alignment: .leading, spacing: 10) { HStack { Spacer() Text(statusTitle) .font(.headline) .foregroundColor(statusBadgeTextColor) .padding(.horizontal, 12) .padding(.vertical, 6) .background(statusBadgeColor) .clipShape(Capsule()) Spacer() } .padding(.top, 35) .padding(.bottom) if let errorMessage = state.errorMessage, let date = state.errorDate { Group { Text("Error During Algorithm Run at \(Formatter.dateFormatter.string(from: date))").font(.headline) Text(errorMessage).font(.caption) }.foregroundColor(.loopRed) } if let determination = state.determinationsFromPersistence.first { if determination.glucose == 400 { Text("Invalid CGM reading (HIGH).").font(.callout).bold().foregroundColor(.loopRed) .padding(.top, 8) Text("SMBs and High Temps Disabled.").font(.caption).padding(.bottom, 4) } else { Text("Latest Raw Algorithm Output").bold() Text( "Trio is currently using these metrics and values as determined by the oref algorithm:" ) .font(.subheadline) let tags = !state.isSmoothingEnabled ? determination.reasonParts : determination .reasonParts + ["Smoothing: On"] TagCloudView( tags: tags, shouldParseToMmolL: state.units == .mmolL ) .animation(.none, value: false) Divider().padding(.vertical) Text("Current Algorithm Reasoning:").bold() Text( self .parseReasonConclusion( determination.reasonConclusion, isMmolL: state.units == .mmolL ) ).font(.subheadline) } } else { Text("No recent oref algorithm determination.") } Spacer() Button { state.isLoopStatusPresented.toggle() } label: { Text("Got it!") .frame(maxWidth: .infinity, alignment: .center) } .buttonStyle(.bordered) .padding(.top) } .padding(.vertical) .padding(.horizontal, 20) .presentationDetents( [.fraction(0.75), .large], selection: $sheetDetent ) .ignoresSafeArea(edges: .top) .background(appState.trioBackgroundColor(for: colorScheme)) .navigationBarTitle("Current Loop Status", displayMode: .inline) .onAppear { setStatusTitle() } } .scrollContentBackground(.hidden) } private var statusBadgeColor: Color { guard let determination = state.determinationsFromPersistence.first, determination.timestamp != nil else { // previously the .timestamp property was used here because this only gets updated when the reportenacted function in the aps manager gets called return .secondary } let delta = state.timerDate.timeIntervalSince(state.lastLoopDate) - 30 if delta <= 5.minutes.timeInterval { guard determination.timestamp != nil else { return .loopYellow } return .loopGreen } else if delta <= 10.minutes.timeInterval { return .loopYellow } else { return .loopRed } } private var statusBadgeTextColor: Color { statusBadgeColor == .secondary || statusBadgeColor == .loopYellow ? .black : .white } private func setStatusTitle() { if let determination = state.determinationsFromPersistence.first { statusTitle = "Enacted at \(Formatter.dateFormatter.string(from: determination.deliverAt ?? Date()))" } else { statusTitle = "Not enacted." } } // TODO: Consolidate all mmol parsing methods (in TagCloudView, NightscoutManager and HomeRootView) to one central func private func parseReasonConclusion(_ reasonConclusion: String, isMmolL _: Bool) -> String { let patterns = [ "minGuardBG\\s*-?\\d+\\.?\\d*<-?\\d+\\.?\\d*", "Eventual BG\\s*-?\\d+\\.?\\d*\\s*>=\\s*-?\\d+\\.?\\d*", "\\S+\\s+-?\\d+\\.?\\d*\\s*>\\s*\\d+%\\s+of\\s+BG\\s+-?\\d+\\.?\\d*" ] let pattern = patterns.joined(separator: "|") let regex = try! NSRegularExpression(pattern: pattern) func convertToMmolL(_ value: String) -> String { if let glucoseValue = Double(value.replacingOccurrences(of: "[^\\d.-]", with: "", options: .regularExpression)) { let mmolValue = Decimal(glucoseValue).asMmolL return mmolValue.description } return value } let matches = regex.matches( in: reasonConclusion, range: NSRange(reasonConclusion.startIndex..., in: reasonConclusion) ) var updatedConclusion = reasonConclusion for match in matches.reversed() { guard let range = Range(match.range, in: reasonConclusion) else { continue } let matchedString = String(reasonConclusion[range]) if matchedString.contains("<") { // Handle "minGuardBG x=") { // Handle "Eventual BG x >= target" pattern let parts = matchedString.components(separatedBy: " >= ") if parts.count == 2, let firstValue = Double( parts[0] .components(separatedBy: CharacterSet(charactersIn: "0123456789.-").inverted).joined() ), let secondValue = Double( parts[1] .components(separatedBy: CharacterSet(charactersIn: "0123456789.-").inverted).joined() ) { let formattedFirstValue = convertToMmolL(String(firstValue)) let formattedSecondValue = convertToMmolL(String(secondValue)) let formattedString = "Eventual BG \(formattedFirstValue) >= \(formattedSecondValue)" updatedConclusion.replaceSubrange(range, with: formattedString) } } else if matchedString.contains(">") { // Handle "maxDelta 37 > 20% of BG 95" style let pattern = "(\\S+)\\s+(-?\\d+\\.?\\d*)\\s*>\\s*(\\d+)%\\s+of\\s+BG\\s+(-?\\d+\\.?\\d*)" let localRegex = try! NSRegularExpression(pattern: pattern) if let localMatch = localRegex.firstMatch( in: matchedString, range: NSRange(matchedString.startIndex..., in: matchedString) ) { let metric = String(matchedString[Range(localMatch.range(at: 1), in: matchedString)!]) let firstValue = String(matchedString[Range(localMatch.range(at: 2), in: matchedString)!]) let percentage = String(matchedString[Range(localMatch.range(at: 3), in: matchedString)!]) let bgValue = String(matchedString[Range(localMatch.range(at: 4), in: matchedString)!]) let formattedFirstValue = convertToMmolL(firstValue) let formattedBGValue = convertToMmolL(bgValue) let formattedString = "\(metric) \(formattedFirstValue) > \(percentage)% of BG \(formattedBGValue)" updatedConclusion.replaceSubrange(range, with: formattedString) } } } return updatedConclusion.capitalizingFirstLetter() } }