| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- import Combine
- import Foundation
- import SwiftUI
- import Swinject
- struct TagCloudView: View {
- var tags: [String]
- var shouldParseToMmolL: Bool
- @Environment(\.colorScheme) var colorScheme
- @State private var totalHeight = CGFloat.zero // << variant for ScrollView/List
- // = CGFloat.infinity // << variant for VStack
- var body: some View {
- VStack {
- GeometryReader { geometry in
- self.generateContent(in: geometry)
- }
- }
- .frame(height: totalHeight) // << variant for ScrollView/List
- // .frame(maxHeight: totalHeight) // << variant for VStack
- }
- private func generateContent(in geometry: GeometryProxy) -> some View {
- var width = CGFloat.zero
- var height = CGFloat.zero
- return ZStack(alignment: .topLeading) {
- ForEach(self.tags, id: \.self) { tag in
- self.drawTag(for: tag, isMmolL: shouldParseToMmolL)
- .padding([.horizontal, .vertical], 3)
- .alignmentGuide(.leading, computeValue: { dimensions in
- if abs(width - dimensions.width) > geometry.size.width {
- width = 0
- height -= dimensions.height
- }
- let result = width
- if tag == self.tags.last! {
- width = 0 // last item
- } else {
- width -= dimensions.width
- }
- return result
- })
- .alignmentGuide(.top, computeValue: { _ in
- let result = height
- if tag == self.tags.last! {
- height = 0 // last item
- }
- return result
- })
- }
- }.background(viewHeightReader($totalHeight))
- }
- private func drawTag(for textTag: String, isMmolL: Bool) -> some View {
- var colorOfTag: Color {
- switch textTag {
- case textTag where textTag.contains("SMB Delivery Ratio:"):
- return .uam
- case textTag where textTag.contains("Bolus"):
- return .green
- case textTag where textTag.contains("TDD:"),
- textTag where textTag.contains("tdd_factor"),
- textTag where textTag.contains("Sigmoid function"),
- textTag where textTag.contains("Logarithmic formula"),
- textTag where textTag.contains("AF:"),
- textTag where textTag.contains("Autosens/Dynamic Limit:"),
- textTag where textTag.contains("Dynamic ISF/CR"),
- textTag where textTag.contains("Basal ratio"),
- textTag where textTag.contains("SMB Ratio"):
- return .zt
- case textTag where textTag.contains("Middleware:"):
- return .red
- case textTag where textTag.contains("SMB Ratio"):
- return .orange
- case textTag where textTag.contains("Smoothing: On"):
- return .gray
- default:
- return .insulin
- }
- }
- let formattedTextTag = formatGlucoseTags(textTag, isMmolL: isMmolL)
- return ZStack {
- Text(formattedTextTag)
- .padding(.horizontal, 10)
- .padding(.vertical, 5)
- .font(.subheadline)
- .fontWeight(.semibold)
- .background(colorOfTag.opacity(colorScheme == .dark ? 0.15 : 0.25))
- .foregroundColor(colorOfTag)
- .clipShape(Capsule())
- .overlay(
- Capsule()
- .stroke(colorOfTag.opacity(0.4), lineWidth: 2)
- )
- }
- }
- /**
- Converts glucose-related values in the given `tag` string to mmol/L, including ranges (e.g., `ISF: 54→54`), comparisons (e.g., `maxDelta 37 > 20% of BG 95`), and both positive and negative values (e.g., `Dev: -36`).
- - Parameters:
- - tag: The string containing glucose-related values to be converted.
- - isMmolL: A Boolean flag indicating whether to convert values to mmol/L.
- - Returns:
- A string with glucose values converted to mmol/L.
- - Glucose tags handled: `ISF:`, `Target:`, `minPredBG`, `minGuardBG`, `IOBpredBG`, `COBpredBG`, `UAMpredBG`, `Dev:`, `maxDelta`, `BGI`.
- */
- // TODO: Consolidate all mmol parsing methods (in TagCloudView, NightscoutManager and HomeRootView) to one central func
- private func formatGlucoseTags(_ tag: String, isMmolL: Bool) -> String {
- let patterns = [
- "ISF:\\s*-?\\d+\\.?\\d*→-?\\d+\\.?\\d*",
- "Dev:\\s*-?\\d+\\.?\\d*",
- "BGI:\\s*-?\\d+\\.?\\d*",
- "Target:\\s*-?\\d+\\.?\\d*",
- "(?:minPredBG|minGuardBG|IOBpredBG|COBpredBG|UAMpredBG)\\s*-?\\d+\\.?\\d*"
- ]
- let pattern = patterns.joined(separator: "|")
- let regex = try! NSRegularExpression(pattern: pattern)
- // Convert only if isMmolL == true; otherwise return original mg/dL string
- func convertToMmolL(_ value: String) -> String {
- if let glucoseValue = Double(value.replacingOccurrences(of: "[^\\d.-]", with: "", options: .regularExpression)) {
- let mmolValue = Decimal(glucoseValue).asMmolL // your mg/dL → mmol/L routine
- return isMmolL ? mmolValue.description : value
- }
- return value
- }
- let matches = regex.matches(in: tag, range: NSRange(tag.startIndex..., in: tag))
- var updatedTag = tag
- // Process each match in reverse order
- for match in matches.reversed() {
- guard let range = Range(match.range, in: tag) else { continue }
- let glucoseValueString = String(tag[range])
- if glucoseValueString.contains("→") {
- // -- Handle ISF: X→Y
- let values = glucoseValueString.components(separatedBy: "→")
- // For example "ISF: 162"
- let firstNumber = values[0].components(separatedBy: ":")[1].trimmingCharacters(in: .whitespaces)
- let secondNumber = values[1].trimmingCharacters(in: .whitespaces)
- let firstValue = convertToMmolL(firstNumber)
- let secondValue = convertToMmolL(secondNumber)
- let formattedString = "ISF: \(firstValue)→\(secondValue)"
- updatedTag.replaceSubrange(range, with: formattedString)
- } else if glucoseValueString.starts(with: "Dev:") {
- // -- Handle Dev
- let value = glucoseValueString.components(separatedBy: ":")[1].trimmingCharacters(in: .whitespaces)
- let formattedValue = convertToMmolL(value)
- let formattedString = "Dev: \(formattedValue)"
- updatedTag.replaceSubrange(range, with: formattedString)
- } else if glucoseValueString.starts(with: "BGI:") {
- // -- Handle BGI
- let value = glucoseValueString.components(separatedBy: ":")[1].trimmingCharacters(in: .whitespaces)
- let formattedValue = convertToMmolL(value)
- let formattedString = "BGI: \(formattedValue)"
- updatedTag.replaceSubrange(range, with: formattedString)
- } else if glucoseValueString.starts(with: "Target:") {
- // -- Handle Target
- let value = glucoseValueString.components(separatedBy: ":")[1].trimmingCharacters(in: .whitespaces)
- let formattedValue = convertToMmolL(value)
- let formattedString = "Target: \(formattedValue)"
- updatedTag.replaceSubrange(range, with: formattedString)
- } else {
- // -- Handle everything else (e.g., "minPredBG 39" etc.)
- let parts = glucoseValueString.components(separatedBy: .whitespaces)
- if parts.count >= 2 {
- let metric = parts[0]
- let value = parts[1]
- let formattedValue = convertToMmolL(value)
- let formattedString = "\(metric): \(formattedValue)"
- updatedTag.replaceSubrange(range, with: formattedString)
- }
- }
- }
- return updatedTag
- }
- private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
- GeometryReader { geometry -> Color in
- let rect = geometry.frame(in: .local)
- DispatchQueue.main.async {
- binding.wrappedValue = rect.size.height
- }
- return .clear
- }
- }
- }
- struct TestTagCloudView: View {
- var body: some View {
- VStack {
- Text("Header").font(.largeTitle)
- TagCloudView(
- tags: ["Ninetendo", "XBox", "PlayStation", "PlayStation 2", "PlayStation 3", "PlayStation 4"],
- shouldParseToMmolL: false
- )
- Text("Some other text")
- Divider()
- Text("Some other cloud")
- TagCloudView(
- tags: ["Apple", "Google", "Amazon", "Microsoft", "Oracle", "Facebook"],
- shouldParseToMmolL: false
- )
- }
- }
- }
|