|
@@ -6,7 +6,12 @@ extension Bolus {
|
|
|
let resolver: Resolver
|
|
let resolver: Resolver
|
|
|
let waitForSuggestion: Bool
|
|
let waitForSuggestion: Bool
|
|
|
@StateObject var state = StateModel()
|
|
@StateObject var state = StateModel()
|
|
|
|
|
+
|
|
|
@State private var isAddInsulinAlertPresented = false
|
|
@State private var isAddInsulinAlertPresented = false
|
|
|
|
|
+ @State private var presentInfo = false
|
|
|
|
|
+ @State private var displayError = false
|
|
|
|
|
+
|
|
|
|
|
+ @Environment(\.colorScheme) var colorScheme
|
|
|
|
|
|
|
|
private var formatter: NumberFormatter {
|
|
private var formatter: NumberFormatter {
|
|
|
let formatter = NumberFormatter()
|
|
let formatter = NumberFormatter()
|
|
@@ -15,9 +20,15 @@ extension Bolus {
|
|
|
return formatter
|
|
return formatter
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private var fractionDigits: Int {
|
|
|
|
|
+ if state.units == .mmolL {
|
|
|
|
|
+ return 1
|
|
|
|
|
+ } else { return 0 }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
var body: some View {
|
|
var body: some View {
|
|
|
Form {
|
|
Form {
|
|
|
- Section(header: Text("Recommendation")) {
|
|
|
|
|
|
|
+ Section {
|
|
|
if state.waitForSuggestion {
|
|
if state.waitForSuggestion {
|
|
|
HStack {
|
|
HStack {
|
|
|
Text("Wait please").foregroundColor(.secondary)
|
|
Text("Wait please").foregroundColor(.secondary)
|
|
@@ -26,34 +37,31 @@ extension Bolus {
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
HStack {
|
|
HStack {
|
|
|
- Text("Insulin required").foregroundColor(.secondary)
|
|
|
|
|
- Spacer()
|
|
|
|
|
- Text(
|
|
|
|
|
- formatter
|
|
|
|
|
- .string(from: state.insulinRequired as NSNumber)! +
|
|
|
|
|
- NSLocalizedString(" U", comment: "Insulin unit")
|
|
|
|
|
- ).foregroundColor(.secondary)
|
|
|
|
|
- }.contentShape(Rectangle())
|
|
|
|
|
- .onTapGesture {
|
|
|
|
|
- state.amount = state.insulinRequired
|
|
|
|
|
- }
|
|
|
|
|
- HStack {
|
|
|
|
|
Text("Insulin recommended")
|
|
Text("Insulin recommended")
|
|
|
Spacer()
|
|
Spacer()
|
|
|
Text(
|
|
Text(
|
|
|
formatter
|
|
formatter
|
|
|
.string(from: state.insulinRecommended as NSNumber)! +
|
|
.string(from: state.insulinRecommended as NSNumber)! +
|
|
|
NSLocalizedString(" U", comment: "Insulin unit")
|
|
NSLocalizedString(" U", comment: "Insulin unit")
|
|
|
- ).foregroundColor(.secondary)
|
|
|
|
|
|
|
+ ).foregroundColor((state.error && state.insulinRecommended > 0) ? .red : .secondary)
|
|
|
}.contentShape(Rectangle())
|
|
}.contentShape(Rectangle())
|
|
|
.onTapGesture {
|
|
.onTapGesture {
|
|
|
- state.amount = state.insulinRecommended
|
|
|
|
|
|
|
+ if state.error, state.insulinRecommended > 0 { displayError = true }
|
|
|
|
|
+ else { state.amount = state.insulinRecommended }
|
|
|
}
|
|
}
|
|
|
|
|
+ HStack {
|
|
|
|
|
+ Image(systemName: "info.bubble").symbolRenderingMode(.palette).foregroundStyle(
|
|
|
|
|
+ .primary, .blue
|
|
|
|
|
+ )
|
|
|
|
|
+ }.onTapGesture {
|
|
|
|
|
+ presentInfo.toggle()
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ header: { Text("Recommendation") }
|
|
|
|
|
|
|
|
if !state.waitForSuggestion {
|
|
if !state.waitForSuggestion {
|
|
|
- Section(header: Text("Bolus")) {
|
|
|
|
|
|
|
+ Section {
|
|
|
HStack {
|
|
HStack {
|
|
|
Text("Amount")
|
|
Text("Amount")
|
|
|
Spacer()
|
|
Spacer()
|
|
@@ -67,13 +75,12 @@ extension Bolus {
|
|
|
Text("U").foregroundColor(.secondary)
|
|
Text("U").foregroundColor(.secondary)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+ header: { Text("Bolus") }
|
|
|
Section {
|
|
Section {
|
|
|
Button { state.add() }
|
|
Button { state.add() }
|
|
|
label: { Text("Enact bolus") }
|
|
label: { Text("Enact bolus") }
|
|
|
.disabled(state.amount <= 0)
|
|
.disabled(state.amount <= 0)
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
Section {
|
|
Section {
|
|
|
if waitForSuggestion {
|
|
if waitForSuggestion {
|
|
|
Button { state.showModal(for: nil) }
|
|
Button { state.showModal(for: nil) }
|
|
@@ -84,17 +91,56 @@ extension Bolus {
|
|
|
.disabled(state.amount <= 0)
|
|
.disabled(state.amount <= 0)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ .alert(isPresented: $isAddInsulinAlertPresented) {
|
|
|
|
|
+ Alert(
|
|
|
|
|
+ title: Text("Are you sure?"),
|
|
|
|
|
+ message: Text(
|
|
|
|
|
+ NSLocalizedString("Add", comment: "Add insulin without bolusing alert") + " " + formatter
|
|
|
|
|
+ .string(from: state.amount as NSNumber)! + NSLocalizedString(" U", comment: "Insulin unit") +
|
|
|
|
|
+ NSLocalizedString(" without bolusing", comment: "Add insulin without bolusing alert")
|
|
|
|
|
+ ),
|
|
|
|
|
+ primaryButton: .destructive(
|
|
|
|
|
+ Text("Add"),
|
|
|
|
|
+ action: {
|
|
|
|
|
+ state.addWithoutBolus()
|
|
|
|
|
+ isAddInsulinAlertPresented = false
|
|
|
|
|
+ }
|
|
|
|
|
+ ),
|
|
|
|
|
+ secondaryButton: .cancel()
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- .alert(isPresented: $isAddInsulinAlertPresented) {
|
|
|
|
|
- let amount = formatter
|
|
|
|
|
- .string(from: state.amount as NSNumber)! + NSLocalizedString(" U", comment: "Insulin unit")
|
|
|
|
|
- return Alert(
|
|
|
|
|
- title: Text("Are you sure?"),
|
|
|
|
|
- message: Text("Add \(amount) without bolusing"),
|
|
|
|
|
|
|
+ .alert(isPresented: $displayError) {
|
|
|
|
|
+ Alert(
|
|
|
|
|
+ title: Text("Warning!"),
|
|
|
|
|
+ message: Text("\n" + alertString() + NSLocalizedString(
|
|
|
|
|
+ "\n\nTap 'Add' to continue with selected amount.",
|
|
|
|
|
+ comment: "Alert text to confirm bolus amount to add"
|
|
|
|
|
+ )),
|
|
|
|
|
+ primaryButton: .destructive(
|
|
|
|
|
+ Text("Add"),
|
|
|
|
|
+ action: {
|
|
|
|
|
+ state.amount = state.insulinRecommended
|
|
|
|
|
+ displayError = false
|
|
|
|
|
+ }
|
|
|
|
|
+ ),
|
|
|
|
|
+ secondaryButton: .cancel()
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ .alert(isPresented: $displayError) {
|
|
|
|
|
+ Alert(
|
|
|
|
|
+ title: Text("Warning!"),
|
|
|
|
|
+ message: Text("\n" + alertString() + NSLocalizedString(
|
|
|
|
|
+ "\n\nTap 'Add' to continue with selected amount.",
|
|
|
|
|
+ comment: "Alert text to confirm bolus amount to add"
|
|
|
|
|
+ )),
|
|
|
primaryButton: .destructive(
|
|
primaryButton: .destructive(
|
|
|
Text("Add"),
|
|
Text("Add"),
|
|
|
- action: { state.addWithoutBolus() }
|
|
|
|
|
|
|
+ action: {
|
|
|
|
|
+ state.amount = state.insulinRecommended
|
|
|
|
|
+ displayError = false
|
|
|
|
|
+ }
|
|
|
),
|
|
),
|
|
|
secondaryButton: .cancel()
|
|
secondaryButton: .cancel()
|
|
|
)
|
|
)
|
|
@@ -108,11 +154,165 @@ extension Bolus {
|
|
|
.navigationTitle("Enact Bolus")
|
|
.navigationTitle("Enact Bolus")
|
|
|
.navigationBarTitleDisplayMode(.automatic)
|
|
.navigationBarTitleDisplayMode(.automatic)
|
|
|
.navigationBarItems(leading: Button("Close", action: state.hideModal))
|
|
.navigationBarItems(leading: Button("Close", action: state.hideModal))
|
|
|
|
|
+ .popup(isPresented: presentInfo, alignment: .center, direction: .bottom) {
|
|
|
|
|
+ bolusInfo
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var bolusInfo: some View {
|
|
|
|
|
+ VStack {
|
|
|
|
|
+ // Variables
|
|
|
|
|
+ VStack(spacing: 3) {
|
|
|
|
|
+ HStack {
|
|
|
|
|
+ Text("Eventual Glucose").foregroundColor(.secondary)
|
|
|
|
|
+ let evg = state.units == .mmolL ? Decimal(state.evBG).asMmolL : Decimal(state.evBG)
|
|
|
|
|
+ Text(evg.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))))
|
|
|
|
|
+ Text(state.units.rawValue).foregroundColor(.secondary)
|
|
|
|
|
+ }
|
|
|
|
|
+ HStack {
|
|
|
|
|
+ Text("Target Glucose").foregroundColor(.secondary)
|
|
|
|
|
+ let target = state.units == .mmolL ? state.target.asMmolL : state.target
|
|
|
|
|
+ Text(target.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))))
|
|
|
|
|
+ Text(state.units.rawValue).foregroundColor(.secondary)
|
|
|
|
|
+ }
|
|
|
|
|
+ HStack {
|
|
|
|
|
+ Text("ISF").foregroundColor(.secondary)
|
|
|
|
|
+ let isf = state.isf
|
|
|
|
|
+ Text(isf.formatted())
|
|
|
|
|
+ Text(state.units.rawValue + NSLocalizedString("/U", comment: "/Insulin unit"))
|
|
|
|
|
+ .foregroundColor(.secondary)
|
|
|
|
|
+ }
|
|
|
|
|
+ HStack {
|
|
|
|
|
+ Text("ISF:")
|
|
|
|
|
+ Text("Insulin Sensitivity")
|
|
|
|
|
+ }.foregroundColor(.secondary).italic()
|
|
|
|
|
+ if state.percentage != 100 {
|
|
|
|
|
+ HStack {
|
|
|
|
|
+ Text("Percentage setting").foregroundColor(.secondary)
|
|
|
|
|
+ let percentage = state.percentage
|
|
|
|
|
+ Text(percentage.formatted())
|
|
|
|
|
+ Text("%").foregroundColor(.secondary)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ HStack {
|
|
|
|
|
+ Text("Formula =")
|
|
|
|
|
+ Text("(Eventual Glucose - Target) / ISF")
|
|
|
|
|
+ }.foregroundColor(.secondary).italic().padding(.top, 5)
|
|
|
|
|
+ }
|
|
|
|
|
+ .font(.footnote)
|
|
|
|
|
+ .padding(.top, 10)
|
|
|
|
|
+ Divider()
|
|
|
|
|
+ // Formula
|
|
|
|
|
+ VStack(spacing: 5) {
|
|
|
|
|
+ let unit = NSLocalizedString(
|
|
|
|
|
+ " U",
|
|
|
|
|
+ comment: "Unit in number of units delivered (keep the space character!)"
|
|
|
|
|
+ )
|
|
|
|
|
+ let color: Color = (state.percentage != 100 && state.insulin > 0) ? .secondary : .blue
|
|
|
|
|
+ let fontWeight: Font.Weight = (state.percentage != 100 && state.insulin > 0) ? .regular : .bold
|
|
|
|
|
+ HStack {
|
|
|
|
|
+ Text(NSLocalizedString("Insulin recommended", comment: "") + ":").font(.callout)
|
|
|
|
|
+ Text(state.insulin.formatted() + unit).font(.callout).foregroundColor(color).fontWeight(fontWeight)
|
|
|
|
|
+ }
|
|
|
|
|
+ if state.percentage != 100, state.insulin > 0 {
|
|
|
|
|
+ Divider()
|
|
|
|
|
+ HStack { Text(state.percentage.formatted() + " % ->").font(.callout).foregroundColor(.secondary)
|
|
|
|
|
+ Text(
|
|
|
|
|
+ state.insulinRecommended.formatted() + unit
|
|
|
|
|
+ ).font(.callout).foregroundColor(.blue).bold()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // Warning
|
|
|
|
|
+ if state.error, state.insulinRecommended > 0 {
|
|
|
|
|
+ VStack(spacing: 5) {
|
|
|
|
|
+ Divider()
|
|
|
|
|
+ Text("Warning!").font(.callout).bold().foregroundColor(.orange)
|
|
|
|
|
+ Text(alertString()).font(.footnote)
|
|
|
|
|
+ Divider()
|
|
|
|
|
+ }.padding(.horizontal, 10)
|
|
|
|
|
+ }
|
|
|
|
|
+ // Footer
|
|
|
|
|
+ if !(state.error && state.insulinRecommended > 0) {
|
|
|
|
|
+ VStack {
|
|
|
|
|
+ Text(
|
|
|
|
|
+ "Carbs and previous insulin are included in the glucose prediction, but if the Eventual Glucose is lower than the Target Glucose, a bolus will not be recommended."
|
|
|
|
|
+ ).font(.caption2).foregroundColor(.secondary)
|
|
|
|
|
+ }.padding(20)
|
|
|
|
|
+ }
|
|
|
|
|
+ // Hide button
|
|
|
|
|
+ VStack {
|
|
|
|
|
+ Button { presentInfo = false }
|
|
|
|
|
+ label: { Text("Hide") }.frame(maxWidth: .infinity, alignment: .center).font(.callout)
|
|
|
|
|
+ .foregroundColor(.blue)
|
|
|
|
|
+ }.padding(.bottom, 10)
|
|
|
|
|
+ }
|
|
|
|
|
+ .background(
|
|
|
|
|
+ RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
|
|
|
+ .fill(Color(colorScheme == .dark ? UIColor.systemGray4 : UIColor.systemGray4))
|
|
|
|
|
+ // .fill(Color(.systemGray).gradient) // A more prominent pop-up, but harder to read
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Localize the Oref0 error/warning strings. The default should never be returned
|
|
|
|
|
+ private func alertString() -> String {
|
|
|
|
|
+ switch state.errorString {
|
|
|
|
|
+ case 1,
|
|
|
|
|
+ 2:
|
|
|
|
|
+ return NSLocalizedString(
|
|
|
|
|
+ "Eventual Glucose > Target Glucose, but glucose is predicted to first drop down to ",
|
|
|
|
|
+ comment: "Bolus pop-up / Alert string. Make translations concise!"
|
|
|
|
|
+ ) + state.minGuardBG
|
|
|
|
|
+ .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) + " " + state.units
|
|
|
|
|
+ .rawValue + ", " +
|
|
|
|
|
+ NSLocalizedString(
|
|
|
|
|
+ "which is below your Threshold (",
|
|
|
|
|
+ comment: "Bolus pop-up / Alert string. Make translations concise!"
|
|
|
|
|
+ ) + state
|
|
|
|
|
+ .threshold.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) + ")"
|
|
|
|
|
+ case 3:
|
|
|
|
|
+ return NSLocalizedString(
|
|
|
|
|
+ "Eventual Glucose > Target Glucose, but glucose is climbing slower than expected. Expected: ",
|
|
|
|
|
+ comment: "Bolus pop-up / Alert string. Make translations concise!"
|
|
|
|
|
+ ) +
|
|
|
|
|
+ state.expectedDelta
|
|
|
|
|
+ .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
|
|
|
|
|
+ NSLocalizedString(". Climbing: ", comment: "Bolus pop-up / Alert string. Make translatons concise!") + state
|
|
|
|
|
+ .minDelta.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
|
|
|
|
|
+ case 4:
|
|
|
|
|
+ return NSLocalizedString(
|
|
|
|
|
+ "Eventual Glucose > Target Glucose, but glucose is falling faster than expected. Expected: ",
|
|
|
|
|
+ comment: "Bolus pop-up / Alert string. Make translations concise!"
|
|
|
|
|
+ ) +
|
|
|
|
|
+ state.expectedDelta
|
|
|
|
|
+ .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
|
|
|
|
|
+ NSLocalizedString(". Falling: ", comment: "Bolus pop-up / Alert string. Make translations concise!") + state
|
|
|
|
|
+ .minDelta.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
|
|
|
|
|
+ case 5:
|
|
|
|
|
+ return NSLocalizedString(
|
|
|
|
|
+ "Eventual Glucose > Target Glucose, but glucose is changing faster than expected. Expected: ",
|
|
|
|
|
+ comment: "Bolus pop-up / Alert string. Make translations concise!"
|
|
|
|
|
+ ) +
|
|
|
|
|
+ state.expectedDelta
|
|
|
|
|
+ .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
|
|
|
|
|
+ NSLocalizedString(". Changing: ", comment: "Bolus pop-up / Alert string. Make translations concise!") + state
|
|
|
|
|
+ .minDelta.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
|
|
|
|
|
+ case 6:
|
|
|
|
|
+ return NSLocalizedString(
|
|
|
|
|
+ "Eventual Glucose > Target Glucose, but glucose is predicted to first drop down to ",
|
|
|
|
|
+ comment: "Bolus pop-up / Alert string. Make translations concise!"
|
|
|
|
|
+ ) + state
|
|
|
|
|
+ .minPredBG
|
|
|
|
|
+ .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) + " " + state
|
|
|
|
|
+ .units
|
|
|
|
|
+ .rawValue
|
|
|
|
|
+ default:
|
|
|
|
|
+ return "Ignore Warning..."
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// fix iOS 15 bug
|
|
|
|
|
struct ActivityIndicator: UIViewRepresentable {
|
|
struct ActivityIndicator: UIViewRepresentable {
|
|
|
@Binding var isAnimating: Bool
|
|
@Binding var isAnimating: Bool
|
|
|
let style: UIActivityIndicatorView.Style
|
|
let style: UIActivityIndicatorView.Style
|