Ivan Valkou 5 лет назад
Родитель
Сommit
2137a1d3c4

+ 12 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -93,6 +93,8 @@
 		3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3821ED4B25DD18BA00BC42AD /* Constants.swift */; };
 		382C133725F13A1E00715CE1 /* InsulinSensitivities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382C133625F13A1E00715CE1 /* InsulinSensitivities.swift */; };
 		382C134B25F14E3700715CE1 /* BGTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382C134A25F14E3700715CE1 /* BGTargets.swift */; };
+		383420D625FFE38C002D46C1 /* LoopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383420D525FFE38C002D46C1 /* LoopView.swift */; };
+		383420D925FFEB3F002D46C1 /* Popup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383420D825FFEB3F002D46C1 /* Popup.swift */; };
 		383948D325CD4D6D00E91849 /* Disk in Frameworks */ = {isa = PBXBuildFile; productRef = 383948D225CD4D6D00E91849 /* Disk */; };
 		383948D625CD4D8900E91849 /* FileStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383948D525CD4D8900E91849 /* FileStorage.swift */; };
 		383948DA25CD64D500E91849 /* Glucose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383948D925CD64D500E91849 /* Glucose.swift */; };
@@ -157,6 +159,7 @@
 		38A13D3225E28B4B00EAA382 /* PumpHistoryEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A13D3125E28B4B00EAA382 /* PumpHistoryEvent.swift */; };
 		38A504A425DD9C4000C5B9E8 /* UserDefaultsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A5049125DD9C4000C5B9E8 /* UserDefaultsExtensions.swift */; };
 		38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38A9260425F012D8009E3739 /* CarbRatios.swift */; };
+		38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AAF85425FFF846004AF583 /* CurrentGlucoseView.swift */; };
 		38AEE73D25F0200C0013F05B /* FreeAPSSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AEE73C25F0200C0013F05B /* FreeAPSSettings.swift */; };
 		38AEE75225F022080013F05B /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AEE75125F022080013F05B /* SettingsManager.swift */; };
 		38AEE75725F0F18E0013F05B /* CarbsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AEE75625F0F18E0013F05B /* CarbsStorage.swift */; };
@@ -397,6 +400,8 @@
 		3821ED4B25DD18BA00BC42AD /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
 		382C133625F13A1E00715CE1 /* InsulinSensitivities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinSensitivities.swift; sourceTree = "<group>"; };
 		382C134A25F14E3700715CE1 /* BGTargets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGTargets.swift; sourceTree = "<group>"; };
+		383420D525FFE38C002D46C1 /* LoopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopView.swift; sourceTree = "<group>"; };
+		383420D825FFEB3F002D46C1 /* Popup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Popup.swift; sourceTree = "<group>"; };
 		383948D525CD4D8900E91849 /* FileStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileStorage.swift; sourceTree = "<group>"; };
 		383948D925CD64D500E91849 /* Glucose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Glucose.swift; sourceTree = "<group>"; };
 		384E803325C385E60086DB71 /* JavaScriptWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JavaScriptWorker.swift; sourceTree = "<group>"; };
@@ -448,6 +453,7 @@
 		38A13D3125E28B4B00EAA382 /* PumpHistoryEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpHistoryEvent.swift; sourceTree = "<group>"; };
 		38A5049125DD9C4000C5B9E8 /* UserDefaultsExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtensions.swift; sourceTree = "<group>"; };
 		38A9260425F012D8009E3739 /* CarbRatios.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbRatios.swift; sourceTree = "<group>"; };
+		38AAF85425FFF846004AF583 /* CurrentGlucoseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentGlucoseView.swift; sourceTree = "<group>"; };
 		38AEE73C25F0200C0013F05B /* FreeAPSSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreeAPSSettings.swift; sourceTree = "<group>"; };
 		38AEE75125F022080013F05B /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = "<group>"; };
 		38AEE75625F0F18E0013F05B /* CarbsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbsStorage.swift; sourceTree = "<group>"; };
@@ -736,6 +742,8 @@
 			children = (
 				3811DE2E25C9D49500A708ED /* HomeRootView.swift */,
 				389FE32925F3AC44002E92E0 /* GlucoseChartView.swift */,
+				383420D525FFE38C002D46C1 /* LoopView.swift */,
+				38AAF85425FFF846004AF583 /* CurrentGlucoseView.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -980,6 +988,7 @@
 			children = (
 				3811DE5925C9D4D500A708ED /* ViewModifiers.swift */,
 				3883581B25EE79BB00E024B2 /* DecimalTextField.swift */,
+				383420D825FFEB3F002D46C1 /* Popup.swift */,
 			);
 			path = Views;
 			sourceTree = "<group>";
@@ -1690,6 +1699,7 @@
 				3811DE6E25C9D62600A708ED /* OnboardingViewModel.swift in Sources */,
 				38B4F3CD25E5031100E76A18 /* Broadcaster.swift in Sources */,
 				3811DE6D25C9D62600A708ED /* OnboardingRootView.swift in Sources */,
+				383420D925FFEB3F002D46C1 /* Popup.swift in Sources */,
 				3811DE3025C9D49500A708ED /* HomeViewModel.swift in Sources */,
 				3811DF0525CAA62600A708ED /* DependeciesContainer.swift in Sources */,
 				38BF021725E7CBBC00579895 /* PumpManagerExtensions.swift in Sources */,
@@ -1746,6 +1756,7 @@
 				38B4F3AF25E2979F00E76A18 /* IndexedCollection.swift in Sources */,
 				3811DEAE25C9D88300A708ED /* Cache.swift in Sources */,
 				6610FA2425FAED29004781D7 /* PredictionsChartView.swift in Sources */,
+				383420D625FFE38C002D46C1 /* LoopView.swift in Sources */,
 				3811DEAD25C9D88300A708ED /* UserDefaults+Cache.swift in Sources */,
 				3811DE2225C9D48300A708ED /* MainProvider.swift in Sources */,
 				3811DE0C25C9D32F00A708ED /* BaseProvider.swift in Sources */,
@@ -1868,6 +1879,7 @@
 				8BC2F5A29AD1ED08AC0EE013 /* AddTempTargetRootView.swift in Sources */,
 				38A00B1F25FC00F7006BC0B0 /* Autotune.swift in Sources */,
 				19434C14DF3F4816F4E4BF2E /* BolusBuilder.swift in Sources */,
+				38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */,
 				041D1E995A6AE92E9289DC49 /* BolusDataFlow.swift in Sources */,
 				6610FA1E25FAED29004781D7 /* MeshEntryOrientations.swift in Sources */,
 				23888883D4EA091C88480FF2 /* BolusProvider.swift in Sources */,

+ 38 - 0
FreeAPS/Resources/Assets.xcassets/LoopGreen.colorset/Contents.json

@@ -0,0 +1,38 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0.592",
+          "green" : "0.812",
+          "red" : "0.435"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0.592",
+          "green" : "0.812",
+          "red" : "0.435"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 38 - 0
FreeAPS/Resources/Assets.xcassets/LoopGrey.colorset/Contents.json

@@ -0,0 +1,38 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0.741",
+          "green" : "0.741",
+          "red" : "0.741"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0.741",
+          "green" : "0.741",
+          "red" : "0.741"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 38 - 0
FreeAPS/Resources/Assets.xcassets/LoopRed.colorset/Contents.json

@@ -0,0 +1,38 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0.341",
+          "green" : "0.341",
+          "red" : "0.922"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0.341",
+          "green" : "0.341",
+          "red" : "0.922"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 38 - 0
FreeAPS/Resources/Assets.xcassets/LoopYellow.colorset/Contents.json

@@ -0,0 +1,38 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0.298",
+          "green" : "0.788",
+          "red" : "0.949"
+        }
+      },
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0.298",
+          "green" : "0.788",
+          "red" : "0.949"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 17 - 3
FreeAPS/Sources/Modules/Home/HomeViewModel.swift

@@ -6,16 +6,20 @@ extension Home {
         @Injected() var broadcaster: Broadcaster!
         @Injected() var settingsManager: SettingsManager!
 
-        private(set) var filteredGlucoseHours = 3
+        private(set) var filteredGlucoseHours = 24
 
         @Published var glucose: [BloodGlucose] = []
         @Published var suggestion: Suggestion?
+        @Published var recentGlucose: BloodGlucose?
+        @Published var glucoseDelta: Int?
 
         @Published var allowManualTemp = false
+        private(set) var units: GlucoseUnits = .mmolL
 
         override func subscribe() {
-            glucose = provider.filteredGlucose(hours: filteredGlucoseHours)
+            setupGlucose()
             suggestion = provider.suggestion
+            units = settingsManager.settings.units
             allowManualTemp = !settingsManager.settings.closedLoop
             broadcaster.register(GlucoseObserver.self, observer: self)
             broadcaster.register(SuggestionObserver.self, observer: self)
@@ -49,12 +53,22 @@ extension Home {
         func setFilteredGlucoseHours(hours: Int) {
             filteredGlucoseHours = hours
         }
+
+        private func setupGlucose() {
+            glucose = provider.filteredGlucose(hours: filteredGlucoseHours)
+            recentGlucose = glucose.last
+            if glucose.count >= 2 {
+                glucoseDelta = (recentGlucose?.glucose ?? 0) - (glucose[glucose.count - 2].glucose ?? 0)
+            } else {
+                glucoseDelta = nil
+            }
+        }
     }
 }
 
 extension Home.ViewModel: GlucoseObserver, SuggestionObserver, SettingsObserver {
     func glucoseDidUpdate(_: [BloodGlucose]) {
-        glucose = provider.filteredGlucose(hours: filteredGlucoseHours)
+        setupGlucose()
     }
 
     func suggestionDidUpdate(_ suggestion: Suggestion) {

+ 85 - 0
FreeAPS/Sources/Modules/Home/View/CurrentGlucoseView.swift

@@ -0,0 +1,85 @@
+import SwiftUI
+
+struct CurrentGlucoseView: View {
+    @Binding var recentGlucose: BloodGlucose?
+    @Binding var delta: Int?
+    let units: GlucoseUnits
+
+    private var glucoseFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 1
+        return formatter
+    }
+
+    private var deltaFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 2
+        formatter.positivePrefix = "+"
+        return formatter
+    }
+
+    private var dateFormatter: DateFormatter {
+        let formatter = DateFormatter()
+        formatter.timeStyle = .short
+        return formatter
+    }
+
+    var body: some View {
+        HStack {
+            VStack {
+                Text(
+                    recentGlucose?.glucose
+                        .map { glucoseFormatter.string(from: Double(units == .mmolL ? $0.asMmolL : Decimal($0)) as NSNumber)! } ??
+                        "--"
+                )
+                .font(.largeTitle)
+                Spacer()
+                Text(
+                    recentGlucose.map { dateFormatter.string(from: $0.dateString) } ?? "--"
+                ).font(.caption)
+            }
+            VStack {
+                Spacer()
+                image.padding(.bottom, 2)
+                Text(
+                    delta
+                        .map { deltaFormatter.string(from: Double(units == .mmolL ? $0.asMmolL : Decimal($0)) as NSNumber)!
+                        } ??
+                        "--"
+
+                ).font(.caption)
+                Text("\(units.rawValue)").font(.caption2)
+            }
+        }.frame(minWidth: 120)
+    }
+
+    var image: Image {
+        guard let direction = recentGlucose?.direction else {
+            return Image(systemName: "arrow.left.and.right")
+        }
+
+        switch direction {
+        case .doubleUp,
+             .singleUp,
+             .tripleUp:
+            return Image(systemName: "arrow.up")
+        case .fortyFiveUp:
+            return Image(systemName: "arrow.up.right")
+        case .flat:
+            return Image(systemName: "arrow.forward")
+        case .fortyFiveDown:
+            return Image(systemName: "arrow.down.forward")
+        case .doubleDown,
+             .singleDown,
+             .tripleDown:
+            return Image(systemName: "arrow.down")
+
+        case .none,
+             .notComputable,
+             .rateOutOfRange:
+            return Image(systemName: "arrow.left.and.right")
+        }
+    }
+}

+ 28 - 7
FreeAPS/Sources/Modules/Home/View/GlucoseChartView.swift

@@ -9,19 +9,37 @@ extension DateFormatter: AxisValueFormatter {
     }
 }
 
+extension NumberFormatter: AxisValueFormatter {
+    public func stringForValue(_ value: Double, axis _: AxisBase?) -> String {
+        numberStyle = .decimal
+        maximumFractionDigits = 1
+        return string(from: value as NSNumber)!
+    }
+}
+
 struct GlucoseChartView: UIViewRepresentable {
     @Binding var glucose: [BloodGlucose]
     @Binding var suggestion: Suggestion?
+    let units: GlucoseUnits
 
     func makeUIView(context _: Context) -> LineChartView {
         let view = LineChartView()
         makeDataPointsFor(view: view)
         view.xAxis.valueFormatter = DateFormatter()
+        view.leftAxis.valueFormatter = NumberFormatter()
+        view.xAxis.labelPosition = .top
+        view.rightAxis.drawLabelsEnabled = false
+        view.drawBordersEnabled = true
+        view.setScaleEnabled(false)
+        view.setVisibleXRangeMaximum(6.hours.timeInterval)
+        view.xAxis.granularityEnabled = true
+        view.xAxis.granularity = 1.hours.timeInterval
         return view
     }
 
     func updateUIView(_ view: LineChartView, context _: Context) {
         makeDataPointsFor(view: view)
+        view.moveViewToX(glucose.last?.dateString.timeIntervalSince1970 ?? 0)
     }
 
     private func makeDataPointsFor(view: LineChartView) {
@@ -30,14 +48,17 @@ struct GlucoseChartView: UIViewRepresentable {
         }
 
         let dataPoints = glucose.map {
-            ChartDataEntry(x: $0.dateString.timeIntervalSince1970, y: Double($0.sgv ?? 0))
+            ChartDataEntry(
+                x: $0.dateString.timeIntervalSince1970,
+                y: Double($0.sgv ?? 0) * (units == .mmolL ? Double(GlucoseUnits.exchangeRate) : 1)
+            )
         }
 
         let data = MyLineChartDataSet(entries: dataPoints, label: "BG")
         data.drawCirclesEnabled = true
         data.circleRadius = 2
-        data.setCircleColor(.green)
-        data.setColor(.green)
+        data.setCircleColor(UIColor(named: "LoopGreen")!)
+        data.setColor(UIColor(named: "LoopGreen")!)
         data.lineWidth = 0
         data.drawValuesEnabled = false
 
@@ -49,7 +70,7 @@ struct GlucoseChartView: UIViewRepresentable {
             let dataPoints = iob.enumerated().map {
                 ChartDataEntry(
                     x: lastDate.addingTimeInterval(Double($0 * 300)).timeIntervalSince1970,
-                    y: Double($1)
+                    y: Double($1) * (units == .mmolL ? Double(GlucoseUnits.exchangeRate) : 1)
                 )
             }
             let data = MyLineChartDataSet(entries: dataPoints, label: "IOB")
@@ -66,7 +87,7 @@ struct GlucoseChartView: UIViewRepresentable {
             let dataPoints = zt.enumerated().map {
                 ChartDataEntry(
                     x: lastDate.addingTimeInterval(Double($0 * 300)).timeIntervalSince1970,
-                    y: Double($1)
+                    y: Double($1) * (units == .mmolL ? Double(GlucoseUnits.exchangeRate) : 1)
                 )
             }
             let data = MyLineChartDataSet(entries: dataPoints, label: "ZT")
@@ -83,7 +104,7 @@ struct GlucoseChartView: UIViewRepresentable {
             let dataPoints = cob.enumerated().map {
                 ChartDataEntry(
                     x: lastDate.addingTimeInterval(Double($0 * 300)).timeIntervalSince1970,
-                    y: Double($1)
+                    y: Double($1) * (units == .mmolL ? Double(GlucoseUnits.exchangeRate) : 1)
                 )
             }
             let data = MyLineChartDataSet(entries: dataPoints, label: "COB")
@@ -100,7 +121,7 @@ struct GlucoseChartView: UIViewRepresentable {
             let dataPoints = uam.enumerated().map {
                 ChartDataEntry(
                     x: lastDate.addingTimeInterval(Double($0 * 300)).timeIntervalSince1970,
-                    y: Double($1)
+                    y: Double($1) * (units == .mmolL ? Double(GlucoseUnits.exchangeRate) : 1)
                 )
             }
             let data = MyLineChartDataSet(entries: dataPoints, label: "UAM")

+ 46 - 51
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -1,69 +1,54 @@
+import SwiftDate
 import SwiftUI
 
 extension Home {
     struct RootView: BaseView {
         @EnvironmentObject var viewModel: ViewModel<Provider>
-        @State var showHours = 1
+        @State var isPopupPresented = false
 
-        var mainChart: some View {
-            GeometryReader { geo in
-                ScrollView(.horizontal, showsIndicators: false) {
-                    CombinedChartView(
-                        maxWidth: geo.size.width,
-                        showHours: showHours,
-                        glucoseData: $viewModel.glucose,
-                        predictionsData: .constant([]),
-                        mode: .dots
-                    )
-                }
-            }
-            .padding(.vertical)
-            .background(Color(.systemGray6))
-            .cornerRadius(12)
+        private var numberFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            return formatter
         }
 
-        var previewChart: some View {
-            GeometryReader { geo in
-                CombinedChartView(
-                    maxWidth: geo.size.width,
-                    showHours: 24,
-                    glucoseData: $viewModel.glucose,
-                    predictionsData: .constant([]),
-                    mode: .line
+        var header: some View {
+            HStack {
+                VStack(alignment: .leading) {
+                    HStack {
+                        Text("IOB").font(.caption)
+                        Text((numberFormatter.string(from: (viewModel.suggestion?.iob ?? 0) as NSNumber) ?? "0") + " U")
+                            .font(.caption2)
+                    }.padding(.top, 16)
+                    Spacer()
+                    HStack {
+                        Text("COB").font(.caption)
+                        Text((numberFormatter.string(from: (viewModel.suggestion?.cob ?? 0) as NSNumber) ?? "0") + " g")
+                            .font(.caption2)
+                    }
+                }
+                Spacer()
+                CurrentGlucoseView(
+                    recentGlucose: $viewModel.recentGlucose,
+                    delta: $viewModel.glucoseDelta,
+                    units: viewModel.units
                 )
-            }
-            .frame(maxWidth: .infinity)
-            .padding(.vertical)
-            .background(Color(.systemGray6))
-            .cornerRadius(10)
-            .drawingGroup()
+                .padding(.horizontal)
+                LoopView(suggestion: $viewModel.suggestion).onTapGesture {
+                    isPopupPresented = true
+                }.onLongPressGesture {
+                    viewModel.runLoop()
+                }
+            }.frame(maxWidth: .infinity)
         }
 
         var body: some View {
             viewModel.setFilteredGlucoseHours(hours: 24)
             return GeometryReader { geo in
                 VStack {
-                    Group {
-                        Text("Header")
-                    }
-                    ScrollView(.vertical, showsIndicators: false) {
-                        HoursPickerView(selectedHour: $showHours).padding(.horizontal)
-
-                        mainChart
-                            .frame(height: geo.size.height * 0.6)
-                            .padding(.horizontal)
-
-                        previewChart
-                            .frame(height: 50)
-                            .padding(.horizontal)
-                        // GlucoseChartView(glucose: $viewModel.glucose, suggestion: $viewModel.suggestion).frame(height: 150)
-                        if let reason = viewModel.suggestion?.reason {
-                            Text(reason).font(.caption).padding()
-                        }
-                        Button(action: viewModel.runLoop) {
-                            Text("Run loop now").buttonBackground().padding()
-                        }.foregroundColor(.white)
-                    }
+                    header.padding().frame(maxHeight: 70)
+                    GlucoseChartView(glucose: $viewModel.glucose, suggestion: $viewModel.suggestion, units: viewModel.units)
+                        .frame(maxHeight: .infinity)
 
                     ZStack {
                         Rectangle().fill(Color.gray.opacity(0.2)).frame(height: 50 + geo.safeAreaInsets.bottom)
@@ -105,6 +90,16 @@ extension Home {
             .navigationTitle("Home")
             .navigationBarHidden(true)
             .ignoresSafeArea(.keyboard)
+            .popup(isPresented: isPopupPresented, alignment: .top, direction: .top) {
+                Text(viewModel.suggestion?.reason ?? "No sugestion found").font(.caption).padding().foregroundColor(.white)
+                    .background(
+                        RoundedRectangle(cornerRadius: 8, style: .continuous)
+                            .fill(Color(UIColor.darkGray))
+                    )
+                    .onTapGesture {
+                        isPopupPresented = false
+                    }
+            }
         }
     }
 }

+ 40 - 0
FreeAPS/Sources/Modules/Home/View/LoopView.swift

@@ -0,0 +1,40 @@
+import SwiftDate
+import SwiftUI
+import UIKit
+
+struct LoopView: View {
+    @Binding var suggestion: Suggestion?
+
+    private var dateFormatter: DateFormatter {
+        let formatter = DateFormatter()
+        formatter.timeStyle = .short
+        return formatter
+    }
+
+    var body: some View {
+        VStack {
+            Circle().strokeBorder(color, lineWidth: 6).frame(width: 38, height: 38)
+            Spacer()
+            if let date = suggestion?.deliverAt {
+                Text(dateFormatter.string(from: date)).font(.caption)
+            } else {
+                Text("--").font(.caption)
+            }
+        }
+    }
+
+    var color: Color {
+        guard let lastDate = suggestion?.deliverAt else {
+            return Color(UIColor(named: "LoopGrey")!)
+        }
+        let delta = Date().timeIntervalSince(lastDate)
+
+        if delta <= 5.minutes.timeInterval {
+            return Color(UIColor(named: "LoopGreen")!)
+        } else if delta <= 10.minutes.timeInterval {
+            return Color(UIColor(named: "LoopYellow")!)
+        } else {
+            return Color(UIColor(named: "LoopRed")!)
+        }
+    }
+}

+ 66 - 0
FreeAPS/Sources/Views/Popup.swift

@@ -0,0 +1,66 @@
+import SwiftUI
+
+struct Popup<T: View>: ViewModifier {
+    let popup: T
+    let isPresented: Bool
+    let alignment: Alignment
+    let direction: Direction
+
+    init(isPresented: Bool, alignment: Alignment, direction: Direction, @ViewBuilder content: () -> T) {
+        self.isPresented = isPresented
+        self.alignment = alignment
+        self.direction = direction
+        popup = content()
+    }
+
+    func body(content: Content) -> some View {
+        content
+            .overlay(popupContent())
+    }
+
+    @ViewBuilder private func popupContent() -> some View {
+        GeometryReader { geometry in
+            if isPresented {
+                popup
+                    .animation(.spring())
+                    .transition(.offset(x: 0, y: direction.offset(popupFrame: geometry.frame(in: .global))))
+                    .frame(width: geometry.size.width, height: geometry.size.height, alignment: alignment)
+            }
+        }
+    }
+}
+
+private extension GeometryProxy {
+    var belowScreenEdge: CGFloat {
+        UIScreen.main.bounds.height - frame(in: .global).minY
+    }
+}
+
+extension Popup {
+    enum Direction {
+        case top
+        case bottom
+
+        func offset(popupFrame: CGRect) -> CGFloat {
+            switch self {
+            case .top:
+                let aboveScreenEdge = -popupFrame.maxY
+                return aboveScreenEdge
+            case .bottom:
+                let belowScreenEdge = UIScreen.main.bounds.height - popupFrame.minY
+                return belowScreenEdge
+            }
+        }
+    }
+}
+
+extension View {
+    func popup<T: View>(
+        isPresented: Bool,
+        alignment: Alignment = .center,
+        direction: Popup<T>.Direction = .bottom,
+        @ViewBuilder content: () -> T
+    ) -> some View {
+        modifier(Popup(isPresented: isPresented, alignment: alignment, direction: direction, content: content))
+    }
+}